From 29af07e383a5a8b85e740b9371878d7556021968 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 9 Apr 2026 20:05:07 +0700 Subject: [PATCH] Fix live agent refresh without persistence --- .../src/server/daemon-client.e2e.test.ts | 150 ++++++++++++++++++ packages/server/src/server/session.ts | 6 +- 2 files changed, 151 insertions(+), 5 deletions(-) diff --git a/packages/server/src/server/daemon-client.e2e.test.ts b/packages/server/src/server/daemon-client.e2e.test.ts index a83c41cb3..1e0086d7b 100644 --- a/packages/server/src/server/daemon-client.e2e.test.ts +++ b/packages/server/src/server/daemon-client.e2e.test.ts @@ -14,6 +14,14 @@ import { } from "./test-utils/index.js"; import { getFullAccessConfig, getAskModeConfig } from "./daemon-e2e/agent-configs.js"; import { chunkPcm16, parsePcm16MonoWav, wordSimilarity } from "./test-utils/dictation-e2e.js"; +import type { + AgentClient, + AgentPersistenceHandle, + AgentRunResult, + AgentSession, + AgentSessionConfig, + AgentStreamEvent, +} from "./agent/agent-sdk-types.js"; const openaiApiKey = process.env.OPENAI_API_KEY ?? null; @@ -94,6 +102,116 @@ function waitForSignal( }); } +class NonPersistentReloadSession implements AgentSession { + readonly provider = "claude" as const; + readonly id = null; + readonly capabilities = { + supportsStreaming: false, + supportsSessionPersistence: true, + supportsDynamicModes: false, + supportsMcpServers: false, + supportsReasoningStream: false, + supportsToolInvocations: false, + } as const; + + constructor(private readonly onClose: () => void) {} + + async run(): Promise { + return { + sessionId: "non-persistent", + finalText: "", + timeline: [], + }; + } + + async startTurn(): Promise<{ turnId: string }> { + return { turnId: "non-persistent-turn" }; + } + + subscribe(_callback: (event: AgentStreamEvent) => void): () => void { + return () => undefined; + } + + async *streamHistory(): AsyncGenerator { + return; + } + + async getRuntimeInfo() { + return { + provider: "claude" as const, + sessionId: null, + model: null, + modeId: null, + }; + } + + async getAvailableModes(): Promise<[]> { + return []; + } + + async getCurrentMode(): Promise { + return null; + } + + async setMode(_modeId: string): Promise {} + + getPendingPermissions() { + return []; + } + + async respondToPermission(): Promise {} + + describePersistence(): AgentPersistenceHandle | null { + return null; + } + + async interrupt(): Promise {} + + async close(): Promise { + this.onClose(); + } +} + +class NonPersistentReloadClient implements AgentClient { + readonly provider = "claude" as const; + readonly capabilities = { + supportsStreaming: false, + supportsSessionPersistence: true, + supportsDynamicModes: false, + supportsMcpServers: false, + supportsReasoningStream: false, + supportsToolInvocations: false, + } as const; + createSessionCalls = 0; + resumeSessionCalls = 0; + closeCalls = 0; + + async isAvailable(): Promise { + return true; + } + + async createSession(_config: AgentSessionConfig): Promise { + this.createSessionCalls += 1; + return new NonPersistentReloadSession(() => { + this.closeCalls += 1; + }); + } + + async resumeSession( + _handle: AgentPersistenceHandle, + _overrides?: Partial, + ): Promise { + this.resumeSessionCalls += 1; + return new NonPersistentReloadSession(() => { + this.closeCalls += 1; + }); + } + + async listModels() { + return []; + } +} + describe("daemon client E2E", () => { let ctx: DaemonTestContext; @@ -279,6 +397,38 @@ describe("daemon client E2E", () => { } }, 120000); + test("refresh_agent rebuilds a live agent even when it has no persistence handle", async () => { + const cwd = tmpCwd(); + const client = new NonPersistentReloadClient(); + const localCtx = await createDaemonTestContext({ + agentClients: { + claude: client, + }, + }); + + try { + const created = await localCtx.client.createAgent({ + config: { + provider: "claude", + cwd, + }, + }); + + expect(client.createSessionCalls).toBe(1); + expect(client.resumeSessionCalls).toBe(0); + expect(client.closeCalls).toBe(0); + + await localCtx.client.refreshAgent(created.id); + + expect(client.createSessionCalls).toBe(2); + expect(client.resumeSessionCalls).toBe(0); + expect(client.closeCalls).toBe(1); + } finally { + await localCtx.cleanup(); + rmSync(cwd, { recursive: true, force: true }); + } + }); + test("resume_agent auto-unarchives archived agents", async () => { const cwd = tmpCwd(); try { diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index 2c5c6c3f5..eafa8e6eb 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -3095,11 +3095,7 @@ export class Session { const existing = this.agentManager.getAgent(agentId); if (existing) { await this.interruptAgentIfRunning(agentId); - if (existing.persistence) { - snapshot = await this.agentManager.reloadAgentSession(agentId); - } else { - snapshot = existing; - } + snapshot = await this.agentManager.reloadAgentSession(agentId); } else { const record = await this.agentStorage.get(agentId); if (!record) {