Skip to content

Commit 62d2c64

Browse files
authored
Load full thread history on session resume (#208)
* Load full thread history on session resume Closes #206 * Preserve failed tool call status in history fallback * Restore agent reasoning from event history * Test thread read during session load * Merge fallback history with parsed thread updates
1 parent 6ef8a4e commit 62d2c64

9 files changed

Lines changed: 2001 additions & 52 deletions

src/CodexAcpClient.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,14 +237,18 @@ export class CodexAcpClient {
237237
threadId: request.sessionId,
238238
});
239239
onSubscribed?.();
240+
const historyResponse = await this.codexClient.threadRead({
241+
threadId: response.thread.id,
242+
includeTurns: true,
243+
});
240244
const codexModels = await this.fetchAvailableModels();
241245
const currentModelId = this.createModelId(codexModels, response.model, response.reasoningEffort).toString();
242246
return {
243247
sessionId: request.sessionId,
244248
currentModelId: currentModelId,
245249
models: codexModels,
246250
currentServiceTier: response.serviceTier as ServiceTier ?? null,
247-
thread: response.thread,
251+
thread: historyResponse.thread,
248252
additionalDirectories,
249253
};
250254
}

src/CodexAcpServer.ts

Lines changed: 92 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {CodexCommands} from "./CodexCommands";
3333
import type {QuotaMeta} from "./QuotaMeta";
3434
import {logger} from "./Logger";
3535
import {sanitizeMcpServerName} from "./McpServerName";
36+
import {createResponseItemHistoryFallbackUpdates} from "./ResponseItemHistoryFallback";
3637
import {
3738
type LegacyLoadSessionResponse,
3839
type LegacyNewSessionResponse,
@@ -44,6 +45,7 @@ import {
4445
LEGACY_SET_SESSION_MODEL_METHOD,
4546
} from "./AcpExtensions";
4647
import {
48+
createCommandExecutionCompleteUpdate,
4749
createCommandExecutionUpdate,
4850
createDynamicToolCallUpdate,
4951
createFileChangeUpdate,
@@ -853,17 +855,29 @@ export class CodexAcpServer {
853855

854856
private async streamThreadHistory(sessionId: string, thread: Thread): Promise<void> {
855857
const session = new ACPSessionConnection(this.connection, sessionId);
858+
const sessionState = this.getSessionState(sessionId);
859+
const responseItemFallbackUpdates = await createResponseItemHistoryFallbackUpdates(
860+
thread,
861+
sessionState.terminalOutputMode,
862+
);
863+
864+
const threadUpdates: UpdateSessionEvent[] = [];
856865
for (const turn of thread.turns) {
857866
for (const item of turn.items) {
858-
const updates = await this.createHistoryUpdates(item);
859-
for (const update of updates) {
860-
await session.update(update);
861-
}
867+
const updates = await this.createHistoryUpdates(item, sessionState);
868+
threadUpdates.push(...updates);
862869
}
863870
}
871+
872+
const updates = responseItemFallbackUpdates
873+
? mergeHistoryUpdates(responseItemFallbackUpdates, threadUpdates)
874+
: threadUpdates;
875+
for (const update of updates) {
876+
await session.update(update);
877+
}
864878
}
865879

866-
private async createHistoryUpdates(item: ThreadItem): Promise<UpdateSessionEvent[]> {
880+
private async createHistoryUpdates(item: ThreadItem, sessionState: SessionState): Promise<UpdateSessionEvent[]> {
867881
switch (item.type) {
868882
case "userMessage":
869883
return this.createUserMessageUpdates(item);
@@ -880,8 +894,14 @@ export class CodexAcpServer {
880894
return this.createReasoningUpdates(item);
881895
case "fileChange":
882896
return [await createFileChangeUpdate(item)];
883-
case "commandExecution":
884-
return [await createCommandExecutionUpdate(item)];
897+
case "commandExecution": {
898+
const updates = [await createCommandExecutionUpdate(item)];
899+
const completeUpdate = createCommandExecutionCompleteUpdate(item, sessionState.terminalOutputMode);
900+
if (completeUpdate) {
901+
updates.push(completeUpdate);
902+
}
903+
return updates;
904+
}
885905
case "mcpToolCall":
886906
return [await createMcpToolCallUpdate(item)];
887907
case "dynamicToolCall":
@@ -1481,6 +1501,71 @@ export class CodexAcpServer {
14811501
}
14821502
}
14831503

1504+
function mergeHistoryUpdates(
1505+
responseItemFallbackUpdates: UpdateSessionEvent[],
1506+
threadUpdates: UpdateSessionEvent[],
1507+
): UpdateSessionEvent[] {
1508+
const merged: UpdateSessionEvent[] = [];
1509+
const seen = new Set<string>();
1510+
let fallbackIndex = 0;
1511+
1512+
const pushUpdate = (update: UpdateSessionEvent) => {
1513+
const key = historyUpdateKey(update);
1514+
if (key && seen.has(key)) {
1515+
return;
1516+
}
1517+
if (key) {
1518+
seen.add(key);
1519+
}
1520+
merged.push(update);
1521+
};
1522+
1523+
const flushFallbackThrough = (targetKey: string): boolean => {
1524+
const matchIndex = responseItemFallbackUpdates.findIndex((update, index) => (
1525+
index >= fallbackIndex && historyUpdateKey(update) === targetKey
1526+
));
1527+
if (matchIndex === -1) {
1528+
return false;
1529+
}
1530+
1531+
while (fallbackIndex <= matchIndex) {
1532+
pushUpdate(responseItemFallbackUpdates[fallbackIndex]!);
1533+
fallbackIndex += 1;
1534+
}
1535+
return true;
1536+
};
1537+
1538+
for (const update of threadUpdates) {
1539+
const key = historyUpdateKey(update);
1540+
if (key && flushFallbackThrough(key)) {
1541+
continue;
1542+
}
1543+
pushUpdate(update);
1544+
}
1545+
1546+
while (fallbackIndex < responseItemFallbackUpdates.length) {
1547+
pushUpdate(responseItemFallbackUpdates[fallbackIndex]!);
1548+
fallbackIndex += 1;
1549+
}
1550+
1551+
return merged;
1552+
}
1553+
1554+
function historyUpdateKey(update: UpdateSessionEvent): string | null {
1555+
switch (update.sessionUpdate) {
1556+
case "user_message_chunk":
1557+
case "agent_message_chunk":
1558+
case "agent_thought_chunk":
1559+
return `${update.sessionUpdate}:${JSON.stringify(update.content)}`;
1560+
case "tool_call":
1561+
return `tool_call:${update.toolCallId}:start`;
1562+
case "tool_call_update":
1563+
return `tool_call:${update.toolCallId}:update`;
1564+
default:
1565+
return null;
1566+
}
1567+
}
1568+
14841569
function getRequestedMcpServerNames(mcpServers: Array<acp.McpServer>): Array<string> {
14851570
return Array.from(new Set(mcpServers.map(server => sanitizeMcpServerName(server.name))));
14861571
}

src/CodexToolCallMapper.ts

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ import type {
2727
} from "./app-server/v2";
2828
import type { JsonValue } from "./app-server/serde_json/JsonValue";
2929
import {logger} from "./Logger";
30+
import {
31+
createTerminalOutputMeta,
32+
type TerminalOutputMode,
33+
} from "./TerminalOutputMode";
3034

3135
type CodexItemStatus = CommandExecutionStatus | PatchApplyStatus | McpToolCallStatus | DynamicToolCallStatus;
3236
type AcpToolCallStatus = "pending" | "in_progress" | "completed" | "failed";
@@ -87,6 +91,47 @@ export async function createCommandExecutionUpdate(item: CommandExecutionItem):
8791
}, item.id, item.cwd);
8892
}
8993

94+
export function createCommandExecutionCompleteUpdate(
95+
item: CommandExecutionItem,
96+
terminalOutputMode: TerminalOutputMode,
97+
): UpdateSessionEvent | null {
98+
if (item.status === "inProgress") {
99+
return null;
100+
}
101+
102+
const update: UpdateSessionEvent = {
103+
sessionUpdate: "tool_call_update",
104+
toolCallId: item.id,
105+
status: item.status === "completed" ? "completed" : "failed",
106+
rawOutput: {
107+
formatted_output: item.aggregatedOutput ?? "",
108+
exit_code: item.exitCode,
109+
},
110+
};
111+
112+
if (!commandExecutionUsesTerminalOutput(item)) {
113+
return update;
114+
}
115+
116+
const terminalMeta: Record<string, unknown> = {};
117+
if (item.aggregatedOutput) {
118+
Object.assign(
119+
terminalMeta,
120+
createTerminalOutputMeta(terminalOutputMode, item.id, item.aggregatedOutput),
121+
);
122+
}
123+
terminalMeta["terminal_exit"] = {
124+
exit_code: item.exitCode,
125+
signal: null,
126+
terminal_id: item.id,
127+
};
128+
129+
return {
130+
...update,
131+
_meta: terminalMeta,
132+
};
133+
}
134+
90135
export async function createMcpToolCallUpdate(
91136
item: ThreadItem & { type: "mcpToolCall" }
92137
): Promise<UpdateSessionEvent> {
@@ -334,12 +379,12 @@ export function formatWebSearchTitle(item: WebSearchItem): string {
334379
}
335380
}
336381

337-
function createCommandActionEvent(
382+
export function createCommandActionEvent(
338383
id: string,
339384
status: CommandExecutionStatus,
340385
cwd: string,
341386
commandAction: CommandAction
342-
): UpdateSessionEvent {
387+
): AcpToolCallEvent {
343388
const acpStatus = toAcpStatus(status);
344389
switch (commandAction.type) {
345390
case "read":
@@ -395,7 +440,7 @@ function createTerminalCommandEvent(
395440
event: AcpToolCallEvent,
396441
terminalId: string,
397442
cwd: string,
398-
): UpdateSessionEvent {
443+
): AcpToolCallEvent {
399444
const { rawInput, ...eventWithoutRawInput } = event;
400445
return {
401446
...eventWithoutRawInput,

0 commit comments

Comments
 (0)