fix: synchronize live worker context metadata

This commit is contained in:
2026-10-02 16:04:55 +09:00
parent cf3cf4b766
commit 3d37f056ad
12 changed files with 352 additions and 68 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -381,7 +381,7 @@ summary: string,
* Full tool output. Absent when the tool chose to return
* summary-only, or when the result was pruned.
*/
output?: string | null, disposition?: ToolResultDisposition | null, is_error: boolean, } } | { "event": "usage", "data": { input_tokens: number | null, output_tokens: number | null, cache_read_input_tokens?: number | null, } } | { "event": "run_end", "data": { result: RunResult, } } | { "event": "error", "data": { code: ErrorCode, message: string, } } | { "event": "snapshot", "data": { session: SessionSnapshot, greeting: Greeting,
output?: string | null, disposition?: ToolResultDisposition | null, is_error: boolean, } } | { "event": "usage", "data": { input_tokens: number | null, output_tokens: number | null, cache_read_input_tokens?: number | null, } } | { "event": "context_usage", "data": { usage?: ContextUsage | null, } } | { "event": "run_end", "data": { result: RunResult, } } | { "event": "error", "data": { code: ErrorCode, message: string, } } | { "event": "snapshot", "data": { session: SessionSnapshot, greeting: Greeting,
/**
* Full revisioned live execution state. `Stopped` remains Runtime
* catalog authority and is deliberately not represented here.
@@ -39,4 +39,15 @@ test("renders honest unavailable state instead of a fabricated zero percent", as
});
expect(view.container.textContent).toContain("claude · 8.2k token budget");
expect(view.container.textContent).toContain("Context ~12.0k / 64.0k (19%)");
await view.rerender({
metadata: {
model: "claude",
reasoning: { kind: "budget_tokens", budget_tokens: -1 },
contextWindow: 64_000,
contextTokens: 12_000,
contextSource: "estimated",
},
});
expect(view.container.textContent).toContain("claude · dynamic token budget");
});
@@ -224,14 +224,17 @@ Deno.test("snapshot and usage keep current context separate from run traffic", (
cache_read_input_tokens: 140_000,
output_tokens: 2_000,
} }, observedAtMs: 2 },
{ eventId: "context", event: { event: "context_usage", data: {
usage: { tokens: 158_000, source: "estimated" },
} }, observedAtMs: 3 },
]);
assertEquals(projection.workerMetadata, {
model: "gpt-6-astra",
reasoning: { kind: "effort", effort: "high" },
contextWindow: 272_000,
contextTokens: 150_000,
contextSource: "measured",
contextTokens: 158_000,
contextSource: "estimated",
});
assertEquals(projection.runActivity.uploadTokens, 10_000);
assertEquals(projection.runActivity.outputTokens, 2_000);
@@ -279,6 +282,20 @@ Deno.test("unknown and destructive boundaries never fabricate context percentage
]);
assertEquals(projection.workerMetadata?.contextTokens, null);
assertEquals(projection.workerMetadata?.contextSource, null);
projection = projectConsole([
{ eventId: "live", event: live },
{ eventId: "rewind", event: { event: "rewind_applied", data: {
session: canonicalSession([]),
input: [],
summary: { truncated_to_entries: 0, discarded_entries: 1, tool_side_effect_warning: false },
} } },
{ eventId: "post-rewind-context", event: { event: "context_usage", data: {
usage: { tokens: 12, source: "estimated" },
} } },
]);
assertEquals(projection.workerMetadata?.contextTokens, 12);
assertEquals(projection.workerMetadata?.contextSource, "estimated");
});
Deno.test("large paste segments project compact artifact metadata", () => {
@@ -3,6 +3,7 @@ import type {
CommandEvent,
CommandSnapshot,
CommandStreamSlice,
ContextUsage,
Event as ProtocolEvent,
Greeting,
InFlightBlock,
@@ -256,6 +257,20 @@ function metadataWithUsage(
};
}
function metadataWithContextUsage(
metadata: ConsoleWorkerMetadata | null,
usage: ContextUsage | null | undefined,
): ConsoleWorkerMetadata {
const tokens = safeTokenCount(usage?.tokens);
return {
model: metadata?.model ?? null,
reasoning: metadata?.reasoning ?? null,
contextWindow: metadata?.contextWindow ?? null,
contextTokens: tokens,
contextSource: tokens === null ? null : usage?.source ?? null,
};
}
function metadataWithoutContext(
metadata: ConsoleWorkerMetadata | null,
): ConsoleWorkerMetadata | null {
@@ -1127,6 +1142,12 @@ export function applyProtocolEvent(
event.data.input_tokens,
);
break;
case "context_usage":
next.workerMetadata = metadataWithContextUsage(
next.workerMetadata,
event.data.usage,
);
break;
case "error":
if (hasDurableRunFailure(next, event.data.message)) break;
next.lines.push(
@@ -58,6 +58,10 @@ Deno.test("worker metadata formats effort, budget, estimate, and unknown safely"
);
assertEquals(
formatReasoning({ kind: "budget_tokens", budget_tokens: -1 }),
"dynamic token budget",
);
assertEquals(
formatReasoning({ kind: "budget_tokens", budget_tokens: -2 }),
"reasoning unavailable",
);
assertEquals(formatContextSummary(null), "Context unavailable");
@@ -8,10 +8,13 @@ export function formatReasoning(reasoning: ReasoningConfig | null): string {
const effort = reasoning.effort.trim();
return effort.length > 0 ? effort : "reasoning unavailable";
}
if (
!Number.isSafeInteger(reasoning.budget_tokens) ||
reasoning.budget_tokens <= 0
) {
if (!Number.isSafeInteger(reasoning.budget_tokens)) {
return "reasoning unavailable";
}
if (reasoning.budget_tokens === -1) {
return "dynamic token budget";
}
if (reasoning.budget_tokens <= 0) {
return "reasoning unavailable";
}
return `${formatRunTokens(reasoning.budget_tokens)} token budget`;