diff --git a/packages/server/src/server/agent/agent-manager.test.ts b/packages/server/src/server/agent/agent-manager.test.ts index 44e38a344f..4678b1e67a 100644 --- a/packages/server/src/server/agent/agent-manager.test.ts +++ b/packages/server/src/server/agent/agent-manager.test.ts @@ -8652,6 +8652,52 @@ test("unarchiveSnapshot unarchives native provider storage before clearing archi expect(stored?.labels).toEqual({ retained: "yes", source: "reimport" }); }); +test("unarchiveSnapshot publishes the stored state for a closed agent", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-unarchive-state-")); + const storagePath = join(workdir, "agents"); + const storage = new AgentStorage(storagePath, logger); + const client = new NativeArchiveRecordingClient(); + const manager = new AgentManager({ + clients: { codex: client }, + registry: storage, + logger, + }); + + try { + const agent = await manager.createAgent( + { + provider: "codex", + cwd: workdir, + title: "Unarchive state target", + }, + undefined, + { workspaceId: undefined }, + ); + await manager.closeAgent(agent.id); + await manager.archiveSnapshot(agent.id, new Date().toISOString()); + + const states: string[] = []; + const unsubscribe = manager.subscribe( + (event) => { + if (event.type === "agent_state" && event.agent.id === agent.id) { + states.push(event.agent.lifecycle); + } + }, + { replayState: false }, + ); + + const unarchived = await manager.unarchiveSnapshot(agent.id); + unsubscribe(); + + expect(unarchived).toBe(true); + expect((await storage.get(agent.id))?.archivedAt).toBeNull(); + expect(states).toEqual(["closed"]); + } finally { + for (const agent of manager.listAgents()) await manager.closeAgent(agent.id); + rmSync(workdir, { recursive: true, force: true }); + } +}); + test("unarchiveSnapshotByHandle unarchives native provider storage for the matched snapshot", async () => { const workdir = mkdtempSync(join(tmpdir(), "agent-manager-native-unarchive-handle-")); const storagePath = join(workdir, "agents"); diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index ff91ef85ef..e4f330a85a 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -2331,16 +2331,22 @@ export class AgentManager { if (this.agents.has(agentId)) await this.closeAgentRuntime(agentId); await this.syncNativeArchiveState(record.provider, record.persistence, "restore"); - await registry.upsert({ + const unarchivedRecord: StoredAgentRecord = { ...record, ...(updates?.workspaceId ? { workspaceId: updates.workspaceId } : {}), ...(updates?.labels ? { labels: applyLabelPatch(record.labels, updates.labels) } : {}), archivedAt: null, updatedAt: new Date().toISOString(), - }); + }; + await registry.upsert(unarchivedRecord); if (this.getAgent(agentId)) { this.notifyAgentState(agentId); + } else if (!unarchivedRecord.internal) { + // An archived agent has no live runtime, so `notifyAgentState` is a no-op. + // Publish the stored snapshot the same way `markRecordArchived` does, or + // clients keep rendering the archived row until they refetch. + this.dispatchStoredAgentState(unarchivedRecord); } return true; }