From 5a20e78dffc11e3b11602dcd9fcfb985ab6a0c95 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 15 Apr 2026 18:20:46 +0700 Subject: [PATCH] fix: restore timeline hydration after daemon restart and clean up defensive code AgentLoadingService was extracted from session.ts but dropped all hydrateTimelineFromProvider() calls, leaving agents with empty timelines after daemon restart. Also fixed a duplicate initial-prompt send bug and removed redundant null checks, dedup maps, and hasOwnProperty theater. --- .../src/server/agent-loading-service.ts | 35 ++++-- packages/server/src/server/session.ts | 103 ++---------------- 2 files changed, 32 insertions(+), 106 deletions(-) diff --git a/packages/server/src/server/agent-loading-service.ts b/packages/server/src/server/agent-loading-service.ts index e15985149..f075850e2 100644 --- a/packages/server/src/server/agent-loading-service.ts +++ b/packages/server/src/server/agent-loading-service.ts @@ -16,7 +16,11 @@ const pendingAgentBootstrapLoads = new Map>(); export type AgentLoadingServiceOptions = { agentManager: Pick< AgentManager, - "createAgent" | "getAgent" | "reloadAgentSession" | "resumeAgentFromPersistence" + | "createAgent" + | "getAgent" + | "hydrateTimelineFromProvider" + | "reloadAgentSession" + | "resumeAgentFromPersistence" >; agentStorage: Pick; logger: pino.Logger; @@ -51,10 +55,7 @@ export class AgentLoadingService { try { return await initPromise; } finally { - const current = pendingAgentBootstrapLoads.get(options.agentId); - if (current === initPromise) { - pendingAgentBootstrapLoads.delete(options.agentId); - } + pendingAgentBootstrapLoads.delete(options.agentId); } } @@ -62,15 +63,24 @@ export class AgentLoadingService { handle: AgentPersistenceHandle; overrides?: Partial; }): Promise { - return this.agentManager.resumeAgentFromPersistence(options.handle, options.overrides); + const snapshot = await this.agentManager.resumeAgentFromPersistence( + options.handle, + options.overrides, + ); + await this.agentManager.hydrateTimelineFromProvider(snapshot.id); + return snapshot; } async refreshAgent(options: { agentId: string }): Promise { const existing = this.agentManager.getAgent(options.agentId); if (existing) { - return existing.persistence - ? await this.agentManager.reloadAgentSession(options.agentId) - : existing; + if (!existing.persistence) { + return existing; + } + + const snapshot = await this.agentManager.reloadAgentSession(options.agentId); + await this.agentManager.hydrateTimelineFromProvider(snapshot.id); + return snapshot; } const record = await this.agentStorage.get(options.agentId); @@ -83,12 +93,14 @@ export class AgentLoadingService { throw new Error(`Agent ${options.agentId} cannot be refreshed because it lacks persistence`); } - return this.agentManager.resumeAgentFromPersistence( + const snapshot = await this.agentManager.resumeAgentFromPersistence( handle, buildConfigOverrides(record), options.agentId, extractTimestamps(record), ); + await this.agentManager.hydrateTimelineFromProvider(snapshot.id); + return snapshot; } private async loadStoredAgent(options: { agentId: string }): Promise { @@ -126,6 +138,7 @@ export class AgentLoadingService { ); } - return this.agentManager.getAgent(options.agentId) ?? snapshot; + await this.agentManager.hydrateTimelineFromProvider(snapshot.id); + return snapshot; } } diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index cd6a09fda..493c8b467 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -195,7 +195,6 @@ import { const execAsync = promisify(exec); const MAX_INITIAL_AGENT_TITLE_CHARS = Math.min(60, MAX_EXPLICIT_AGENT_TITLE_CHARS); -const pendingAgentInitializations = new Map>(); const DEFAULT_AGENT_PROVIDER = AGENT_PROVIDER_IDS[0]; const WORKSPACE_GIT_WATCH_REMOVED_FINGERPRINT = "__removed__"; @@ -1191,10 +1190,7 @@ export class Session { if (storedUpdatedAt) { const liveUpdatedAt = Date.parse(payload.updatedAt); const persistedUpdatedAt = Date.parse(storedUpdatedAt); - if ( - !Number.isNaN(persistedUpdatedAt) && - (Number.isNaN(liveUpdatedAt) || persistedUpdatedAt > liveUpdatedAt) - ) { + if (Number.isNaN(liveUpdatedAt) || persistedUpdatedAt > liveUpdatedAt) { payload.updatedAt = storedUpdatedAt; } } @@ -1231,15 +1227,9 @@ export class Session { record.id, ), sessionId: record.runtimeInfo.sessionId, - ...(Object.prototype.hasOwnProperty.call(record.runtimeInfo, "model") - ? { model: record.runtimeInfo.model ?? null } - : {}), - ...(Object.prototype.hasOwnProperty.call(record.runtimeInfo, "thinkingOptionId") - ? { thinkingOptionId: record.runtimeInfo.thinkingOptionId ?? null } - : {}), - ...(Object.prototype.hasOwnProperty.call(record.runtimeInfo, "modeId") - ? { modeId: record.runtimeInfo.modeId ?? null } - : {}), + model: record.runtimeInfo.model ?? null, + thinkingOptionId: record.runtimeInfo.thinkingOptionId ?? null, + modeId: record.runtimeInfo.modeId ?? null, ...(record.runtimeInfo.extra ? { extra: record.runtimeInfo.extra } : {}), } : undefined; @@ -1287,48 +1277,12 @@ export class Session { })) .filter((value) => !Number.isNaN(value.parsed)); - if (timestamps.length === 0) { - return record.updatedAt; - } - timestamps.sort((a, b) => b.parsed - a.parsed); return timestamps[0].raw; } private async ensureAgentLoaded(agentId: string): Promise { - const existing = this.agentManager.getAgent(agentId); - if (existing) { - return existing; - } - - const inflight = pendingAgentInitializations.get(agentId); - if (inflight) { - return inflight; - } - - const initPromise = (async () => { - await this.requireStoredAgentRecord(agentId); - return this.agentLoadingService.ensureAgentLoaded({ agentId }); - })(); - - pendingAgentInitializations.set(agentId, initPromise); - - try { - return await initPromise; - } finally { - const current = pendingAgentInitializations.get(agentId); - if (current === initPromise) { - pendingAgentInitializations.delete(agentId); - } - } - } - - private async requireStoredAgentRecord(agentId: string): Promise { - const record = await this.agentStorage.get(agentId); - if (!record) { - throw new Error(`Agent not found: ${agentId}`); - } - return record; + return this.agentLoadingService.ensureAgentLoaded({ agentId }); } private isProviderVisibleToClient(provider: string): boolean { @@ -3006,7 +2960,7 @@ export class Session { const started = await this.handleSendAgentMessage( snapshot.id, - trimmedPrompt ?? "", + trimmedPrompt || "", resolveClientMessageId(clientMessageId), images, attachments, @@ -3018,10 +2972,7 @@ export class Session { } if (requestId) { - const agentPayload = await this.getAgentPayloadById(snapshot.id); - if (!agentPayload) { - throw new Error(`Agent ${snapshot.id} not found after creation`); - } + const agentPayload = await this.buildAgentPayload(snapshot); this.emit({ type: "status", payload: { @@ -3033,41 +2984,6 @@ export class Session { }); } - if (trimmedPrompt || (images?.length ?? 0) > 0 || (attachments?.length ?? 0) > 0) { - scheduleAgentMetadataGeneration({ - agentManager: this.agentManager, - agentId: snapshot.id, - cwd: snapshot.cwd, - initialPrompt: trimmedPrompt, - explicitTitle, - paseoHome: this.paseoHome, - logger: this.sessionLogger, - }); - - void this.handleSendAgentMessage( - snapshot.id, - trimmedPrompt ?? "", - resolveClientMessageId(clientMessageId), - images, - attachments, - outputSchema ? { outputSchema } : undefined, - ).catch((promptError) => { - this.sessionLogger.error( - { err: promptError, agentId: snapshot.id }, - `Failed to run initial prompt for agent ${snapshot.id}`, - ); - this.emit({ - type: "activity_log", - payload: { - id: uuidv4(), - timestamp: new Date(), - type: "error", - content: `Initial prompt failed: ${(promptError as Error)?.message ?? promptError}`, - }, - }); - }); - } - if (worktreeBootstrap) { void runAsyncWorktreeBootstrap({ agentId: snapshot.id, @@ -3149,10 +3065,7 @@ export class Session { await this.forwardAgentUpdate(snapshot); const timelineSize = this.agentManager.getTimeline(snapshot.id).length; if (requestId) { - const agentPayload = await this.getAgentPayloadById(snapshot.id); - if (!agentPayload) { - throw new Error(`Agent ${snapshot.id} not found after resume`); - } + const agentPayload = await this.buildAgentPayload(snapshot); this.emit({ type: "status", payload: {