Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 184 additions & 20 deletions apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3577,6 +3577,7 @@ describe("ClaudeAdapterV2 background wake turns", () => {
event.type === "turn.terminal",
);
return {
adapter,
attachmentsDir,
runtime,
providerThread,
Expand Down Expand Up @@ -8542,14 +8543,41 @@ describe("ClaudeAdapterV2 background wake turns", () => {
),
);

it.effect("measures a model outside the catalog against the window Claude reports", () =>
it.effect.each([
{
model: "gateway-model",
autoCompactWindow: "",
reported: { "gateway-model": 1_048_576 },
windows: [null, 1_048_576],
thresholds: [undefined, undefined],
},
{
model: "claude-opus-5-5",
autoCompactWindow: "300000",
reported: { "claude-opus-5-5": 200_000 },
windows: [1_000_000, 200_000],
thresholds: [267_000, undefined],
},
{
model: "claude-opus-5-5",
autoCompactWindow: "300000",
reported: { "claude-haiku-5-5": 200_000 },
windows: [1_000_000, 1_000_000],
thresholds: [267_000, 267_000],
},
])("measures $model against the window Claude reports for it", (row) =>
Effect.gen(function* () {
const harness = yield* makeWakeHarness;
const harness = yield* makeWakeHarnessWithOptions({
settings: { ...DEFAULT_CLAUDE_SETTINGS, autoCompactWindow: row.autoCompactWindow },
});
const modelSelection = {
instanceId: ProviderInstanceId.make(ClaudeAdapterV2.CLAUDE_PROVIDER),
model: "gateway-model",
model: row.model,
} satisfies ModelSelection;
assert.strictEqual(harness.runtime.getModelContextWindow?.(modelSelection), undefined);
assert.strictEqual(
harness.runtime.getModelContextWindow?.(modelSelection) ?? null,
row.windows[0],
);
yield* harness.runtime.startTurn(
makeClaudeTestTurnInput({
threadId: harness.threadId,
Expand All @@ -8567,30 +8595,43 @@ describe("ClaudeAdapterV2 background wake turns", () => {
makeResultFrame({
uuid: "00000000-0000-4000-8000-000000000680",
result: "OK",
modelUsage: {
"gateway-model": {
inputTokens: 9_667,
outputTokens: 2,
cacheReadInputTokens: 0,
cacheCreationInputTokens: 0,
webSearchRequests: 0,
costUSD: 0,
contextWindow: 1_048_576,
maxOutputTokens: 32_000,
},
},
modelUsage: Object.fromEntries(
Object.entries(row.reported).map(([model, contextWindow]) => [
model,
{
inputTokens: 9_667,
outputTokens: 2,
cacheReadInputTokens: 0,
cacheCreationInputTokens: 0,
webSearchRequests: 0,
costUSD: 0,
contextWindow,
maxOutputTokens: 32_000,
},
]),
),
}),
];
for (const frame of frames) yield* Queue.offer(harness.sdkMessages, frame);
const terminal = yield* Queue.take(harness.terminalReceipts);
assert.equal(terminal.status, "completed");
const windows = harness.events.flatMap((event) =>
const usages = harness.events.flatMap((event) =>
event.type === "provider_turn.updated" && event.providerTurn.tokenUsage
? [event.providerTurn.tokenUsage.maxTokens]
? [event.providerTurn.tokenUsage]
: [],
);
assert.deepEqual(windows, [null, 1_048_576]);
assert.strictEqual(harness.runtime.getModelContextWindow?.(modelSelection), 1_048_576);
assert.deepEqual(
usages.map((usage) => usage.maxTokens),
row.windows,
);
assert.deepEqual(
usages.map((usage) => usage.autoCompactThreshold),
row.thresholds,
);
assert.strictEqual(
harness.runtime.getModelContextWindow?.(modelSelection),
row.windows.at(-1),
);
}).pipe(
Effect.provide(
Layer.mergeAll(
Expand Down Expand Up @@ -8698,6 +8739,129 @@ describe("ClaudeAdapterV2 background wake turns", () => {
),
);

it.effect("keeps each session on the window its own Claude process reports", () =>
Effect.gen(function* () {
const harness = yield* makeWakeHarnessWithOptions({ freshQueueOnReopen: true });
const modelSelection = {
instanceId: ProviderInstanceId.make(ClaudeAdapterV2.CLAUDE_PROVIDER),
model: "claude-opus-5-5",
} satisfies ModelSelection;
const usage = { ...zeroUsage, input_tokens: 9_667, output_tokens: 2 };
const reportedWindow = (uuid: string, contextWindow: number) =>
makeResultFrame({
uuid,
result: "OK",
modelUsage: {
"claude-opus-5-5": {
inputTokens: 9_667,
outputTokens: 2,
cacheReadInputTokens: 0,
cacheCreationInputTokens: 0,
webSearchRequests: 0,
costUSD: 0,
contextWindow,
maxOutputTokens: 32_000,
},
},
});
const windows = (events: ReadonlyArray<ProviderAdapter.ProviderAdapterV2Event>) =>
events.flatMap((event) =>
event.type === "provider_turn.updated" && event.providerTurn.tokenUsage
? [event.providerTurn.tokenUsage.maxTokens]
: [],
);
const startTurn = (
runtime: ProviderAdapter.ProviderAdapterV2SessionRuntime,
threadId: ThreadId,
providerThread: typeof harness.providerThread,
attemptId: string,
) =>
Effect.gen(function* () {
yield* runtime.startTurn(
makeClaudeTestTurnInput({
threadId,
providerThread,
now: yield* DateTime.now,
attemptId: RunAttemptId.make(attemptId),
text: "Continue.",
attachments: [],
modelSelection,
}),
);
});

yield* startTurn(harness.runtime, harness.threadId, harness.providerThread, "attempt-a-1");
yield* Queue.offer(
harness.sdkMessages,
usageStreamFrame({ type: "message_start", message: { id: "msg_a1", usage } }),
);
yield* Queue.offer(
harness.sdkMessages,
reportedWindow("00000000-0000-4000-8000-000000000690", 200_000),
);
assert.equal((yield* Queue.take(harness.terminalReceipts)).status, "completed");

const threadB = ThreadId.make("thread-claude-wake-b");
const runtimeB = yield* harness.adapter.openSession({
threadId: threadB,
providerSessionId: ProviderSessionId.make("provider-session-claude-wake-b"),
modelSelection,
runtimePolicy: CLAUDE_TEST_RUNTIME_POLICY,
});
const providerThreadB = yield* runtimeB.ensureThread({
threadId: threadB,
modelSelection,
runtimePolicy: CLAUDE_TEST_RUNTIME_POLICY,
});
const eventsB: Array<ProviderAdapter.ProviderAdapterV2Event> = [];
const terminalsB = yield* Queue.unbounded<ProviderAdapter.ProviderAdapterV2Event>();
yield* runtimeB.events.pipe(
Stream.runForEach((event) =>
Effect.gen(function* () {
eventsB.push(event);
if (event.type === "turn.terminal") yield* Queue.offer(terminalsB, event);
}),
),
Effect.forkScoped,
);
yield* startTurn(runtimeB, threadB, providerThreadB, "attempt-b-1");
const queueB = harness.processQueues.at(-1);
assert.isDefined(queueB);
assert.notStrictEqual(queueB, harness.sdkMessages);
yield* Queue.offer(
queueB,
usageStreamFrame({ type: "message_start", message: { id: "msg_b1", usage } }),
);
yield* Queue.offer(queueB, reportedWindow("00000000-0000-4000-8000-000000000691", 1_000_000));
yield* Queue.take(terminalsB);

yield* startTurn(harness.runtime, harness.threadId, harness.providerThread, "attempt-a-2");
yield* Queue.offer(
harness.sdkMessages,
usageStreamFrame({ type: "message_start", message: { id: "msg_a2", usage } }),
);
yield* Queue.offer(
harness.sdkMessages,
reportedWindow("00000000-0000-4000-8000-000000000692", 200_000),
);
assert.equal((yield* Queue.take(harness.terminalReceipts)).status, "completed");

assert.deepEqual(windows(harness.events), [1_000_000, 200_000, 200_000]);
assert.deepEqual(windows(eventsB), [1_000_000, 1_000_000]);
assert.strictEqual(harness.runtime.getModelContextWindow?.(modelSelection), 200_000);
assert.strictEqual(runtimeB.getModelContextWindow?.(modelSelection), 1_000_000);
}).pipe(
Effect.provide(
Layer.mergeAll(
IdAllocator.layer,
McpProviderSessions.layer,
NodeServices.layer,
NodeCrypto.layer,
),
),
),
);

it.effect("merges cumulative stream usage into the message_start counts", () =>
Effect.gen(function* () {
const harness = yield* makeWakeHarnessWithOptions({
Expand Down
18 changes: 10 additions & 8 deletions apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5148,14 +5148,6 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
);
// Reloaded at each turn and steer so downloaded manifest updates apply without a release.
let modelCatalog = yield* loadModelCatalog;
// Results report the window Claude Code compacts against; outside the catalog it is the only source.
const reportedContextWindows = new Map<string, number>();
const claudeContextWindow = (modelSelection: ModelSelection): number | null =>
resolveClaudeCatalogContextWindowTokens(modelCatalog, modelSelection) ??
reportedContextWindows.get(
compileClaudeModelSelection(modelSelection, modelCatalog).apiModelId,
) ??
null;
const continuationRequests = adapterOptions.continuationRequests ?? {
offer: () => Effect.void,
};
Expand Down Expand Up @@ -5438,6 +5430,16 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
// Wake buffers replay result frames, so ledger growth is measured once on arrival.
const resultLedgerUsage = new WeakMap<SDKMessage, ClaudeLedgerUsage>();
const resultsReportingNewContextWindow = new WeakSet<SDKMessage>();
// Results report the window this session's Claude process enforces, which outranks the
// catalog: user and project settings such as CLAUDE_CODE_DISABLE_1M_CONTEXT shrink it, and
// outside the catalog it is the only source. Other sessions may load other settings.
const reportedContextWindows = new Map<string, number>();
const claudeContextWindow = (modelSelection: ModelSelection): number | null =>
reportedContextWindows.get(
compileClaudeModelSelection(modelSelection, modelCatalog).apiModelId,
) ??
resolveClaudeCatalogContextWindowTokens(modelCatalog, modelSelection) ??
null;
// A worktree handoff changes the project settings even within one session.
let bypassCache: { cwd: string | null; availability: ClaudeBypassAvailability } | null =
null;
Expand Down
Loading