fix(codex): restore externally archived active threads (#3334)
Codex threads can be archived outside Paseo while their Paseo agent remains active. Repair that provider-state drift during interactive resume while preserving the existing read-only history path for Paseo-archived agents.
This commit is contained in:
@@ -1394,16 +1394,55 @@ describe("Codex app-server provider", () => {
|
||||
appServer.assertNoErrors();
|
||||
});
|
||||
|
||||
test("unarchives Codex when an active Paseo agent resumes an archived thread", async () => {
|
||||
const threadRequests: string[] = [];
|
||||
let resumeAttempts = 0;
|
||||
const appServer = createFakeCodexAppServer({
|
||||
"thread/loaded/list": () => {
|
||||
threadRequests.push("thread/loaded/list");
|
||||
return { data: [] };
|
||||
},
|
||||
"thread/resume": () => {
|
||||
threadRequests.push("thread/resume");
|
||||
resumeAttempts += 1;
|
||||
if (resumeAttempts === 1) {
|
||||
return Promise.reject(new Error(archivedThreadErrorMessage("archived-thread-id")));
|
||||
}
|
||||
return { thread: { id: "archived-thread-id" } };
|
||||
},
|
||||
"thread/unarchive": () => {
|
||||
threadRequests.push("thread/unarchive");
|
||||
return { thread: { id: "archived-thread-id" } };
|
||||
},
|
||||
"thread/read": () => {
|
||||
threadRequests.push("thread/read");
|
||||
return { thread: { turns: [] } };
|
||||
},
|
||||
});
|
||||
const provider = createProviderWithFakeAppServer(appServer);
|
||||
|
||||
const session = await provider.resumeSession(archivedThreadHandle());
|
||||
|
||||
expect(threadRequests).toEqual([
|
||||
"thread/loaded/list",
|
||||
"thread/resume",
|
||||
"thread/unarchive",
|
||||
"thread/resume",
|
||||
"thread/read",
|
||||
]);
|
||||
await session.close();
|
||||
appServer.assertNoErrors();
|
||||
});
|
||||
|
||||
test("closes Codex app-server when an interactive resume fails", async () => {
|
||||
const appServer = createFakeCodexAppServer({
|
||||
"thread/resume": () =>
|
||||
Promise.reject(new Error(archivedThreadErrorMessage("archived-thread-id"))),
|
||||
"thread/resume": () => Promise.reject(new Error("thread resume failed")),
|
||||
});
|
||||
const killSpy = vi.spyOn(appServer.child, "kill");
|
||||
const provider = createProviderWithFakeAppServer(appServer);
|
||||
|
||||
await expect(provider.resumeSession(archivedThreadHandle())).rejects.toThrow(
|
||||
archivedThreadErrorMessage("archived-thread-id"),
|
||||
"thread resume failed",
|
||||
);
|
||||
|
||||
expect(killSpy).toHaveBeenCalledWith("SIGTERM");
|
||||
|
||||
@@ -3778,24 +3778,24 @@ export class CodexAppServerAgentSession implements AgentSession {
|
||||
options: { allowArchivedHistory?: boolean } = {},
|
||||
): Promise<void> {
|
||||
if (!this.client || !this.currentThreadId) return;
|
||||
const params: Record<string, unknown> = { threadId: this.currentThreadId };
|
||||
const developerInstructions = composeSystemPromptParts(
|
||||
this.config.systemPrompt,
|
||||
this.config.daemonAppendSystemPrompt,
|
||||
);
|
||||
if (developerInstructions) {
|
||||
params.developerInstructions = developerInstructions;
|
||||
}
|
||||
const codexConfig = this.buildCodexInnerConfig();
|
||||
if (codexConfig) {
|
||||
params.config = codexConfig;
|
||||
}
|
||||
try {
|
||||
const loaded = toObjectRecord(await this.client.request("thread/loaded/list", {}));
|
||||
const ids = Array.isArray(loaded?.data) ? loaded.data : [];
|
||||
if (ids.includes(this.currentThreadId)) {
|
||||
return;
|
||||
}
|
||||
const params: Record<string, unknown> = { threadId: this.currentThreadId };
|
||||
const developerInstructions = composeSystemPromptParts(
|
||||
this.config.systemPrompt,
|
||||
this.config.daemonAppendSystemPrompt,
|
||||
);
|
||||
if (developerInstructions) {
|
||||
params.developerInstructions = developerInstructions;
|
||||
}
|
||||
const codexConfig = this.buildCodexInnerConfig();
|
||||
if (codexConfig) {
|
||||
params.config = codexConfig;
|
||||
}
|
||||
const response = await this.client.request("thread/resume", params);
|
||||
this.rememberResolvedSandboxPolicy(response);
|
||||
} catch (error) {
|
||||
@@ -3811,6 +3811,19 @@ export class CodexAppServerAgentSession implements AgentSession {
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (isArchivedCodexThreadResumeError(error, threadId)) {
|
||||
try {
|
||||
await this.client.request("thread/unarchive", { threadId });
|
||||
} catch (unarchiveError) {
|
||||
if (!isCodexAlreadyUnarchivedError(unarchiveError, threadId)) {
|
||||
throw unarchiveError;
|
||||
}
|
||||
}
|
||||
const response = await this.client.request("thread/resume", params);
|
||||
this.rememberResolvedSandboxPolicy(response);
|
||||
this.logger.info({ threadId }, "Unarchived Codex thread to restore active Paseo agent");
|
||||
return;
|
||||
}
|
||||
this.logger.warn({ error, threadId }, "Failed to resume persisted Codex thread");
|
||||
throw new Error(`Failed to resume Codex thread ${threadId}: ${message}`, { cause: error });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user