Something went wrong. Try again.
fork of prime agent with changes i want but are not upstreamable probqbly (and some fixes)
Something went wrong. Try again.
25 kB · 781 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782import { randomUUID } from "node:crypto";import { createConnection, type Socket } from "node:net";import { getDaemonLogPath } from "../../config.js";import { attachJsonlLineReader, serializeJsonLine } from "../rpc/jsonl.js";import { createDaemonCommandEnvelope, DAEMON_COMMAND_ENVELOPE_MIN_PROTOCOL_VERSION, DAEMON_PROTOCOL_VERSION, type DaemonClosingReason, type DaemonCommand, type DaemonCommandCompatibility, type DaemonCommandEnvelope, type DaemonOutbound, type DaemonProtocolVersion, type DaemonRequestProgress, type DaemonResponse, type DaemonSavedSessionInfo, type DaemonServerCapability, getDaemonCommandCompatibilities, isDaemonMutatingCommand, meetsDaemonCommandCompatibility,} from "./daemon-protocol.js";import type { DaemonWorkerCommand, DaemonWorkerCommandBody } from "./daemon-worker-protocol.js";
type DistributiveOmit<T, K extends keyof T> = T extends unknown ? Omit<T, K> : never;export type DaemonCommandBody = DistributiveOmit<DaemonCommand, "id">;
type DaemonWireCommandBody = DaemonCommandBody | DaemonWorkerCommandBody;
export type DaemonHello = Extract<DaemonOutbound, { type: "daemon_hello" }>;
export type DaemonClientMessageListener = (message: DaemonOutbound) => void;export type DaemonClientCloseListener = (error: Error) => void;export type DaemonClientProgressListener = (message: DaemonRequestProgress) => void;
export interface DaemonClientRequestOptions { onProgress?: DaemonClientProgressListener; /** Runs synchronously before the reader dispatches any records following this response. */ onResponse?: (response: DaemonResponse) => void; /** * False opts out of reconnect parking: a close rejects so the caller's own retry loop stays live. * Any caller that owns its own bounded retry MUST pass false; a parked request waits for a hello * that only the caller's stuck loop could produce. */ recoverable?: boolean;}
interface PendingDaemonRequest { resolve: (response: DaemonResponse) => void; reject: (error: Error) => void; timeout?: ReturnType<typeof setTimeout>; timeoutMs: number; commandType: string; onProgress?: DaemonClientProgressListener; wireData: string; awaitingReconnect: boolean; acknowledgeResult: boolean; recoverable: boolean; /** Re-checked against the new hello before a reconnect replay. */ compatibilities: readonly DaemonCommandCompatibility[];}
function daemonEndpointDetails(socketPath: string): string { return `Socket: ${socketPath}. Daemon log: ${getDaemonLogPath(socketPath)}.`;}
export class DaemonSocketClosedError extends Error { constructor( socketPath: string, readonly daemonClosingReason?: DaemonClosingReason, cause?: string, ) { const reasonDetails = daemonClosingReason ? ` Reason: ${daemonClosingReason}.` : ""; const causeDetails = cause ? ` Cause: ${cause}.` : ""; super( `Connection to the Prime Agent daemon closed.${reasonDetails}${causeDetails} ${daemonEndpointDetails(socketPath)}`, ); this.name = "DaemonSocketClosedError"; }}
export class DaemonCapabilityUnavailableError extends Error { constructor( readonly command: DaemonCommand["type"], readonly capability: DaemonServerCapability | undefined, readonly afterReconnect = false, ) { super( capability ? `The running Prime Agent daemon does not support ${capability}.` : `The running Prime Agent daemon does not support ${command}.`, ); this.name = "DaemonCapabilityUnavailableError"; }}
export class DaemonNotConnectedError extends Error { readonly code = "daemon_not_connected" as const; constructor( readonly commandType: string, readonly socketPath: string, ) { super( `Cannot send daemon command "${commandType}" because the Prime Agent daemon is not connected. ${daemonEndpointDetails(socketPath)}`, ); this.name = "DaemonNotConnectedError"; }}
export class DaemonConnectionFailedError extends Error { readonly code: string | undefined; constructor( readonly socketPath: string, cause: Error & { code?: string }, ) { super(`Failed to connect to the Prime Agent daemon: ${cause.message}. ${daemonEndpointDetails(socketPath)}`, { cause, }); this.name = "DaemonConnectionFailedError"; this.code = cause.code; }}
export function isDaemonNotConnectedError(error: unknown): boolean { if (error instanceof DaemonNotConnectedError) return true; if ( error instanceof Error && (error.name === "DaemonNotConnectedError" || (error as { code?: unknown }).code === "daemon_not_connected") ) { return true; } if (typeof error === "object" && error !== null && "cause" in error && (error as { cause: unknown }).cause) { return isDaemonNotConnectedError((error as { cause: unknown }).cause); } return false;}
export function isDaemonSocketTransientError(error: unknown): boolean { if (!error || typeof error !== "object") return false; const code = (error as { code?: unknown }).code ?? ((error as { cause?: { code?: unknown } }).cause as { code?: unknown } | undefined)?.code; if (code === "ECONNREFUSED" || code === "ENOENT" || code === "ECONNRESET" || code === "EPIPE") { return true; } if (error instanceof DaemonConnectionFailedError && (error.code === "ECONNREFUSED" || error.code === "ENOENT")) { return true; } if (error instanceof DaemonSocketClosedError) { return true; } return false;}
export function getDaemonSocketCloseReason(error: Error): DaemonClosingReason | undefined { return error instanceof DaemonSocketClosedError ? error.daemonClosingReason : undefined;}
export type DaemonClientReconnectStatus = | { status: "reconnecting"; error: string } | { status: "connected" } | { status: "failed"; error: string };
export interface DaemonClientReconnectOptions { recoverDaemon: () => Promise<void>; timeoutMs?: number; onStatus?: (status: DaemonClientReconnectStatus) => void;}
export interface DaemonTransportClient { readonly hello: DaemonHello | undefined; readonly isConnected: boolean; supportsServerCapability(capability: DaemonServerCapability): boolean; waitForHello(timeoutMs?: number): Promise<DaemonHello>; connect(timeoutMs?: number): Promise<void>; reconnect(timeoutMs?: number): Promise<void>; disconnectForReconnect(reason: DaemonClosingReason): void; resetTransportForReconnect(): void; onMessage(listener: DaemonClientMessageListener): () => void; onClose(listener: DaemonClientCloseListener): () => void; enableRequestRecovery(): void; request( command: DaemonCommandBody, timeoutMs?: number, options?: DaemonClientRequestOptions, ): Promise<DaemonResponse>; close(): void;}
const DEFAULT_DAEMON_REQUEST_TIMEOUT_MS = 30_000;// Windows worker startup can exceed 30 seconds under antivirus scanning.const WINDOWS_DAEMON_CREATE_TIMEOUT_MS = 120_000;
function defaultDaemonRequestTimeout(command: DaemonCommandBody): number { return command.type === "create" && process.platform === "win32" ? WINDOWS_DAEMON_CREATE_TIMEOUT_MS : DEFAULT_DAEMON_REQUEST_TIMEOUT_MS;}
const DEFAULT_RECONNECT_TIMEOUT_MS = 60_000;const RECONNECT_CONNECT_TIMEOUT_MS = 1000;const RECONNECT_HELLO_TIMEOUT_MS = 3000;const MAX_RECONNECT_DELAY_MS = 2000;
export class DaemonClient { private socket?: Socket; private detachReader?: () => void; private readonly listeners = new Set<DaemonClientMessageListener>(); private readonly closeListeners = new Set<DaemonClientCloseListener>(); private readonly pendingRequests = new Map<string, PendingDaemonRequest>(); private requestId = 0; private readonly protocolClientId = `daemon-client:${randomUUID()}`; private requestRecoveryEnabled = false; private reconnectOptions?: DaemonClientReconnectOptions; private autoReconnectPromise?: Promise<void>; private closed = false; private helloMessage?: DaemonHello; private daemonClosingReason?: DaemonClosingReason; private reconnectPromise?: Promise<void>; private readonly helloWaiters = new Set<{ resolve: (hello: DaemonHello) => void; reject: (error: Error) => void; timeout: ReturnType<typeof setTimeout>; }>();
constructor(private readonly socketPath: string) {}
get hello(): DaemonHello | undefined { return this.helloMessage; }
get isConnected(): boolean { return this.socket !== undefined && !this.socket.destroyed; }
supportsServerCapability(capability: DaemonServerCapability): boolean { return this.helloMessage?.serverCapabilities?.includes(capability) === true; }
/** Wait for the daemon_hello greeting sent on connect. */ async waitForHello(timeoutMs = 3000): Promise<DaemonHello> { if (this.helloMessage) { return this.helloMessage; } if (!this.socket || this.socket.destroyed) { throw new Error( `Cannot wait for the Prime Agent daemon handshake because the daemon is not connected. ${daemonEndpointDetails(this.socketPath)}`, ); } return new Promise<DaemonHello>((resolve, reject) => { const waiter = { resolve, reject, timeout: setTimeout(() => { this.helloWaiters.delete(waiter); reject( new Error( `Timed out after ${timeoutMs}ms waiting for the Prime Agent daemon handshake. ${daemonEndpointDetails(this.socketPath)}`, ), ); }, timeoutMs), }; this.helloWaiters.add(waiter); }); }
async connect(timeoutMs = 3000): Promise<void> { if (this.socket) { throw new Error(`Prime Agent daemon client is already connected. ${daemonEndpointDetails(this.socketPath)}`); } this.helloMessage = undefined; this.daemonClosingReason = undefined; const socket = createConnection(this.socketPath); this.socket = socket; this.detachReader = attachJsonlLineReader(socket, (line) => this.handleLine(line));
await new Promise<void>((resolve, reject) => { const timeout = setTimeout(() => { cleanup(); this.clearSocketReference(socket); socket.destroy(); reject( new Error( `Timed out after ${timeoutMs}ms connecting to the Prime Agent daemon. ${daemonEndpointDetails(this.socketPath)}`, ), ); }, timeoutMs); const cleanup = () => { clearTimeout(timeout); socket.off("connect", onConnect); socket.off("error", onError); }; const onConnect = () => { cleanup(); resolve(); }; const onError = (error: Error) => { cleanup(); this.clearSocketReference(socket); reject(new DaemonConnectionFailedError(this.socketPath, error)); }; socket.once("connect", onConnect); socket.once("error", onError); });
socket.on("error", (error) => this.notifyClosed( socket, this.daemonClosingReason ? new DaemonSocketClosedError(this.socketPath, this.daemonClosingReason, error.message) : error, ), ); socket.on("close", () => this.notifyClosed(socket, new DaemonSocketClosedError(this.socketPath, this.daemonClosingReason)), ); }
async reconnect(timeoutMs = 3000): Promise<void> { if (this.reconnectPromise) { return this.reconnectPromise; } if (this.socket && !this.socket.destroyed) { return; } const reconnectPromise = this.connect(timeoutMs); this.reconnectPromise = reconnectPromise; try { await reconnectPromise; } finally { if (this.reconnectPromise === reconnectPromise) { this.reconnectPromise = undefined; } } }
disconnectForReconnect(reason: DaemonClosingReason): void { const socket = this.socket; if (!socket || socket.destroyed) { return; } this.daemonClosingReason = reason; this.notifyClosed(socket, new DaemonSocketClosedError(this.socketPath, reason)); socket.end(); socket.destroy(); }
/** Discard a partially recovered transport so the next retry can reconnect cleanly. */ resetTransportForReconnect(): void { const socket = this.socket; if (!socket) { return; } this.clearSocketReference(socket); this.rejectAll( new DaemonSocketClosedError(this.socketPath, undefined, "reconnect attempt did not complete"), this.requestRecoveryEnabled, ); socket.destroy(); }
onMessage(listener: DaemonClientMessageListener): () => void { this.listeners.add(listener); return () => { this.listeners.delete(listener); }; }
onClose(listener: DaemonClientCloseListener): () => void { this.closeListeners.add(listener); return () => { this.closeListeners.delete(listener); }; }
/** Keep in-flight command promises alive and resend their stable envelopes after reconnect. */ enableRequestRecovery(): void { this.requestRecoveryEnabled = true; }
/** Reconnect a global/raw daemon client after supervisor replacement. */ enableAutoReconnect(options: DaemonClientReconnectOptions): void { this.requestRecoveryEnabled = true; this.reconnectOptions = options; }
async request( command: DaemonCommandBody, timeoutMs = defaultDaemonRequestTimeout(command), options: DaemonClientRequestOptions = {}, ): Promise<DaemonResponse> { if (!this.socket || this.socket.destroyed) { throw new DaemonNotConnectedError(command.type, this.socketPath); } const hello = this.helloMessage ?? (await this.waitForHello()); const compatibilities = getDaemonCommandCompatibilities(command); const missingCompatibility = compatibilities.find( (compatibility) => !meetsDaemonCommandCompatibility(hello, compatibility), ); if (missingCompatibility) { throw new DaemonCapabilityUnavailableError(command.type, missingCompatibility.capability); } const envelopeProtocolVersion = Math.min(hello.protocol.version, DAEMON_PROTOCOL_VERSION); return this.requestWire( command, timeoutMs, options, envelopeProtocolVersion >= DAEMON_COMMAND_ENVELOPE_MIN_PROTOCOL_VERSION ? envelopeProtocolVersion : undefined, compatibilities, ); }
async authenticateWorker(token: string, timeoutMs = 3000): Promise<void> { const legacyAuthentication = { type: "worker_auth", token } as DaemonWorkerCommandBody; const response = await this.requestWire(legacyAuthentication, timeoutMs); if (!response.success) { throw new Error(response.error); } }
async requestWorker(command: DaemonWorkerCommandBody, timeoutMs = 30000): Promise<DaemonResponse> { return this.requestWire(command, timeoutMs); }
private async requestWire( command: DaemonWireCommandBody, timeoutMs: number, options: DaemonClientRequestOptions = {}, publicEnvelopeProtocolVersion?: DaemonProtocolVersion, compatibilities: readonly DaemonCommandCompatibility[] = [], ): Promise<DaemonResponse> { if (!this.socket || this.socket.destroyed) { throw new DaemonNotConnectedError(command.type, this.socketPath); }
const id = `daemon_${++this.requestId}`; const fullCommand = { ...command, id } as DaemonCommand | DaemonWorkerCommand; const wireCommand: DaemonCommand | DaemonWorkerCommand | DaemonCommandEnvelope = publicEnvelopeProtocolVersion ? createDaemonCommandEnvelope( fullCommand as DaemonCommand, id, this.protocolClientId, publicEnvelopeProtocolVersion, ) : fullCommand; const wireData = serializeJsonLine(wireCommand); const acknowledgeResult = publicEnvelopeProtocolVersion !== undefined && isDaemonMutatingCommand(fullCommand as DaemonCommand);
return new Promise((resolve, reject) => { const pending: PendingDaemonRequest = { resolve: (response) => { try { options.onResponse?.(response); resolve(response); } catch (error) { reject(error); } }, reject, timeoutMs, commandType: command.type, onProgress: options.onProgress, wireData, awaitingReconnect: false, acknowledgeResult, recoverable: options.recoverable !== false, compatibilities, }; this.pendingRequests.set(id, pending); this.armPendingRequestTimeout(id, pending); this.socket!.write(wireData); }); }
private armPendingRequestTimeout(id: string, pending: PendingDaemonRequest): void { pending.timeout = setTimeout(() => { this.pendingRequests.delete(id); pending.reject( new Error( `Timed out after ${pending.timeoutMs}ms waiting for the Prime Agent daemon response to "${pending.commandType}". ${daemonEndpointDetails(this.socketPath)}`, ), ); }, pending.timeoutMs); }
close(): void { this.closed = true; this.reconnectOptions = undefined; this.detachReader?.(); this.detachReader = undefined; this.rejectAll( new Error( `Prime Agent daemon client closed before the operation completed. ${daemonEndpointDetails(this.socketPath)}`, ), ); this.socket?.end(); this.socket?.destroy(); this.socket = undefined; }
private clearSocketReference(socket: Socket): void { if (this.socket !== socket) { return; } this.detachReader?.(); this.detachReader = undefined; this.socket = undefined; }
private handleLine(line: string): void { let message: unknown; try { message = JSON.parse(line); } catch { return; }
if (isDaemonHello(message)) { this.helloMessage = message; for (const waiter of [...this.helloWaiters]) { clearTimeout(waiter.timeout); this.helloWaiters.delete(waiter); waiter.resolve(message); } if (this.socket && !this.socket.destroyed) { for (const [id, pending] of this.pendingRequests) { if (!pending.awaitingReconnect) { continue; } pending.awaitingReconnect = false; const missingCompatibility = pending.compatibilities.find( (compatibility) => !meetsDaemonCommandCompatibility(message, compatibility), ); if (missingCompatibility) { this.pendingRequests.delete(id); pending.reject( new DaemonCapabilityUnavailableError( pending.commandType as DaemonCommand["type"], missingCompatibility.capability, true, ), ); continue; } this.armPendingRequestTimeout(id, pending); this.socket.write(pending.wireData); } } } if (isDaemonClosing(message)) { this.daemonClosingReason = message.reason; }
if (isDaemonResponse(message) && message.id) { const pending = this.pendingRequests.get(message.id); if (pending) { if (pending.timeout) { clearTimeout(pending.timeout); } this.pendingRequests.delete(message.id); pending.resolve(message); if (pending.acknowledgeResult) { this.acknowledgeCommandResult(message.id); } return; } } if (isDaemonRequestProgress(message) && message.id) { const pending = this.pendingRequests.get(message.id); if (pending) { pending.onProgress?.(message); return; } }
for (const listener of this.listeners) { try { listener(message as DaemonOutbound); } catch { // A consumer failure must not interrupt protocol parsing for other clients. } } }
private acknowledgeCommandResult(commandId: string): void { const hello = this.helloMessage; if ( !this.socket || this.socket.destroyed || !hello || hello.protocol.version < DAEMON_COMMAND_ENVELOPE_MIN_PROTOCOL_VERSION ) { return; } const id = `daemon_ack_${++this.requestId}`; const command: DaemonCommand = { id, type: "ack_result", commandId }; const protocolVersion = Math.min(hello.protocol.version, DAEMON_PROTOCOL_VERSION); this.socket.write( serializeJsonLine(createDaemonCommandEnvelope(command, id, this.protocolClientId, protocolVersion)), ); }
private rejectAll(error: Error, preservePendingRequests = false): void { for (const [id, pending] of this.pendingRequests) { if (preservePendingRequests && pending.recoverable) { if (pending.timeout) { clearTimeout(pending.timeout); pending.timeout = undefined; } pending.awaitingReconnect = true; continue; } if (pending.timeout) { clearTimeout(pending.timeout); } pending.reject(error); this.pendingRequests.delete(id); } for (const waiter of [...this.helloWaiters]) { clearTimeout(waiter.timeout); this.helloWaiters.delete(waiter); waiter.reject(error); } }
private notifyClosed(socket: Socket, error: Error): void { if (this.socket !== socket) { return; } this.clearSocketReference(socket); this.rejectAll(error, this.requestRecoveryEnabled); for (const listener of [...this.closeListeners]) { listener(error); } if (this.reconnectOptions && !this.closed) { void this.autoReconnect(error); } }
private async autoReconnect(cause: Error): Promise<void> { if (this.autoReconnectPromise) { return this.autoReconnectPromise; } const options = this.reconnectOptions; if (!options || this.closed) { return; } this.emitReconnectStatus({ status: "reconnecting", error: cause.message }); this.autoReconnectPromise = (async () => { const deadline = Date.now() + (options.timeoutMs ?? DEFAULT_RECONNECT_TIMEOUT_MS); let attempt = 0; let lastError: Error = cause; while (!this.closed && this.reconnectOptions === options && Date.now() < deadline) { try { await options.recoverDaemon(); if (this.closed || this.reconnectOptions !== options) { return; } await this.connect(RECONNECT_CONNECT_TIMEOUT_MS); await this.waitForHello(RECONNECT_HELLO_TIMEOUT_MS); this.emitReconnectStatus({ status: "connected" }); return; } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); this.resetTransportForReconnect(); const remainingMs = deadline - Date.now(); if (remainingMs <= 0) { break; } const delayMs = Math.min(remainingMs, MAX_RECONNECT_DELAY_MS, 100 * 2 ** Math.min(attempt, 5)); attempt++; await delay(delayMs); } } if (this.closed || this.reconnectOptions !== options) { return; } const failure = new Error(`Daemon reconnection failed: ${lastError.message}`); this.rejectAll(failure); this.emitReconnectStatus({ status: "failed", error: failure.message }); this.reconnectOptions = undefined; })().finally(() => { this.autoReconnectPromise = undefined; }); return this.autoReconnectPromise; }
private emitReconnectStatus(status: DaemonClientReconnectStatus): void { try { this.reconnectOptions?.onStatus?.(status); } catch { // UI status callbacks must never interrupt transport recovery. } }}
function delay(ms: number): Promise<void> { return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));}
function isDaemonClosing(value: unknown): value is Extract<DaemonOutbound, { type: "daemon_closing" }> { if (!value || typeof value !== "object") { return false; } const candidate = value as { type?: unknown; reason?: unknown }; return candidate.type === "daemon_closing" && (candidate.reason === "shutdown" || candidate.reason === "update");}
function isDaemonHello(value: unknown): value is DaemonHello { if (!value || typeof value !== "object") { return false; } const candidate = value as { type?: unknown; protocol?: unknown }; return candidate.type === "daemon_hello" && typeof candidate.protocol === "object" && candidate.protocol !== null;}
function isDaemonResponse(value: unknown): value is DaemonResponse { if (!value || typeof value !== "object") { return false; } const candidate = value as { type?: unknown; success?: unknown; command?: unknown }; return ( candidate.type === "response" && typeof candidate.success === "boolean" && typeof candidate.command === "string" );}
function isDaemonRequestProgress(value: unknown): value is DaemonRequestProgress { if (!value || typeof value !== "object") { return false; } const candidate = value as { type?: unknown; command?: unknown; id?: unknown; activeSessionId?: unknown; loaded?: unknown; total?: unknown; session?: unknown; }; if (candidate.command !== "list_saved_sessions" || typeof candidate.id !== "string") { return false; } if (candidate.type === "session_list_progress") { return typeof candidate.loaded === "number" && typeof candidate.total === "number"; } return candidate.type === "session_list_item" && isDaemonSavedSessionInfo(candidate.session);}
function isDaemonSavedSessionInfo(value: unknown): value is DaemonSavedSessionInfo { if (!value || typeof value !== "object") { return false; } const candidate = value as Record<string, unknown>; return ( typeof candidate.path === "string" && typeof candidate.id === "string" && typeof candidate.cwd === "string" && typeof candidate.created === "string" && typeof candidate.modified === "string" && typeof candidate.messageCount === "number" && typeof candidate.firstMessage === "string" && typeof candidate.allMessagesText === "string" && (candidate.agentStatus === undefined || isDaemonSavedSessionAgentStatus(candidate.agentStatus)) );}
function isDaemonSavedSessionAgentStatus(value: unknown): boolean { if (!value || typeof value !== "object") { return false; } const candidate = value as Record<string, unknown>; return ( typeof candidate.summary === "string" && typeof candidate.basedOnMessageCount === "number" && (candidate.taskState === undefined || candidate.taskState === "needs_input" || candidate.taskState === "completed" || candidate.taskState === "error") );}