From ac16111efc4c435063ce47409c720b17f571bc55 Mon Sep 17 00:00:00 2001 From: F4llen <76191805+f4llenz@users.noreply.github.com> Date: Tue, 10 Nov 2026 00:37:01 +0000 Subject: [PATCH 1/3] fix(server): a running Claude turn keeps the catalog it started with A manifest refresh between a turn's start and a steer changed the steer's prompt effort, the reported context window and new subagents' effort for the turn already running. Each turn now snapshots the catalog at start and reads it for those. --- .../Adapters/ClaudeAdapterV2.test.ts | 103 ++++++++++++++++++ .../Adapters/ClaudeAdapterV2.ts | 46 +++++--- 2 files changed, 131 insertions(+), 18 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts index fd1fd57481422..4e7374215afa2 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts @@ -7876,6 +7876,109 @@ describe("ClaudeAdapterV2 background wake turns", () => { ), ); + it.effect("keeps a running turn's catalog through a refresh and a steer", () => + Effect.gen(function* () { + let catalog: ClaudeModelCatalog = BUNDLED_CLAUDE_MODEL_CATALOG; + const harness = yield* makeWakeHarnessWithOptions({ + modelCatalog: Effect.sync(() => catalog), + }); + const input = makeClaudeTestTurnInput({ + threadId: harness.threadId, + providerThread: harness.providerThread, + now: yield* DateTime.now, + attemptId: RunAttemptId.make("attempt-catalog-refresh"), + text: "Hello", + attachments: [], + }); + yield* harness.runtime.startTurn(input); + const startWindow = harness.runtime.getModelContextWindow?.(CLAUDE_TEST_MODEL_SELECTION); + assert.isNumber(startWindow); + catalog = { models: [] }; + const idAllocator = yield* IdAllocator.IdAllocatorV2; + yield* harness.runtime.steerTurn({ + threadId: harness.threadId, + runId: input.runId, + providerThread: harness.providerThread, + providerTurnId: idAllocator.derive.providerTurn({ + driver: ClaudeAdapterV2.CLAUDE_PROVIDER, + nativeTurnId: `turn:${input.attemptId}`, + }), + message: { + createdBy: "user", + creationSource: "web", + messageId: MessageId.make("catalog-refresh-steer"), + text: "Keep going", + attachments: [], + }, + }); + const usage = { ...zeroUsage, input_tokens: 1_000, output_tokens: 2 }; + const frames = [ + usageStreamFrame({ type: "message_start", message: { id: "msg_refresh", usage } }), + claudeSdkFrame({ + type: "assistant", + parent_tool_use_id: null, + message: { + model: CLAUDE_TEST_MODEL_SELECTION.model, + id: "msg_refresh", + type: "message", + role: "assistant", + content: [ + { + type: "tool_use", + id: "toolu-catalog-refresh", + name: "Agent", + input: { description: "Check", subagent_type: "general-purpose", prompt: "Check." }, + }, + ], + }, + uuid: "00000000-0000-4000-8000-000000000690", + session_id: WAKE_NATIVE_SESSION, + }), + claudeSdkFrame({ + type: "system", + subtype: "task_started", + task_id: "task-catalog-refresh", + tool_use_id: "toolu-catalog-refresh", + description: "Check", + task_type: "local_agent", + uuid: "00000000-0000-4000-8000-000000000691", + session_id: WAKE_NATIVE_SESSION, + }), + makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000692", result: "OK" }), + ]; + for (const frame of frames) yield* Queue.offer(harness.sdkMessages, frame); + yield* Queue.take(harness.terminalReceipts); + + assert.deepEqual( + harness.offeredMessages.map((message) => message.message.content), + ["Ultrathink:\nHello", "Ultrathink:\nKeep going"], + ); + const windows = harness.events.flatMap((event) => + event.type === "provider_turn.updated" && event.providerTurn.tokenUsage + ? [event.providerTurn.tokenUsage.maxTokens] + : [], + ); + assert.deepEqual(windows, [startWindow]); + const subagent = harness.events.find((event) => event.type === "subagent.updated"); + assert.equal( + subagent?.type === "subagent.updated" + ? getModelSelectionStringOptionValue(subagent.subagent.modelSelection, "effort") + : undefined, + "high", + ); + assert.isUndefined(harness.runtime.getModelContextWindow?.(CLAUDE_TEST_MODEL_SELECTION)); + }).pipe( + Effect.provide( + Layer.mergeAll( + IdAllocator.layer, + McpProviderSessions.layer, + NodeServices.layer, + NodeCrypto.layer, + ), + ), + ), + ); + it.effect.each(["root", "mcp", "subagent", "remote"] as const)( "asks Claude for suggestions only on threads a person drives (%s)", (lineage) => diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index deacdfd617373..80447ae6c513d 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -4330,6 +4330,8 @@ function formatClaudeUsageLimitWait(waitMs: number): string { interface ActiveClaudeTurnContext { readonly queryPolicy: ClaudeRuntimeQueryPolicy; readonly input: ProviderAdapter.ProviderAdapterV2TurnInput; + // Snapshot taken at turn start; a later refresh applies from the next turn. + readonly modelCatalog: ClaudeModelCatalog; readonly autoCompactWindow: string; readonly nativeThreadId: string; readonly nativeTurnId: string; @@ -4881,11 +4883,12 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( let modelCatalog = yield* loadModelCatalog; // Results report the window Claude Code compacts against; outside the catalog it is the only source. const reportedContextWindows = new Map(); - const claudeContextWindow = (modelSelection: ModelSelection): number | null => - resolveClaudeCatalogContextWindowTokens(modelCatalog, modelSelection) ?? - reportedContextWindows.get( - compileClaudeModelSelection(modelSelection, modelCatalog).apiModelId, - ) ?? + const claudeContextWindow = ( + catalog: ClaudeModelCatalog, + modelSelection: ModelSelection, + ): number | null => + resolveClaudeCatalogContextWindowTokens(catalog, modelSelection) ?? + reportedContextWindows.get(compileClaudeModelSelection(modelSelection, catalog).apiModelId) ?? null; const continuationRequests = adapterOptions.continuationRequests ?? { offer: () => Effect.void, @@ -5462,7 +5465,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( // Only message_start marks the request start; an assistant snapshot // arrives after generation, so it leaves the cache expiry unknown. if (requestStarted && context.streamUsage !== null) context.streamUsage.requestedAt = now; - const maxTokens = claudeContextWindow(context.input.modelSelection); + const maxTokens = claudeContextWindow(context.modelCatalog, context.input.modelSelection); const tokenUsage = typeof usage === "number" ? { @@ -6860,8 +6863,10 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( existingSubagent === undefined ? (input.effort ?? input.owner?.effort ?? - compileClaudeModelSelection(input.context.input.modelSelection, modelCatalog) - .effort) + compileClaudeModelSelection( + input.context.input.modelSelection, + input.context.modelCatalog, + ).effort) : existingSubagent.effort; const model = input.model ?? priorTask?.model ?? null; const task = { @@ -6899,7 +6904,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( ...(existingSubagent === undefined || existingSubagent.task.modelSelection !== undefined ? { modelSelection: claudeSubagentModelSelection( - modelCatalog, + input.context.modelCatalog, input.context.input.modelSelection.instanceId, model || input.context.input.modelSelection.model, effort, @@ -9142,7 +9147,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( if (message.message.model && message.message.model !== "") { context.rootModels.add(message.message.model); context.executedModel = resolveClaudeReportedModelSlug( - modelCatalog, + context.modelCatalog, message.message.model, ); } @@ -9426,7 +9431,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( const model = snapshotModel.length === 0 || snapshotModel === "" ? undefined - : canonicalClaudeSubagentModel(modelCatalog, snapshotModel); + : canonicalClaudeSubagentModel(context.modelCatalog, snapshotModel); if (parentToolUseId !== null && model !== undefined) { const subagent = yield* resolveSubagentByToolUseId(context, parentToolUseId); if (subagent === undefined) { @@ -9732,7 +9737,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( const nativeToolInput = claudeNativeToolInputFromUnknown(toolUse.input); if (toolUse.name === "Agent") { rememberClaudeSubagentLaunch( - modelCatalog, + context.modelCatalog, context, pendingSubagentLaunchesByToolUseId, toolUse.id, @@ -10332,7 +10337,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( const startToolCall = toolName !== "Agent" && options?.startToolCall !== false; if (toolName === "Agent") { rememberClaudeSubagentLaunch( - modelCatalog, + context.modelCatalog, context, pendingSubagentLaunchesByToolUseId, nativeRequestId, @@ -11148,7 +11153,8 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( const startTurn = Effect.fn("ClaudeAdapterV2.startTurn")( function* (turnInput: ProviderAdapter.ProviderAdapterV2TurnInput) { - modelCatalog = yield* loadModelCatalog; + const turnCatalog = yield* loadModelCatalog; + modelCatalog = turnCatalog; const startedAt = yield* DateTime.now; const nativeThreadId = yield* getNativeThreadId(turnInput.providerThread); const nativeTurnId = `turn:${turnInput.attemptId}`; @@ -11207,7 +11213,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( : yield* makeClaudeUserMessageWithAttachments({ text: applyClaudePromptEffortPrefix( turnInput.message.text, - compileClaudeModelSelection(turnInput.modelSelection, modelCatalog) + compileClaudeModelSelection(turnInput.modelSelection, turnCatalog) .promptEffort, ), attachments: turnInput.message.attachments, @@ -11228,6 +11234,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( ); const context: ActiveClaudeTurnContext = { input: turnInput, + modelCatalog: turnCatalog, autoCompactWindow, queryPolicy: querySession.queryPolicy, nativeThreadId, @@ -11665,8 +11672,10 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( const userMessage = yield* makeClaudeUserMessageWithAttachments({ text: applyClaudePromptEffortPrefix( turnInput.message.text, - compileClaudeModelSelection(currentTurn.input.modelSelection, modelCatalog) - .promptEffort, + compileClaudeModelSelection( + currentTurn.input.modelSelection, + currentTurn.modelCatalog, + ).promptEffort, ), attachments: turnInput.message.attachments, priority: "now", @@ -11821,7 +11830,8 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( driver: CLAUDE_PROVIDER, providerSessionId: input.providerSessionId, providerSession: session, - getModelContextWindow: (selection) => claudeContextWindow(selection) ?? undefined, + getModelContextWindow: (selection) => + claudeContextWindow(modelCatalog, selection) ?? undefined, events: Stream.fromEffectRepeat(Queue.take(events)), backgroundTasksEndWithProcess: true, hasPendingBackgroundWork: Effect.gen(function* () { From a0898ec258c80dec3d1c85266160169c4b8f42f8 Mon Sep 17 00:00:00 2001 From: F4llen <76191805+f4llenz@users.noreply.github.com> Date: Tue, 10 Nov 2026 01:01:37 +0000 Subject: [PATCH 2/3] fix(server): a Claude turn opens its query with the catalog it started with --- .../src/orchestration-v2/Adapters/ClaudeAdapterV2.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index 80447ae6c513d..43144ba1ddfeb 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -10828,6 +10828,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( const openQuery = Effect.fnUntraced(function* ( turnInput: ProviderAdapter.ProviderAdapterV2TurnInput, nativeThreadId: string, + turnCatalog: ClaudeModelCatalog, ) { const queryPolicy = yield* queryPolicyForTurn(turnInput); const mcpSession = yield* mcpSessions.read(turnInput.threadId); @@ -10842,7 +10843,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( const queryPolicyKey = claudeEffectiveQueryPolicyKey(queryPolicy, mcpOverrides); const compiledSelection = compileClaudeModelSelection( turnInput.modelSelection, - modelCatalog, + turnCatalog, ); const resumeSessionAt = yield* getNativeConversationHeadId(turnInput.providerThread); const existing = yield* Ref.get(queryContext); @@ -10958,7 +10959,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( ); const queryOptions = makeClaudeQueryOptions({ modelSelection: turnInput.modelSelection, - modelCatalog, + modelCatalog: turnCatalog, nativeThreadId, title: turnInput.appThread.title, resume: shouldResume, @@ -11226,7 +11227,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), Effect.provideService(Path.Path, path), ); - const querySession = yield* openQuery(turnInput, nativeThreadId); + const querySession = yield* openQuery(turnInput, nativeThreadId, turnCatalog); const autoCompactWindow = yield* resolveAutoCompactWindow( querySession.settingsCwd, turnInput.modelSelection, From 9d6547cb4cf7d7a643a88ac2b6c18c976ab504b9 Mon Sep 17 00:00:00 2001 From: F4llen <76191805+f4llenz@users.noreply.github.com> Date: Tue, 10 Nov 2026 01:33:01 +0000 Subject: [PATCH 3/3] test(server): a catalog refresh during Claude turn startup --- .../Adapters/ClaudeAdapterV2.test.ts | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts index 4e7374215afa2..8e8a364960bce 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts @@ -7979,6 +7979,66 @@ describe("ClaudeAdapterV2 background wake turns", () => { ), ); + it.effect("opens a turn's query with its catalog after a refresh during startup", () => + Effect.gen(function* () { + let catalog: ClaudeModelCatalog = BUNDLED_CLAUDE_MODEL_CATALOG; + let beforeMcpRead: Effect.Effect = Effect.void; + const harness = yield* makeWakeHarnessWithOptions({ + modelCatalog: Effect.sync(() => catalog), + }).pipe( + Effect.updateService(McpProviderSessions.McpProviderSessions, (sessions) => ({ + ...sessions, + read: (threadId) => Effect.andThen(beforeMcpRead, sessions.read(threadId)), + })), + ); + const reached = yield* Deferred.make(); + const release = yield* Deferred.make(); + beforeMcpRead = Deferred.succeed(reached, undefined).pipe( + Effect.andThen(Deferred.await(release)), + ); + const input = makeClaudeTestTurnInput({ + threadId: harness.threadId, + providerThread: harness.providerThread, + now: yield* DateTime.now, + attemptId: RunAttemptId.make("attempt-catalog-startup"), + text: "Hello", + attachments: [], + }); + const starting = yield* harness.runtime.startTurn(input).pipe(Effect.forkChild); + yield* Deferred.await(reached); + catalog = { models: [] }; + // Any steer reloads the adapter-wide catalog, even one that then finds no live query. + yield* harness.runtime + .steerTurn({ + threadId: harness.threadId, + runId: input.runId, + providerThread: harness.providerThread, + providerTurnId: ProviderTurnId.make("provider-turn-catalog-startup"), + message: { + createdBy: "user", + creationSource: "web", + messageId: MessageId.make("catalog-startup-steer"), + text: "Keep going", + attachments: [], + }, + }) + .pipe(Effect.flip); + assert.isUndefined(harness.runtime.getModelContextWindow?.(CLAUDE_TEST_MODEL_SELECTION)); + yield* Deferred.succeed(release, undefined); + yield* Fiber.join(starting); + assert.equal(harness.getOpenedOptions()?.effort, "high"); + }).pipe( + Effect.provide( + Layer.mergeAll( + IdAllocator.layer, + McpProviderSessions.layer, + NodeServices.layer, + NodeCrypto.layer, + ), + ), + ), + ); + it.effect.each(["root", "mcp", "subagent", "remote"] as const)( "asks Claude for suggestions only on threads a person drives (%s)", (lineage) =>