From 0be9764194416946c44b62a8b12f0a6c45c74f2e Mon Sep 17 00:00:00 2001 From: ezra Date: Sat, 9 May 2026 12:37:09 +0800 Subject: [PATCH] feat(codex): archive native Codex thread when archiving agent (#827) When a Codex-backed agent is archived from Paseo, call Codex's thread/archive API so the session is also marked archived in the Codex app. This keeps both UIs in sync. - Add optional archiveNativeSession() to AgentClient interface - Implement for Codex provider via thread/archive JSON-RPC call - Call best-effort in both archiveAgent() and archiveSnapshot() paths - Failures are logged as warnings and do not block the archive flow Closes #822 --- .../server/src/server/agent/agent-manager.ts | 23 +++++++++++++++++++ .../src/server/agent/agent-sdk-types.ts | 5 ++++ .../agent/providers/codex-app-server-agent.ts | 17 ++++++++++++++ 3 files changed, 45 insertions(+) diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index 3c9e79919..fc04e3b99 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -975,6 +975,9 @@ export class AgentManager { attentionReason: null, attentionTimestamp: null, }); + + await this.archiveNativeSessionBestEffort(agent.provider, stored.persistence); + this.notifyAgentState(agentId); await this.closeAgent(agentId); @@ -1118,6 +1121,9 @@ export class AgentManager { attentionTimestamp: null, }; await registry.upsert(nextRecord); + + await this.archiveNativeSessionBestEffort(record.provider, record.persistence); + return nextRecord; } @@ -3136,6 +3142,23 @@ export class AgentManager { return client; } + async archiveNativeSessionBestEffort( + provider: AgentProvider, + persistence: AgentPersistenceHandle | null | undefined, + ): Promise { + if (!persistence) return; + const client = this.clients.get(provider); + if (!client?.archiveNativeSession) return; + try { + await client.archiveNativeSession(persistence); + } catch (error) { + this.logger.warn( + { error, provider, sessionId: persistence.sessionId }, + "Failed to archive native session (best-effort)", + ); + } + } + private requireAgent(id: string): LiveManagedAgent { const normalizedId = validateAgentId(id, "requireAgent"); const agent = this.agents.get(normalizedId); diff --git a/packages/server/src/server/agent/agent-sdk-types.ts b/packages/server/src/server/agent/agent-sdk-types.ts index e08d1cba0..3928ffb72 100644 --- a/packages/server/src/server/agent/agent-sdk-types.ts +++ b/packages/server/src/server/agent/agent-sdk-types.ts @@ -558,4 +558,9 @@ export interface AgentClient { */ isAvailable(): Promise; getDiagnostic?(): Promise<{ diagnostic: string }>; + /** + * Archive a persisted session in the native provider (best-effort). + * Called when Paseo archives an agent so the provider's own UI reflects the same state. + */ + archiveNativeSession?(handle: AgentPersistenceHandle): Promise; } diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.ts index 89644968c..397112f77 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.ts @@ -8,6 +8,7 @@ import type { AgentMode, AgentModelDefinition, McpServerConfig, + AgentPersistenceHandle, AgentPermissionRequest, AgentPermissionResponse, AgentPermissionResult, @@ -4904,6 +4905,22 @@ export class CodexAppServerAgentClient implements AgentClient { } } + async archiveNativeSession(handle: AgentPersistenceHandle): Promise { + const threadId = handle.nativeHandle ?? handle.sessionId; + if (!threadId) return; + + const child = await this.spawnAppServer(); + const client = new CodexAppServerClient(child, this.logger); + + try { + await client.request("initialize", buildCodexAppServerInitializeParams()); + client.notify("initialized", {}); + await client.request("thread/archive", { threadId }); + } finally { + await client.dispose(); + } + } + async isAvailable(): Promise { const command = this.runtimeSettings?.command; if (command?.mode === "replace") {