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.
This commit is contained in:
Mohamed Boudra
2026-04-15 18:20:46 +07:00
parent e6044264e2
commit 5a20e78dff
2 changed files with 32 additions and 106 deletions
@@ -16,7 +16,11 @@ const pendingAgentBootstrapLoads = new Map<string, Promise<ManagedAgent>>();
export type AgentLoadingServiceOptions = { export type AgentLoadingServiceOptions = {
agentManager: Pick< agentManager: Pick<
AgentManager, AgentManager,
"createAgent" | "getAgent" | "reloadAgentSession" | "resumeAgentFromPersistence" | "createAgent"
| "getAgent"
| "hydrateTimelineFromProvider"
| "reloadAgentSession"
| "resumeAgentFromPersistence"
>; >;
agentStorage: Pick<AgentStorage, "get">; agentStorage: Pick<AgentStorage, "get">;
logger: pino.Logger; logger: pino.Logger;
@@ -51,10 +55,7 @@ export class AgentLoadingService {
try { try {
return await initPromise; return await initPromise;
} finally { } finally {
const current = pendingAgentBootstrapLoads.get(options.agentId); pendingAgentBootstrapLoads.delete(options.agentId);
if (current === initPromise) {
pendingAgentBootstrapLoads.delete(options.agentId);
}
} }
} }
@@ -62,15 +63,24 @@ export class AgentLoadingService {
handle: AgentPersistenceHandle; handle: AgentPersistenceHandle;
overrides?: Partial<AgentSessionConfig>; overrides?: Partial<AgentSessionConfig>;
}): Promise<ManagedAgent> { }): Promise<ManagedAgent> {
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<ManagedAgent> { async refreshAgent(options: { agentId: string }): Promise<ManagedAgent> {
const existing = this.agentManager.getAgent(options.agentId); const existing = this.agentManager.getAgent(options.agentId);
if (existing) { if (existing) {
return existing.persistence if (!existing.persistence) {
? await this.agentManager.reloadAgentSession(options.agentId) return existing;
: 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); 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`); throw new Error(`Agent ${options.agentId} cannot be refreshed because it lacks persistence`);
} }
return this.agentManager.resumeAgentFromPersistence( const snapshot = await this.agentManager.resumeAgentFromPersistence(
handle, handle,
buildConfigOverrides(record), buildConfigOverrides(record),
options.agentId, options.agentId,
extractTimestamps(record), extractTimestamps(record),
); );
await this.agentManager.hydrateTimelineFromProvider(snapshot.id);
return snapshot;
} }
private async loadStoredAgent(options: { agentId: string }): Promise<ManagedAgent> { private async loadStoredAgent(options: { agentId: string }): Promise<ManagedAgent> {
@@ -126,6 +138,7 @@ export class AgentLoadingService {
); );
} }
return this.agentManager.getAgent(options.agentId) ?? snapshot; await this.agentManager.hydrateTimelineFromProvider(snapshot.id);
return snapshot;
} }
} }
+8 -95
View File
@@ -195,7 +195,6 @@ import {
const execAsync = promisify(exec); const execAsync = promisify(exec);
const MAX_INITIAL_AGENT_TITLE_CHARS = Math.min(60, MAX_EXPLICIT_AGENT_TITLE_CHARS); const MAX_INITIAL_AGENT_TITLE_CHARS = Math.min(60, MAX_EXPLICIT_AGENT_TITLE_CHARS);
const pendingAgentInitializations = new Map<string, Promise<ManagedAgent>>();
const DEFAULT_AGENT_PROVIDER = AGENT_PROVIDER_IDS[0]; const DEFAULT_AGENT_PROVIDER = AGENT_PROVIDER_IDS[0];
const WORKSPACE_GIT_WATCH_REMOVED_FINGERPRINT = "__removed__"; const WORKSPACE_GIT_WATCH_REMOVED_FINGERPRINT = "__removed__";
@@ -1191,10 +1190,7 @@ export class Session {
if (storedUpdatedAt) { if (storedUpdatedAt) {
const liveUpdatedAt = Date.parse(payload.updatedAt); const liveUpdatedAt = Date.parse(payload.updatedAt);
const persistedUpdatedAt = Date.parse(storedUpdatedAt); const persistedUpdatedAt = Date.parse(storedUpdatedAt);
if ( if (Number.isNaN(liveUpdatedAt) || persistedUpdatedAt > liveUpdatedAt) {
!Number.isNaN(persistedUpdatedAt) &&
(Number.isNaN(liveUpdatedAt) || persistedUpdatedAt > liveUpdatedAt)
) {
payload.updatedAt = storedUpdatedAt; payload.updatedAt = storedUpdatedAt;
} }
} }
@@ -1231,15 +1227,9 @@ export class Session {
record.id, record.id,
), ),
sessionId: record.runtimeInfo.sessionId, sessionId: record.runtimeInfo.sessionId,
...(Object.prototype.hasOwnProperty.call(record.runtimeInfo, "model") model: record.runtimeInfo.model ?? null,
? { model: record.runtimeInfo.model ?? null } thinkingOptionId: record.runtimeInfo.thinkingOptionId ?? null,
: {}), modeId: record.runtimeInfo.modeId ?? 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 }
: {}),
...(record.runtimeInfo.extra ? { extra: record.runtimeInfo.extra } : {}), ...(record.runtimeInfo.extra ? { extra: record.runtimeInfo.extra } : {}),
} }
: undefined; : undefined;
@@ -1287,48 +1277,12 @@ export class Session {
})) }))
.filter((value) => !Number.isNaN(value.parsed)); .filter((value) => !Number.isNaN(value.parsed));
if (timestamps.length === 0) {
return record.updatedAt;
}
timestamps.sort((a, b) => b.parsed - a.parsed); timestamps.sort((a, b) => b.parsed - a.parsed);
return timestamps[0].raw; return timestamps[0].raw;
} }
private async ensureAgentLoaded(agentId: string): Promise<ManagedAgent> { private async ensureAgentLoaded(agentId: string): Promise<ManagedAgent> {
const existing = this.agentManager.getAgent(agentId); return this.agentLoadingService.ensureAgentLoaded({ 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<StoredAgentRecord> {
const record = await this.agentStorage.get(agentId);
if (!record) {
throw new Error(`Agent not found: ${agentId}`);
}
return record;
} }
private isProviderVisibleToClient(provider: string): boolean { private isProviderVisibleToClient(provider: string): boolean {
@@ -3006,7 +2960,7 @@ export class Session {
const started = await this.handleSendAgentMessage( const started = await this.handleSendAgentMessage(
snapshot.id, snapshot.id,
trimmedPrompt ?? "", trimmedPrompt || "",
resolveClientMessageId(clientMessageId), resolveClientMessageId(clientMessageId),
images, images,
attachments, attachments,
@@ -3018,10 +2972,7 @@ export class Session {
} }
if (requestId) { if (requestId) {
const agentPayload = await this.getAgentPayloadById(snapshot.id); const agentPayload = await this.buildAgentPayload(snapshot);
if (!agentPayload) {
throw new Error(`Agent ${snapshot.id} not found after creation`);
}
this.emit({ this.emit({
type: "status", type: "status",
payload: { 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) { if (worktreeBootstrap) {
void runAsyncWorktreeBootstrap({ void runAsyncWorktreeBootstrap({
agentId: snapshot.id, agentId: snapshot.id,
@@ -3149,10 +3065,7 @@ export class Session {
await this.forwardAgentUpdate(snapshot); await this.forwardAgentUpdate(snapshot);
const timelineSize = this.agentManager.getTimeline(snapshot.id).length; const timelineSize = this.agentManager.getTimeline(snapshot.id).length;
if (requestId) { if (requestId) {
const agentPayload = await this.getAgentPayloadById(snapshot.id); const agentPayload = await this.buildAgentPayload(snapshot);
if (!agentPayload) {
throw new Error(`Agent ${snapshot.id} not found after resume`);
}
this.emit({ this.emit({
type: "status", type: "status",
payload: { payload: {