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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user