Repository navigation
fix(server): preserve delegated results across wake turns #34568
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2254f83
cdf2053
2b7bf60
1a483b0
ee041dc
cf7c946
5ae360c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6040,6 +6040,24 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( | |
| taskId, | ||
| ); | ||
|
|
||
| // Untraced with roster emission: this runs for each streamed roster update. | ||
| const owedWakeTasksForNativeThread = Effect.fnUntraced(function* (nativeThreadId: string) { | ||
| const previous = (yield* Ref.get(lastProviderThreadByNativeThread)).get(nativeThreadId); | ||
| const roster = rosterForNativeThread( | ||
| yield* Ref.get(pendingBackgroundTasksByNativeThread), | ||
| nativeThreadId, | ||
| ); | ||
| const tasks: OrchestrationV2PendingBackgroundTask[] = []; | ||
| for (const task of previous?.pendingBackgroundTasks ?? []) { | ||
| if ( | ||
| !roster.has(task.taskId) && | ||
| (yield* isKnownOpaqueBackgroundTaskOnNativeThread(nativeThreadId, task.taskId)) | ||
| ) | ||
| tasks.push(task); | ||
| } | ||
| return tasks; | ||
| }); | ||
|
|
||
| const emitProviderThreadRoster = Effect.fnUntraced(function* (input: { | ||
| readonly nativeThreadId: string; | ||
| readonly providerThread: OrchestrationV2ProviderThread; | ||
|
|
@@ -6053,16 +6071,16 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( | |
| yield* Ref.get(awaitedBackgroundTasksByNativeThread), | ||
| input.nativeThreadId, | ||
| ); | ||
| const owedWakeTasks = yield* owedWakeTasksForNativeThread(input.nativeThreadId); | ||
| const now = yield* DateTime.now; | ||
| const providerThread: OrchestrationV2ProviderThread = { | ||
| ...input.providerThread, | ||
| providerSessionId: session.id, | ||
| ...(input.status === undefined ? {} : { status: input.status }), | ||
| pendingBackgroundTasks: publishedPendingBackgroundTasks( | ||
| input.nativeThreadId, | ||
| roster, | ||
| awaitedTaskIds, | ||
| ), | ||
| pendingBackgroundTasks: [ | ||
| ...publishedPendingBackgroundTasks(input.nativeThreadId, roster, awaitedTaskIds), | ||
| ...owedWakeTasks, | ||
| ], | ||
| ...(goalsByNativeThread.has(input.nativeThreadId) | ||
| ? { goal: goalsByNativeThread.get(input.nativeThreadId) ?? null } | ||
| : {}), | ||
|
|
@@ -6073,6 +6091,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( | |
| type: "provider_thread.updated", | ||
| driver: CLAUDE_PROVIDER, | ||
| providerThread, | ||
| pendingBackgroundWork: owedWakeTasks.length > 0, | ||
| }); | ||
| }); | ||
|
|
||
|
|
@@ -8665,6 +8684,10 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( | |
| yield* Ref.get(awaitedBackgroundTasksByNativeThread), | ||
| nativeThreadId, | ||
| ); | ||
| const owedWakeTasks = | ||
| nativeThreadId === null | ||
| ? [] | ||
| : yield* owedWakeTasksForNativeThread(nativeThreadId); | ||
| // A stopped turn that wrote to the transcript moved past the pinned head. | ||
| const clearConversationHead = | ||
| (input.status === "completed" || input.context.nativeMessageCursor !== null) && | ||
|
|
@@ -8703,11 +8726,10 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( | |
| input.context.input.providerThread.firstRunOrdinal ?? | ||
| input.context.input.runOrdinal, | ||
| lastRunOrdinal: input.context.input.runOrdinal, | ||
| pendingBackgroundTasks: publishedPendingBackgroundTasks( | ||
| nativeThreadId, | ||
| roster, | ||
| awaitedTaskIds, | ||
| ), | ||
| pendingBackgroundTasks: [ | ||
| ...publishedPendingBackgroundTasks(nativeThreadId, roster, awaitedTaskIds), | ||
| ...owedWakeTasks, | ||
| ], | ||
| status: input.status === "completed" ? "active" : "idle", | ||
| updatedAt: input.completedAt, | ||
| }; | ||
|
|
@@ -8716,6 +8738,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( | |
| type: "provider_thread.updated" as const, | ||
| driver: CLAUDE_PROVIDER, | ||
| providerThread, | ||
| pendingBackgroundWork: owedWakeTasks.length > 0, | ||
| }); | ||
| }), | ||
| emitProviderEvent(terminalEvent), | ||
|
|
@@ -9235,6 +9258,21 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( | |
| ...later, | ||
| ]; | ||
| if (!(yield* replaceWakeBuffer(nativeThreadId, buffered, kept, retiredTaskIds))) return; | ||
| for (const taskId of retiredTaskIds) { | ||
| yield* clearPendingBackgroundTask(nativeThreadId, taskId); | ||
| yield* clearTaskIdFromNativeThreadSet( | ||
| wakeEligibleBackgroundTasksByNativeThread, | ||
| nativeThreadId, | ||
| taskId, | ||
| ); | ||
| yield* clearOpaqueBackgroundTaskReplayTombstone(nativeThreadId, taskId); | ||
| } | ||
| yield* endClaudeMonitorTasks((taskId) => retiredTaskIds.has(taskId)); | ||
| const providerThread = (yield* Ref.get(lastProviderThreadByNativeThread)).get( | ||
| nativeThreadId, | ||
| ); | ||
| if (providerThread !== undefined) | ||
| yield* emitProviderThreadRoster({ nativeThreadId, providerThread }); | ||
| if (latch.end === undefined) { | ||
| // A lifecycle result closes the root frames before it without opening the wake's turn. | ||
| const sinceLifecycleResult = retired.slice( | ||
|
|
@@ -9355,7 +9393,11 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( | |
| : baseThread.status === "idle" | ||
| ? ("active" as const) | ||
| : baseThread.status; | ||
| yield* emitProviderThreadRoster({ nativeThreadId, providerThread: baseThread, status }); | ||
| yield* emitProviderThreadRoster({ | ||
| nativeThreadId, | ||
| providerThread: baseThread, | ||
| status, | ||
| }); | ||
| }); | ||
|
|
||
| const applyBackgroundTaskRosterMessage = Effect.fnUntraced(function* (input: { | ||
|
|
@@ -11961,7 +12003,24 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( | |
| }); | ||
| return updated; | ||
| }); | ||
| yield* rememberProviderThread(turnInput.providerThread); | ||
| // A queued turn's snapshot can predate another task's exit. | ||
| const owedWakeTasks = yield* owedWakeTasksForNativeThread(nativeThreadId); | ||
| yield* rememberProviderThread( | ||
| owedWakeTasks.length === 0 | ||
| ? turnInput.providerThread | ||
| : { | ||
| ...turnInput.providerThread, | ||
| pendingBackgroundTasks: [ | ||
| ...(turnInput.providerThread.pendingBackgroundTasks ?? []), | ||
| ...owedWakeTasks.filter( | ||
| (task) => | ||
| !(turnInput.providerThread.pendingBackgroundTasks ?? []).some( | ||
| (current) => current.taskId === task.taskId, | ||
|
Comment on lines
+12015
to
+12018
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keep the owed task's awaited mark when the queued snapshot repeats its task ID. A queued turn's snapshot can predate a later Later roster updates and turn finalization rebuild owed tasks from this remembered entry. They do not reapply awaited marks to owed tasks, and A's awaited ID is removed when it leaves the roster. The loss can therefore persist until the owed entry clears. Promote the awaited mark from the owed entry, and keep the snapshot's other fields: Suggested fix
|
||
| ), | ||
| ), | ||
| ], | ||
| }, | ||
| ); | ||
| if (!goalsByNativeThread.has(nativeThreadId)) { | ||
| goalsByNativeThread.set(nativeThreadId, turnInput.providerThread.goal ?? null); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keep the process alive while an owed wake task remains.
An owed wake task is published in
pendingBackgroundTasksand setspendingBackgroundWork, but neither process-lifetime check counts it. The session-widehasPendingBackgroundWork(ClaudeAdapterV2.ts:12724-12753) andliveProcessRunsBackgroundWork(ClaudeAdapterV2.ts:11594-11625) read the roster, running subagents, and wake buffers only. When the roster is empty, the owed task is invisible to both.As a result:
ProviderSessionManager.ts:1209-1224and1862-1868can close the runtime after no turn is busy. The runtime usesbackgroundTasksEndWithProcess: true, so the owed wake's notification is lost.openQuery(ClaudeAdapterV2.ts:11719-11728) can replace the live process when a later turn cannot reuse it. The owed wake is lost in that case too.Include owed tasks in
hasPendingBackgroundWorkand in theopenQueryreplacement check. Do not changeliveProcessRunsBackgroundWorkfor its other callers. The goal-completion check (ClaudeAdapterV2.ts:8706-8710) and theincludeSubagentsprobe (ClaudeAdapterV2.ts:12760-12768) use it too. IfopenQueryblocks replacement for an owed task, the turn fails withClaudeBackgroundWorkBlocksQueryReplacementErroruntil the task clears. Confirm that behavior is intended.