diff --git a/apps/code/src/main/services/agent/service.ts b/apps/code/src/main/services/agent/service.ts index ba8228961..b03024eb5 100644 --- a/apps/code/src/main/services/agent/service.ts +++ b/apps/code/src/main/services/agent/service.ts @@ -709,14 +709,17 @@ When creating pull requests, add the following footer at the end of the PR descr let configOptions: SessionConfigOption[] | undefined; let agentSessionId: string; + // Claude-specific: hydrate session JSONL from PostHog before resuming. + // If hydration finds no conversation to restore, skip the resume and + // fall through to creating a new session. This avoids a doomed + // unstable_resumeSession that would fail with "Resource not found" if (isReconnect && config.sessionId) { const existingSessionId = config.sessionId; - // Claude-specific: hydrate session JSONL from PostHog before resuming if (adapter !== "codex") { const posthogAPI = agent.getPosthogAPI(); if (posthogAPI) { - await hydrateSessionJsonl({ + const hasSession = await hydrateSessionJsonl({ sessionId: existingSessionId, cwd: repoPath, taskId, @@ -725,8 +728,19 @@ When creating pull requests, add the following footer at the end of the PR descr posthogAPI, log, }); + if (!hasSession) { + log.info( + "No session JSONL to resume, creating new session instead", + { taskId, taskRunId }, + ); + config.sessionId = undefined; + } } } + } + + if (isReconnect && config.sessionId) { + const existingSessionId = config.sessionId; // Both adapters implement unstable_resumeSession: // - Claude: delegates to SDK's resumeSession with JSONL hydration diff --git a/packages/agent/src/adapters/claude/session/jsonl-hydration.ts b/packages/agent/src/adapters/claude/session/jsonl-hydration.ts index 96fb66ec4..1805870a4 100644 --- a/packages/agent/src/adapters/claude/session/jsonl-hydration.ts +++ b/packages/agent/src/adapters/claude/session/jsonl-hydration.ts @@ -496,7 +496,7 @@ export async function hydrateSessionJsonl(params: { permissionMode?: string; posthogAPI: PostHogAPIClient; log: HydrationLog; -}): Promise { +}): Promise { const { posthogAPI, log } = params; try { @@ -506,7 +506,7 @@ export async function hydrateSessionJsonl(params: { log.info("Local JSONL exists, skipping S3 hydration", { sessionId: params.sessionId, }); - return; + return true; } catch { // File doesn't exist, proceed with hydration } @@ -514,13 +514,13 @@ export async function hydrateSessionJsonl(params: { const taskRun = await posthogAPI.getTaskRun(params.taskId, params.runId); if (!taskRun.log_url) { log.info("No log URL, skipping JSONL hydration"); - return; + return false; } const entries = await posthogAPI.fetchTaskRunLogs(taskRun); if (entries.length === 0) { log.info("No S3 log entries, skipping JSONL hydration"); - return; + return false; } const entryCounts: Record = {}; @@ -545,7 +545,7 @@ export async function hydrateSessionJsonl(params: { const allTurns = rebuildConversation(entries); if (allTurns.length === 0) { log.info("No conversation in S3 logs, skipping JSONL hydration"); - return; + return false; } const maxTokens = supports1MContext(params.model ?? "") @@ -577,10 +577,12 @@ export async function hydrateSessionJsonl(params: { turns: conversation.length, lines: jsonlLines.length, }); + return true; } catch (err) { log.warn("Failed to hydrate session JSONL, continuing", { sessionId: params.sessionId, error: err instanceof Error ? err.message : String(err), }); + return false; } }