diff --git a/src/main/claude/claude-structured-init-deadline.ts b/src/main/claude/claude-structured-init-deadline.ts new file mode 100644 index 00000000000..f8531985be6 --- /dev/null +++ b/src/main/claude/claude-structured-init-deadline.ts @@ -0,0 +1,39 @@ +import type { ClaudeInitObservation } from './claude-structured-init-proof' + +export type ClaudeInitDeadline = { + promise: Promise + resolve: (init: ClaudeInitObservation) => void + reject: (error: Error) => void + start: () => void + clear: () => void +} + +export function createClaudeInitDeadline(sessionId: string, timeoutMs: number): ClaudeInitDeadline { + let resolve = (_init: ClaudeInitObservation): void => {} + let reject = (_error: Error): void => {} + const promise = new Promise((resolvePromise, rejectPromise) => { + resolve = resolvePromise + reject = rejectPromise + }) + void promise.catch(() => {}) + let timer: ReturnType | null = null + + return { + promise, + resolve, + reject, + start: () => { + timer = setTimeout( + () => reject(new Error(`claude session ${sessionId} did not emit system/init`)), + timeoutMs + ) + timer.unref?.() + }, + clear: () => { + if (timer) { + clearTimeout(timer) + timer = null + } + } + } +} diff --git a/src/main/claude/claude-structured-session-adapter.ts b/src/main/claude/claude-structured-session-adapter.ts index 7a7f08a2bf3..8a874132419 100644 --- a/src/main/claude/claude-structured-session-adapter.ts +++ b/src/main/claude/claude-structured-session-adapter.ts @@ -15,9 +15,9 @@ import { claudeAuthDiagnostic, readClaudeFrameString, readClaudeInit, - readClaudeModels, - type ClaudeInitObservation + readClaudeModels } from './claude-structured-init-proof' +import { createClaudeInitDeadline } from './claude-structured-init-deadline' import { supportsClaudeStructuredLocation } from './claude-structured-location-support' import { CLAUDE_SPAWN_TOKEN_ENV, claudeProcessIdentity } from './claude-structured-owner-identity' import { setClaudeStructuredOption } from './claude-structured-options' @@ -66,22 +66,12 @@ export class ClaudeStructuredSessionAdapter implements StructuredAgentSessionAda const { previous, attempt } = this.acquisitions.start(sessionId, prompts) let liveSession: ClaudeSession | null = null let observedLeafUuid: string | null = null - let settleInit = (_init: ClaudeInitObservation): void => {} - let rejectInit = (_error: Error): void => {} - const initialized = new Promise((resolve, reject) => { - settleInit = resolve - rejectInit = reject - }) - const initTimer = setTimeout( - () => rejectInit(new Error(`claude session ${sessionId} did not emit system/init`)), - INIT_TIMEOUT_MS - ) - initTimer.unref?.() + const initDeadline = createClaudeInitDeadline(sessionId, INIT_TIMEOUT_MS) const onMessage = (message: Record): void => { const init = readClaudeInit(message) if (init) { - settleInit(init) + initDeadline.resolve(init) } observedLeafUuid = readClaudeFrameString(message, 'uuid') ?? observedLeafUuid if (liveSession) { @@ -139,7 +129,7 @@ export class ClaudeStructuredSessionAdapter implements StructuredAgentSessionAda }, onExit: (error) => { if (!attempt.published) { - rejectInit(error) + initDeadline.reject(error) } this.handleExit(sessionId, attempt, error) } @@ -147,19 +137,20 @@ export class ClaudeStructuredSessionAdapter implements StructuredAgentSessionAda ) attempt.connection = connection this.acquisitions.assertCurrent(sessionId, attempt) + initDeadline.start() const [initialization, init] = await Promise.all([ connection.request( 'initialize', { supportedDialogKinds: [] }, { timeoutMs: this.deps.requestTimeoutMs } ), - initialized + initDeadline.promise ]) const models = readClaudeModels(initialization) this.deliver(attempt, sessionId, () => this.emit(liveSession, input.events, { type: 'options', sessionId, models }) ) - clearTimeout(initTimer) + initDeadline.clear() this.acquisitions.assertCurrent(sessionId, attempt) if (init.providerSessionId !== launch.providerSessionId) { throw new Error( @@ -208,7 +199,7 @@ export class ClaudeStructuredSessionAdapter implements StructuredAgentSessionAda } return acquired } catch (error) { - clearTimeout(initTimer) + initDeadline.clear() this.acquisitions.deleteIfCurrent(sessionId, attempt) if (this.sessions.get(sessionId)?.connection !== attempt.connection) { prompts.clear()