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 = {
agentManager: Pick<
AgentManager,
"createAgent" | "getAgent" | "reloadAgentSession" | "resumeAgentFromPersistence"
| "createAgent"
| "getAgent"
| "hydrateTimelineFromProvider"
| "reloadAgentSession"
| "resumeAgentFromPersistence"
>;
agentStorage: Pick<AgentStorage, "get">;
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<AgentSessionConfig>;
}): 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> {
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<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 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 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<ManagedAgent> {
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<StoredAgentRecord> {
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: {