fix: render compaction activity in web console

This commit is contained in:
2026-09-22 22:57:44 +09:00
parent 043cb7bf22
commit d8444e49bd
5 changed files with 707 additions and 306 deletions
@@ -12,7 +12,7 @@
let nowMs = $state(Date.now());
$effect(() => {
if (item.compaction?.state !== 'running') return;
if (!item.compaction) return;
nowMs = Date.now();
const timer = window.setInterval(() => {
nowMs = Date.now();
@@ -23,7 +23,7 @@
function compactionElapsedMs(line: ConsoleLine): number {
const compaction = line.compaction;
if (!compaction) return 0;
return Math.max(0, (compaction.endedAtMs ?? nowMs) - compaction.startedAtMs);
return Math.max(0, nowMs - compaction.startedAtMs);
}
function formatElapsed(ms: number): string {
@@ -33,8 +33,12 @@
return `${minutes}m ${seconds % 60}s`;
}
function compactionState(line: ConsoleLine): string {
return line.compaction?.state.replace('_', ' ') ?? '';
function compactionHeading(line: ConsoleLine): string {
const compaction = line.compaction;
if (!compaction) return '';
return compaction.phase
? `Compacting · ${compaction.phase.replace('_', ' ')}`
: 'Compacting…';
}
function lineClass(line: ConsoleLine): string {
@@ -80,7 +84,7 @@
>
{#if item.compaction}
<div class="compaction-heading">
<span>Compaction · {compactionState(item)}</span>
<span>{compactionHeading(item)}</span>
<span>{formatElapsed(compactionElapsedMs(item))}</span>
</div>
{#if item.compaction.activity.length > 0}
@@ -90,16 +94,6 @@
{/each}
</ul>
{/if}
{#if item.compaction.summary}
<div class="compaction-summary">{item.compaction.summary}</div>
{:else if item.compaction.candidate}
<div class="compaction-candidate">
<span class="compaction-candidate-label">candidate</span>
{item.compaction.candidate}
</div>
{:else if item.compaction.error}
<div class="compaction-error">{item.compaction.error}</div>
{/if}
{:else if shouldRenderHeading(item)}
<div class="message-heading">
<span>{item.title}</span>
@@ -119,7 +113,7 @@
</div>
{/if}
{#if item.compaction}
<!-- rendered as one lifecycle item above -->
<!-- rendered as one runtime activity item above -->
{:else if item.kind === 'tool'}
{#if toolBodyText(item)}
<p class="console-plain-text">
@@ -216,24 +210,6 @@
font-size: var(--font-size-compact);
}
.compaction-summary,
.compaction-candidate,
.compaction-error {
margin-top: var(--space-1);
white-space: pre-wrap;
}
.compaction-candidate-label {
display: block;
color: var(--text-muted);
font-family: var(--font-mono);
font-size: var(--font-size-compact);
}
.compaction-error {
color: var(--danger);
}
.activity-summary {
font-size: var(--font-size-body);
}
@@ -1,4 +1,8 @@
import type { Event, WorkerStateSnapshot, WorkerStatus } from "$lib/generated/protocol";
import type {
Event,
WorkerStateSnapshot,
WorkerStatus,
} from "$lib/generated/protocol";
import {
type ConsoleEventInput,
type ConsoleLine,
@@ -22,15 +26,13 @@ declare const Deno: {
function workerState(status: WorkerStatus): WorkerStateSnapshot {
return {
last_command_id: 0,
state: status === "idle"
? { kind: "idle" }
: {
kind: "busy",
state: {
kind: "run",
state: status === "paused" ? "paused" : "running",
},
state: status === "idle" ? { kind: "idle" } : {
kind: "busy",
state: {
kind: "run",
state: status === "paused" ? "paused" : "running",
},
},
};
}
@@ -61,7 +63,8 @@ function consoleLine(id: string, kind: ConsoleLine["kind"]): ConsoleLine {
function canonicalSession(logEntries: unknown[]): Event extends {
event: "snapshot";
data: infer D;
} ? D extends { session: infer S } ? S : never : never {
} ? D extends { session: infer S } ? S : never
: never {
const entries: Record<string, unknown>[] = [];
let sequence = 0;
const itemEntry = (item: Record<string, unknown>) => {
@@ -80,9 +83,15 @@ function canonicalSession(logEntries: unknown[]): Event extends {
case "segment_start":
case "annotated_segment_start":
entries.length = 0;
for (const history of Array.isArray(entry["history"]) ? entry["history"] : []) {
for (
const history of Array.isArray(entry["history"])
? entry["history"]
: []
) {
const value = history as Record<string, unknown>;
itemEntry((value["item"] as Record<string, unknown> | undefined) ?? value);
itemEntry(
(value["item"] as Record<string, unknown> | undefined) ?? value,
);
}
break;
case "user_input":
@@ -99,15 +108,18 @@ function canonicalSession(logEntries: unknown[]): Event extends {
case "annotated_assistant_item":
case "annotated_tool_result": {
const annotated = entry["entry"] as Record<string, unknown> | undefined;
itemEntry((annotated?.["item"] as Record<string, unknown> | undefined) ??
(entry["item"] as Record<string, unknown>));
itemEntry(
(annotated?.["item"] as Record<string, unknown> | undefined) ??
(entry["item"] as Record<string, unknown>),
);
break;
}
case "system_item":
case "annotated_system_item": {
const annotated = entry["entry"] as Record<string, unknown> | undefined;
const item = (annotated?.["item"] as Record<string, unknown> | undefined) ??
(entry["item"] as Record<string, unknown>);
const item =
(annotated?.["item"] as Record<string, unknown> | undefined) ??
(entry["item"] as Record<string, unknown>);
entries.push({
entry_id: `legacy-test-${sequence++}`,
provenance: "legacy_unknown",
@@ -168,7 +180,10 @@ Deno.test("large paste segments project compact artifact metadata", () => {
source_entry_id: "entry-1",
},
}]);
assert(text.includes("019ca7c8-57b6-7f05-8edf-524147aba7b2"), "artifact id is visible");
assert(
text.includes("019ca7c8-57b6-7f05-8edf-524147aba7b2"),
"artifact id is visible",
);
assert(text.includes("65536 bytes"), "bounded size metadata is visible");
assert(text.includes("text_plain_utf8"), "media type is visible");
assert(text.includes("available"), "availability is visible");
@@ -267,7 +282,9 @@ Deno.test("Worker state events and acknowledgements replace the full state", ()
assertEquals(projection.workerState, freshIdle);
assertEquals(projection.status, "idle");
assert(
!projection.lines.some((line) => line.eventId?.includes("worker-state-conflict")),
!projection.lines.some((line) =>
line.eventId?.includes("worker-state-conflict")
),
"full snapshots must not be rejected by a client-side version comparison",
);
});
@@ -284,7 +301,10 @@ Deno.test("snapshot replaces a live error with one durable run_errored row", ()
},
{
eventId: "idle-after-error",
event: { event: "worker_state", data: { snapshot: workerState("idle") } } satisfies Event,
event: {
event: "worker_state",
data: { snapshot: workerState("idle") },
} satisfies Event,
},
]);
@@ -722,7 +742,9 @@ Deno.test("projectConsole streams distinct Bash stdout and stderr through termin
Deno.test("snapshot restores bounded in-flight Bash command output", () => {
const snapshot = snapshotEvent("/repo");
if (snapshot.event !== "snapshot") throw new Error("snapshot fixture expected");
if (snapshot.event !== "snapshot") {
throw new Error("snapshot fixture expected");
}
snapshot.data.state = workerState("running");
snapshot.data.in_flight = {
blocks: [{
@@ -750,7 +772,10 @@ Deno.test("snapshot restores bounded in-flight Bash command output", () => {
}],
};
const projection = projectConsole([{ eventId: "snapshot-command", event: snapshot }]);
const projection = projectConsole([{
eventId: "snapshot-command",
event: snapshot,
}]);
const [line] = projection.lines.filter((line) => line.kind === "tool");
assertEquals(line.toolCallLabel, "Bash($ slow)");
assertEquals(line.toolStatus, "running…");
@@ -798,14 +823,20 @@ Deno.test("projectConsole caps default preview but keeps complete detail body",
const [line] = projection.lines.filter((line) => line.kind === "tool");
assertEquals(line.title, "Call · CustomTool");
assertEquals(line.toolCallLabel, 'CustomTool("first":"one","second":"two","third":"three","fourth":"four")');
assertEquals(
line.toolCallLabel,
'CustomTool("first":"one","second":"two","third":"three","fourth":"four")',
);
assertEquals(line.toolStatus, "done");
assertEquals(line.body.split("\n").length, 3);
assert(
line.body.includes("out1") && line.body.includes("… +3 more lines"),
"normal display should retain the capped response preview",
);
assert(!line.body.includes("first"), "request arguments should stay in the Call signature and detail");
assert(
!line.body.includes("first"),
"request arguments should stay in the Call signature and detail",
);
assert(
line.detail?.includes("arguments:\nfirst: one") === true &&
line.detail?.includes("fourth: four") === true,
@@ -873,7 +904,9 @@ Deno.test("projectConsole renders JSON tool responses as YAML", () => {
const toolLines = projection.lines.filter((line) => line.kind === "tool");
const jsonLine = toolLines.find((line) => line.id.includes("json-tool"));
const invalidLine = toolLines.find((line) => line.id.includes("invalid-json-tool"));
const invalidLine = toolLines.find((line) =>
line.id.includes("invalid-json-tool")
);
assert(jsonLine, "JSON tool line should be projected");
assert(invalidLine, "invalid JSON tool line should be projected");
assert(
@@ -924,7 +957,10 @@ Deno.test("projectConsole caps Grep preview but keeps complete detail body", ()
assertEquals(line.toolStatus, "done");
assert(line.body.includes("hit1"), "first result should be shown");
assert(line.body.includes("hit5"), "fifth result should be shown");
assert(!line.body.includes("hit6"), "normal preview should retain its result cap");
assert(
!line.body.includes("hit6"),
"normal preview should retain its result cap",
);
assert(
line.body.includes("… +1 more results"),
"preview should show the omitted result count",
@@ -1018,7 +1054,7 @@ Deno.test("projectConsole renders alert events", () => {
assertEquals(projection.lines[1].error, true);
});
Deno.test("projectConsole upserts compaction lifecycle by stable id", () => {
Deno.test("legacy compaction lifecycle events are inert", () => {
const running = {
schema_version: 2,
compaction_id: "compaction-1",
@@ -1033,23 +1069,14 @@ Deno.test("projectConsole upserts compaction lifecycle by stable id", () => {
} as const;
const projection = projectConsole([
{
eventId: "compact-1",
event: { event: "compact_start", data: { lifecycle: running } } satisfies Event,
},
]);
assertEquals(projection.lines.length, 1);
assertEquals(projection.lines[0].id, "compaction-compaction-1");
assertEquals(projection.lines[0].compaction?.state, "running");
assertEquals(projection.lines[0].streaming, true);
const completed = projectConsole([
{
eventId: "compact-1",
event: { event: "compact_start", data: { lifecycle: running } } satisfies Event,
eventId: "compact-start",
event: {
event: "compact_start",
data: { lifecycle: running },
} satisfies Event,
},
{
eventId: "compact-2",
eventId: "compact-done",
event: {
event: "compact_done",
data: {
@@ -1064,17 +1091,32 @@ Deno.test("projectConsole upserts compaction lifecycle by stable id", () => {
},
} satisfies Event,
},
{
eventId: "compact-failed",
event: {
event: "compact_failed",
data: {
lifecycle: {
...running,
revision: 3,
state: "failed",
ended_at_ms: 5_000,
error: "legacy failure",
},
},
} satisfies Event,
},
]);
assertEquals(completed.lines.length, 1);
assertEquals(completed.lines[0].id, "compaction-compaction-1");
assertEquals(completed.lines[0].compaction?.state, "done");
assertEquals(completed.lines[0].compaction?.summary, "accepted summary");
assertEquals(completed.lines[0].streaming, false);
assertEquals(projection.lines, []);
assertEquals(projection.compaction, null);
});
Deno.test("snapshot restores running compaction without staged content", () => {
const snapshot = snapshotEvent("/repo") as Extract<Event, { event: "snapshot" }>;
const snapshot = snapshotEvent("/repo") as Extract<
Event,
{ event: "snapshot" }
>;
snapshot.data.in_flight = {
blocks: [],
compaction: {
@@ -1083,37 +1125,223 @@ Deno.test("snapshot restores running compaction without staged content", () => {
trigger: "manual",
},
};
snapshot.data.internal_workers = [{
worker: {
session_id: "snapshot-compactor",
name: "Compaction",
parent_session_id: "parent-session",
kind: { service: { kind: "compaction" } },
},
revision: 1,
session: canonicalSession([]),
status: "running",
in_flight: {
blocks: [{
kind: "tool_call",
id: "snapshot-read",
name: "ReadEntry",
args: JSON.stringify({ entry_ref: "E1" }),
state: "pending",
}],
},
internal_workers: [],
}];
const projection = projectConsole([{ eventId: "snapshot", event: snapshot }]);
assertEquals(projection.lines.length, 0);
assertEquals(projection.lines.length, 1);
assertEquals(projection.lines[0].id, "compaction-runtime");
assertEquals(projection.lines[0].compaction?.phase, "summarizing");
assertEquals(
projection.lines[0].compaction?.internalWorkerSessionId,
"snapshot-compactor",
);
assertEquals(projection.lines[0].compaction?.activity, [
"ReadEntry — pending",
]);
assertEquals(projection.compaction?.phase, "summarizing");
assertEquals(projection.compaction?.trigger, "manual");
});
Deno.test("compaction service activity stays nested in one lifecycle item", () => {
Deno.test("snapshot authoritatively clears and restores compaction progress", () => {
const projector = createConsoleProjector();
let projection = projector.append([{
eventId: "live-progress",
event: {
event: "compaction_progress",
data: {
compaction: {
phase: "summarizing",
started_at_ms: 1_000,
trigger: "manual",
},
},
} satisfies Event,
}]);
assertEquals(projection.lines[0]?.id, "compaction-runtime");
projection = projector.append([{
eventId: "clear-snapshot",
event: snapshotEvent("/repo"),
}]);
assertEquals(projection.compaction, null);
assertEquals(projection.lines, []);
const restore = snapshotEvent("/repo") as Extract<
Event,
{ event: "snapshot" }
>;
restore.data.in_flight = {
blocks: [],
compaction: {
phase: "committing",
started_at_ms: 2_000,
trigger: "request_threshold",
},
};
projection = projector.append([{
eventId: "restore-snapshot",
event: restore,
}]);
assertEquals(projection.lines.length, 1);
assertEquals(projection.lines[0].id, "compaction-runtime");
assertEquals(projection.lines[0].compaction?.phase, "committing");
assertEquals(projection.compaction?.started_at_ms, 2_000);
});
Deno.test("snapshot does not guess between multiple compaction services", () => {
const snapshot = snapshotEvent("/repo") as Extract<
Event,
{ event: "snapshot" }
>;
snapshot.data.in_flight = {
blocks: [],
compaction: {
phase: "summarizing",
started_at_ms: 1_000,
trigger: "manual",
},
};
snapshot.data.internal_workers = ["compactor-a", "compactor-b"].map(
(sessionId) => ({
worker: {
session_id: sessionId,
name: "Compaction",
parent_session_id: "parent-session",
kind: { service: { kind: "compaction" } },
},
revision: 1,
session: canonicalSession([]),
status: "running" as const,
in_flight: {
blocks: [{
kind: "tool_call" as const,
id: `read-${sessionId}`,
name: "ReadEntry",
args: "{}",
state: "pending" as const,
}],
},
internal_workers: [],
}),
);
const projection = projectConsole([{ eventId: "snapshot", event: snapshot }]);
assertEquals(projection.lines.length, 1);
assertEquals(
projection.lines[0].compaction?.internalWorkerSessionId,
undefined,
);
assertEquals(projection.lines[0].compaction?.activity, []);
assertEquals(consoleWorkerViews(projection).length, 1);
});
Deno.test("live compaction keeps its first service binding until terminal progress", () => {
const workerA = {
session_id: "compactor-a",
name: "Compaction A",
parent_session_id: "parent-session",
kind: { service: { kind: "compaction" } },
} as const;
const workerB = {
...workerA,
session_id: "compactor-b",
name: "Compaction B",
} as const;
const projection = projectConsole([
{
eventId: "compaction-progress",
event: {
event: "compaction_progress",
data: {
compaction: {
phase: "summarizing",
started_at_ms: 1_000,
trigger: "manual",
},
},
} satisfies Event,
},
{
eventId: "compactor-a-read",
event: {
event: "internal_worker",
data: {
worker: workerA,
revision: 1,
event: {
event: "tool_call_start",
data: { id: "read-a", name: "ReadEntry" },
},
},
} satisfies Event,
},
{
eventId: "compactor-b-search",
event: {
event: "internal_worker",
data: {
worker: workerB,
revision: 1,
event: {
event: "tool_call_start",
data: { id: "search-b", name: "SearchSessionEntries" },
},
},
} satisfies Event,
},
]);
assertEquals(
projection.lines[0].compaction?.internalWorkerSessionId,
workerA.session_id,
);
assertEquals(projection.lines[0].compaction?.activity, [
"ReadEntry — pending",
]);
});
Deno.test("compaction summary candidate stays private", () => {
const worker = {
session_id: "compactor-session",
name: "Compaction",
parent_session_id: "parent-session",
kind: { service: { kind: "compaction" } },
} as const;
const lifecycle = {
schema_version: 2,
compaction_id: "compaction-nested",
revision: 2,
internal_worker: worker,
state: "running",
started_at_ms: 1_000,
ended_at_ms: null,
summary: null,
error: null,
new_segment_id: null,
} as const;
const projection = projectConsole([
{
eventId: "compaction-start",
event: { event: "compact_start", data: { lifecycle } } satisfies Event,
eventId: "compaction-progress",
event: {
event: "compaction_progress",
data: {
compaction: {
phase: "summarizing",
started_at_ms: 1_000,
trigger: "manual",
},
},
} satisfies Event,
},
{
eventId: "compaction-tool",
@@ -1150,15 +1378,115 @@ Deno.test("compaction service activity stays nested in one lifecycle item", () =
]);
assertEquals(projection.lines.length, 1);
assertEquals(projection.lines[0].id, "compaction-compaction-nested");
assertEquals(projection.lines[0].compaction?.activity, ["write_summary — running"]);
assertEquals(projection.lines[0].compaction?.candidate, "draft candidate");
assertEquals(projection.lines[0].id, "compaction-runtime");
assertEquals(projection.lines[0].compaction?.activity, [
"write_summary — running",
]);
assert(
!JSON.stringify(projection.lines[0]).includes("draft candidate"),
"uncommitted summary content is not projected into the parent Console",
);
assert(
consoleWorkerViews(projection).length === 1,
"service is not a selectable SubWorker pane",
);
});
Deno.test("runtime compaction progress nests service activity and clears it", () => {
const worker = {
session_id: "runtime-compactor-session",
name: "Compaction",
parent_session_id: "parent-session",
kind: { service: { kind: "compaction" } },
} as const;
const projector = createConsoleProjector();
let projection = projector.append([{
eventId: "compaction-progress",
event: {
event: "compaction_progress",
data: {
compaction: {
phase: "summarizing",
started_at_ms: 1_000,
trigger: "request_threshold",
},
},
} satisfies Event,
}, {
eventId: "compaction-read",
event: {
event: "internal_worker",
data: {
worker,
revision: 1,
event: {
event: "tool_call_done",
data: {
id: "call-read",
name: "ReadEntry",
arguments: JSON.stringify({ entry_ref: "E1" }),
},
},
},
} satisfies Event,
}, {
eventId: "compaction-mark",
event: {
event: "internal_worker",
data: {
worker,
revision: 2,
event: {
event: "tool_call_done",
data: {
id: "call-mark",
name: "mark_read_required",
arguments: JSON.stringify({ path: "src/lib.rs" }),
},
},
},
} satisfies Event,
}]);
assertEquals(projection.lines.length, 1);
assertEquals(projection.lines[0].id, "compaction-runtime");
assertEquals(
projection.lines[0].compaction?.internalWorkerSessionId,
worker.session_id,
);
assertEquals(projection.lines[0].compaction?.activity, [
"ReadEntry — running",
"mark_read_required — running",
]);
assert(
consoleWorkerViews(projection).length === 1,
"service activity remains nested instead of becoming a selectable Worker view",
);
projection = projector.append([{
eventId: "compaction-service-removed",
event: {
event: "internal_worker_removed",
data: { worker, revision: 3 },
} satisfies Event,
}]);
assertEquals(projection.internalWorkers, []);
assertEquals(projection.lines[0].compaction?.activity, [
"ReadEntry — running",
"mark_read_required — running",
]);
projection = projector.append([{
eventId: "compaction-clear",
event: {
event: "compaction_progress",
data: { compaction: null },
} satisfies Event,
}]);
assertEquals(projection.compaction, null);
assertEquals(projection.lines, []);
});
Deno.test("snapshot excludes storage-only compaction extension records", () => {
const projection = projectConsole([{
eventId: "snapshot",
@@ -1186,61 +1514,126 @@ Deno.test("snapshot excludes storage-only compaction extension records", () => {
assertEquals(projection.lines.length, 0);
});
Deno.test("createConsoleProjector ignores stale compaction revisions", () => {
const projector = createConsoleProjector();
const base = {
schema_version: 2,
compaction_id: "compaction-identity",
revision: 1,
internal_worker: null,
state: "running",
started_at_ms: 1_000,
ended_at_ms: null,
summary: null,
error: null,
new_segment_id: null,
Deno.test("segment rotation preserves compaction progress in either event order", () => {
const progress = {
event: "compaction_progress",
data: {
compaction: {
phase: "committing",
started_at_ms: 1_000,
trigger: "manual",
},
},
} satisfies Event;
const rotation = {
event: "segment_rotated",
data: { session: canonicalSession([]) },
} satisfies Event;
const progressThenRotation = projectConsole([
{ eventId: "progress-first", event: progress },
{ eventId: "rotation-second", event: rotation },
]);
assertEquals(progressThenRotation.lines.length, 1);
assertEquals(progressThenRotation.lines[0].id, "compaction-runtime");
assertEquals(progressThenRotation.lines[0].compaction?.phase, "committing");
const rotationThenProgress = projectConsole([
{ eventId: "rotation-first", event: rotation },
{ eventId: "progress-second", event: progress },
]);
assertEquals(rotationThenProgress.lines.length, 1);
assertEquals(rotationThenProgress.lines[0].id, "compaction-runtime");
assertEquals(rotationThenProgress.lines[0].compaction?.phase, "committing");
});
Deno.test("back-to-back compactions do not retain prior activity", () => {
const worker = {
session_id: "first-compactor",
name: "Compaction",
parent_session_id: "parent-session",
kind: { service: { kind: "compaction" } },
} as const;
const projector = createConsoleProjector();
let projection = projector.append([
{
eventId: "compact-identity-1",
eventId: "first-progress",
event: {
event: "user_message",
data: { segments: [{ kind: "text", content: "hello" }] },
} satisfies Event,
},
{
eventId: "compact-identity-2",
event: { event: "compact_start", data: { lifecycle: base } } satisfies Event,
},
]);
const userLine = projection.lines[0];
projection = projector.append([
{
eventId: "compact-identity-3",
event: {
event: "compact_done",
event: "compaction_progress",
data: {
lifecycle: {
...base,
revision: 2,
state: "done",
ended_at_ms: 2_000,
summary: "accepted",
new_segment_id: "00000000-0000-0000-0000-000000000001",
compaction: {
phase: "summarizing",
started_at_ms: 1_000,
trigger: "manual",
},
},
} satisfies Event,
},
{
eventId: "compact-identity-stale",
event: { event: "compact_start", data: { lifecycle: base } } satisfies Event,
eventId: "first-read",
event: {
event: "internal_worker",
data: {
worker,
revision: 1,
event: {
event: "tool_call_done",
data: { id: "read-1", name: "ReadEntry", arguments: "{}" },
},
},
} satisfies Event,
},
{
eventId: "first-removed",
event: {
event: "internal_worker_removed",
data: { worker, revision: 2 },
} satisfies Event,
},
{
eventId: "first-clear",
event: {
event: "compaction_progress",
data: { compaction: null },
} satisfies Event,
},
{
eventId: "second-progress",
event: {
event: "compaction_progress",
data: {
compaction: {
phase: "preparing",
started_at_ms: 2_000,
trigger: "request_threshold",
},
},
} satisfies Event,
},
]);
assert(projection.lines[0] === userLine, "unrelated line retains identity");
assertEquals(projection.lines[1].compaction?.state, "done");
assertEquals(projection.lines[1].compaction?.summary, "accepted");
assertEquals(projection.lines.length, 1);
assertEquals(projection.lines[0].compaction?.startedAtMs, 2_000);
assertEquals(
projection.lines[0].compaction?.internalWorkerSessionId,
undefined,
);
assertEquals(projection.lines[0].compaction?.activity, []);
projection = projector.append([{
eventId: "second-commit",
event: {
event: "compaction_progress",
data: {
compaction: {
phase: "committing",
started_at_ms: 2_000,
trigger: "request_threshold",
},
},
} satisfies Event,
}]);
assertEquals(projection.lines[0].compaction?.activity, []);
});
Deno.test("projectConsole keeps streaming tool call updates in the same Call block", () => {
@@ -1491,7 +1884,10 @@ Deno.test("projectConsole hides lifecycle events and renders system items", () =
const projection = projectConsole([
{
eventId: "30",
event: { event: "worker_state", data: { snapshot: workerState("running") } } satisfies Event,
event: {
event: "worker_state",
data: { snapshot: workerState("running") },
} satisfies Event,
},
{
eventId: "31",
@@ -1629,7 +2025,9 @@ Deno.test("projectConsole renders snapshot entries and in-flight output", () =>
assertEquals(projection.status, "running");
assertEquals(
projection.lines.map((line) =>
`${line.kind}:${line.toolCallLabel ? `${line.toolCallLabel}\n${line.body}` : line.body}:${line.streaming}`
`${line.kind}:${
line.toolCallLabel ? `${line.toolCallLabel}\n${line.body}` : line.body
}:${line.streaming}`
),
[
"user:seed user:false",
@@ -1872,7 +2270,8 @@ Deno.test("projectConsole relativizes known tool path displays from snapshot cwd
);
assert(
toolLines.some((line) =>
line.toolCallLabel === "Write(out.txt)" && line.body.includes("Wrote out.txt")
line.toolCallLabel === "Write(out.txt)" &&
line.body.includes("Wrote out.txt")
),
"Write signature and known result path should be relative",
);
@@ -2010,7 +2409,10 @@ Deno.test("console Worker views expose only direct Internal Workers", () => {
kind: "sub_worker",
},
revision: 1,
event: { event: "worker_state", data: { snapshot: workerState("running") } },
event: {
event: "worker_state",
data: { snapshot: workerState("running") },
},
},
},
},
@@ -2029,7 +2431,10 @@ Deno.test("console Worker views expose only direct Internal Workers", () => {
kind: "sub_worker",
},
revision: 1,
event: { event: "worker_state", data: { snapshot: workerState("idle") } },
event: {
event: "worker_state",
data: { snapshot: workerState("idle") },
},
},
},
}]);
@@ -2121,7 +2526,10 @@ Deno.test("parent snapshot authoritatively replaces Internal Worker projections"
kind: "sub_worker",
},
revision: 1,
event: { event: "worker_state", data: { snapshot: workerState("running") } },
event: {
event: "worker_state",
data: { snapshot: workerState("running") },
},
},
},
}]);
+114 -157
View File
@@ -3,7 +3,6 @@ import type {
CommandEvent,
CommandSnapshot,
CommandStreamSlice,
CompactionLifecycle,
Event as ProtocolEvent,
InFlightBlock,
InFlightCompaction,
@@ -73,14 +72,8 @@ export type ConsoleDiffLine = {
export type ConsoleViewMode = "overview" | "normal";
export type ConsoleCompaction = {
id: string;
revision: number;
state: "running" | "done" | "failed" | "interrupted";
phase?: InFlightCompaction["phase"];
startedAtMs: number;
endedAtMs?: number;
summary?: string;
candidate?: string;
error?: string;
internalWorkerSessionId?: string;
activity: string[];
};
@@ -514,7 +507,9 @@ function appendSnapshotCommands(
commands: CommandSnapshot[],
eventId: string,
): void {
commands.forEach((command) => upsertCommandSnapshot(projection, eventId, command));
commands.forEach((command) =>
upsertCommandSnapshot(projection, eventId, command)
);
}
function upsertCommandSnapshot(
@@ -631,7 +626,8 @@ function appendCommandStream(
start_offset: endOffset - tail.length,
end_offset: endOffset,
content: tail,
truncated: existing.truncated || !contiguous || tail.length < combined.length ||
truncated: existing.truncated || !contiguous ||
tail.length < combined.length ||
startOffset > 0,
};
}
@@ -678,107 +674,115 @@ function projectInternalWorkerSnapshot(
return { worker: snapshot.worker, revision: snapshot.revision, console };
}
function compactionCandidate(
const RUNTIME_COMPACTION_LINE_ID = "compaction-runtime";
function isCompactionService(worker: InternalWorkerRef): boolean {
return worker.kind !== "sub_worker" &&
worker.kind.service.kind === "compaction";
}
function activeCompactionServiceSessionId(
projection: ConsoleProjection,
sessionId: string | undefined,
): string | undefined {
if (!sessionId) return undefined;
const worker = projection.internalWorkers.find(
(candidate) => candidate.worker.session_id === sessionId,
const candidates = projection.internalWorkers.filter((worker) =>
isCompactionService(worker.worker)
);
const calls = worker?.console.lines
.map((line) => line.toolCall)
.filter((call): call is ToolCallView => call?.name === "write_summary") ?? [];
const latest = calls.at(-1);
const raw = latest?.arguments ?? latest?.argsStream;
if (!raw) return undefined;
try {
const parsed = JSON.parse(raw) as { text?: unknown };
return typeof parsed.text === "string" ? parsed.text : undefined;
} catch {
return undefined;
}
return candidates.length === 1 ? candidates[0]?.worker.session_id : undefined;
}
function compactionActivity(
projection: ConsoleProjection,
sessionId: string | undefined
sessionId: string | undefined,
): string[] {
if (!sessionId) return [];
const worker = projection.internalWorkers.find(
(candidate) => candidate.worker.session_id === sessionId
(candidate) => candidate.worker.session_id === sessionId,
);
if (!worker) return [];
return worker.console.lines
.filter((line) => line.kind === "tool" || line.kind === "status" || line.kind === "error")
.filter((line) => line.kind === "tool")
.slice(-12)
.map((line) =>
line.toolCall?.name === "write_summary"
? `write_summary — ${line.toolCall.state}`
line.toolCall
? `${line.toolCall.name} — ${line.toolCall.state}`
: line.body || line.title
)
.filter((value, index, values) => value.length > 0 && values.indexOf(value) === index);
.filter((value, index, values) =>
value.length > 0 && values.indexOf(value) === index
);
}
function applyInFlightCompaction(
projection: ConsoleProjection,
progress: InFlightCompaction | null,
): ConsoleProjection {
return { ...projection, compaction: progress };
}
const lineIndex = projection.lines.findIndex((line) =>
line.id === RUNTIME_COMPACTION_LINE_ID
);
const existing = lineIndex >= 0
? projection.lines[lineIndex]?.compaction
: undefined;
if (!progress) {
return {
...projection,
compaction: null,
lines: projection.lines.filter((line) =>
line.id !== RUNTIME_COMPACTION_LINE_ID
),
};
}
function applyCompactionLifecycle(
projection: ConsoleProjection,
lifecycle: CompactionLifecycle
): ConsoleProjection {
const lineId = `compaction-${lifecycle.compaction_id}`;
const existing = projection.lines.find((line) => line.id === lineId)?.compaction;
if (existing && existing.revision >= lifecycle.revision) return projection;
const internalWorkerSessionId = lifecycle.internal_worker?.session_id;
const compaction: ConsoleCompaction = {
id: lifecycle.compaction_id,
revision: lifecycle.revision,
state: lifecycle.state,
startedAtMs: lifecycle.started_at_ms,
endedAtMs: lifecycle.ended_at_ms ?? undefined,
summary: lifecycle.summary ?? undefined,
candidate: compactionCandidate(projection, internalWorkerSessionId),
error: lifecycle.error ?? undefined,
const internalWorkerSessionId = existing?.internalWorkerSessionId ??
activeCompactionServiceSessionId(projection);
const observedActivity = compactionActivity(
projection,
internalWorkerSessionId,
activity: compactionActivity(projection, internalWorkerSessionId)
);
const compaction: ConsoleCompaction = {
phase: progress.phase,
startedAtMs: progress.started_at_ms,
internalWorkerSessionId,
activity: observedActivity.length > 0
? observedActivity
: existing?.activity ?? [],
};
const line: ConsoleLine = {
id: lineId,
id: RUNTIME_COMPACTION_LINE_ID,
source: "event",
kind: compaction.state === "failed" ? "error" : "status",
kind: "status",
title: "Compaction",
body: compaction.summary ?? compaction.error ?? "",
streaming: compaction.state === "running",
error: compaction.state === "failed",
compaction
body: "",
streaming: true,
compaction,
};
const index = projection.lines.findIndex((candidate) => candidate.id === lineId);
const lines = [...projection.lines];
if (index >= 0) lines[index] = line;
if (lineIndex >= 0) lines[lineIndex] = line;
else lines.push(line);
return { ...projection, lines };
return { ...projection, compaction: progress, lines };
}
function refreshCompactionActivity(
projection: ConsoleProjection,
sessionId: string
worker: InternalWorkerRef,
): ConsoleProjection {
let changed = false;
const lines = projection.lines.map((line) => {
if (line.compaction?.internalWorkerSessionId !== sessionId) return line;
const compaction = line.compaction;
if (!compaction) return line;
const internalWorkerSessionId = compaction.internalWorkerSessionId ??
(line.id === RUNTIME_COMPACTION_LINE_ID && isCompactionService(worker) &&
activeCompactionServiceSessionId(projection) === worker.session_id
? worker.session_id
: undefined);
if (internalWorkerSessionId !== worker.session_id) return line;
changed = true;
return {
...line,
compaction: {
...line.compaction,
candidate: compactionCandidate(projection, sessionId),
activity: compactionActivity(projection, sessionId)
}
...compaction,
internalWorkerSessionId,
activity: compactionActivity(projection, internalWorkerSessionId),
},
};
});
return changed ? { ...projection, lines } : projection;
@@ -938,26 +942,12 @@ export function applyProtocolEvent(
projectInternalWorkerSnapshot(worker, envelope.eventId, next.cwd)
);
next.removedInternalWorkers = {};
if (event.data.in_flight?.compaction) {
const withCompaction = applyInFlightCompaction(
next,
event.data.in_flight.compaction,
);
next.lines = withCompaction.lines;
next.compaction = withCompaction.compaction;
}
for (const line of next.lines) {
const compaction = line.compaction;
if (!compaction) continue;
const sessionId = compaction.internalWorkerSessionId;
if (sessionId) {
line.compaction = {
...compaction,
candidate: compactionCandidate(next, sessionId),
activity: compactionActivity(next, sessionId),
};
}
}
const withCompaction = applyInFlightCompaction(
next,
event.data.in_flight?.compaction ?? null,
);
next.lines = withCompaction.lines;
next.compaction = withCompaction.compaction;
applyWorkerStateSnapshot(next, event.data.state);
break;
}
@@ -991,7 +981,7 @@ export function applyProtocolEvent(
};
if (existingIndex >= 0) next.internalWorkers[existingIndex] = updated;
else next.internalWorkers.push(updated);
return refreshCompactionActivity(next, event.data.worker.session_id);
return refreshCompactionActivity(next, event.data.worker);
}
case "internal_worker_removed": {
const existingIndex = next.internalWorkers.findIndex((worker) =>
@@ -1017,12 +1007,15 @@ export function applyProtocolEvent(
break;
case "segment_rotated": {
const retainedErrors = next.lines.filter((line) => line.kind === "error");
const retainedCompaction = next.lines.filter((line) =>
line.id === RUNTIME_COMPACTION_LINE_ID
);
const segment = snapshotProjectionFromSession(
envelope.eventId,
event.data.session,
next.cwd,
);
next.lines = [...segment.lines, ...retainedErrors];
next.lines = [...segment.lines, ...retainedErrors, ...retainedCompaction];
next.tasks = segment.tasks;
next.taskNextId = segment.taskNextId;
break;
@@ -1062,7 +1055,9 @@ export function applyProtocolEvent(
case "compact_start":
case "compact_done":
case "compact_failed":
return applyCompactionLifecycle(next, event.data.lifecycle);
// Durable lifecycle events are retained only as historical protocol
// compatibility. Runtime progress is the sole Console block authority.
break;
case "shutdown":
next.status = "shutdown";
break;
@@ -1432,16 +1427,25 @@ function toolCallSignature(toolCall: ToolCallView): string {
return `Read(${readPath(toolCall)})`;
case "Write":
case "Edit": {
const path = displayPath(stringField(args, "file_path") ?? "?", toolCall.cwd);
const path = displayPath(
stringField(args, "file_path") ?? "?",
toolCall.cwd,
);
return `${toolCall.name}(${path})`;
}
case "Glob":
return `Glob(${stringField(args, "pattern") ?? genericCallArguments(toolCall)})`;
return `Glob(${
stringField(args, "pattern") ?? genericCallArguments(toolCall)
})`;
case "Grep":
return `Grep(${stringField(args, "pattern") ?? genericCallArguments(toolCall)})`;
return `Grep(${
stringField(args, "pattern") ?? genericCallArguments(toolCall)
})`;
case "Bash": {
const command = stringField(args, "command");
return `Bash(${command ? `$ ${singleLine(command)}` : genericCallArguments(toolCall)})`;
return `Bash(${
command ? `$ ${singleLine(command)}` : genericCallArguments(toolCall)
})`;
}
default:
return `${toolCall.name}(${genericCallArguments(toolCall)})`;
@@ -1462,7 +1466,9 @@ function singleLine(value: string): string {
}
function toolCallStatus(toolCall: ToolCallView): string {
return toolCall.name === "Bash" ? commandStateSuffix(toolCall) : stateSuffix(toolCall.state);
return toolCall.name === "Bash"
? commandStateSuffix(toolCall)
: stateSuffix(toolCall.state);
}
function aggregateReadToolLines(lines: ConsoleLine[]): ConsoleLine[] {
@@ -1661,7 +1667,9 @@ function commandStateSuffix(toolCall: ToolCallView): string {
: `completed (exit ${command.exit_code})`;
}
if (command.status === "failed") {
return command.exit_code === null ? "failed" : `failed (exit ${command.exit_code})`;
return command.exit_code === null
? "failed"
: `failed (exit ${command.exit_code})`;
}
if (command.status === "timed_out") return "timed out";
if (command.status === "cancelled") return "cancelled";
@@ -1679,7 +1687,9 @@ function commandTiming(command?: CommandSnapshot): string | undefined {
0,
command.last_output_at_ms - command.started_at_ms,
);
return `elapsed ${durationLabel(elapsed)} · last output at +${durationLabel(lastOutputElapsed)}`;
return `elapsed ${durationLabel(elapsed)} · last output at +${
durationLabel(lastOutputElapsed)
}`;
}
function durationLabel(milliseconds: number): string {
@@ -1687,7 +1697,9 @@ function durationLabel(milliseconds: number): string {
return `${(milliseconds / 1000).toFixed(milliseconds < 10_000 ? 1 : 0)}s`;
}
function renderLiveCommandOutput(command?: CommandSnapshot): string | undefined {
function renderLiveCommandOutput(
command?: CommandSnapshot,
): string | undefined {
if (!command) return undefined;
const stdout = compactLines([
command.stdout.truncated ? "[… earlier stdout omitted]" : undefined,
@@ -1712,7 +1724,9 @@ function toolCallDetail(toolCall: ToolCallView): string {
return compactLines([
`id: ${toolCall.id}`,
`state: ${stateSuffix(toolCall.state)}`,
toolCall.command ? `command: ${commandTiming(toolCall.command)}` : undefined,
toolCall.command
? `command: ${commandTiming(toolCall.command)}`
: undefined,
toolCall.summary
? `summary: ${
normalizeKnownToolResult(toolCall.name, toolCall.summary, toolCall.cwd)
@@ -2032,63 +2046,6 @@ function applySessionEntry(
}
}
function applyExtensionEntry(
projection: ConsoleProjection,
eventId: string,
entry: Record<string, unknown>,
) {
if (entry["domain"] !== "yoi.compaction") {
return;
}
const payload = entry["payload"];
if (!isRecord(payload)) return;
if (
typeof payload["compaction_id"] === "string" &&
typeof payload["revision"] === "number" &&
typeof payload["state"] === "string" &&
typeof payload["started_at_ms"] === "number"
) {
const updated = applyCompactionLifecycle(
projection,
payload as unknown as CompactionLifecycle,
);
projection.lines = updated.lines;
return;
}
// Schema v1 remains readable historical evidence.
if (payload["kind"] !== "compaction_block") {
return;
}
const blockId = stringField(payload, "block_id") || "compact";
const state = stringField(payload, "state") || "running";
const message = stringField(payload, "message") ||
compactMessageForState(state, payload);
upsertStatusLine(
projection,
blockId,
eventId,
message,
state === "running",
state === "failed",
);
}
function compactMessageForState(
state: string,
payload: Record<string, unknown>,
): string {
switch (state) {
case "done":
return "Compacted.";
case "failed": {
const error = stringField(payload, "error");
return error ? `Compact failed: ${error}` : "Compact failed.";
}
default:
return "Compacting…";
}
}
function applyLoggedUserInput(
projection: ConsoleProjection,
eventId: string,
@@ -560,6 +560,20 @@ Deno.test("Worker Console owns its narrower centered shell width", async () => {
);
});
Deno.test("Worker Console renders runtime compaction as one activity block", async () => {
const consoleLine = await Deno.readTextFile(
new URL("./ConsoleLineItem.svelte", import.meta.url),
);
assert(
consoleLine.includes("function compactionHeading") &&
consoleLine.includes("Compacting ·") &&
consoleLine.includes("{#each item.compaction.activity as activity}") &&
consoleLine.includes("<li>{activity}</li>"),
"Compaction should render as one live block with nested service activity",
);
});
Deno.test("Worker Console overview activity summaries use 14px text", async () => {
const consoleLine = await Deno.readTextFile(
new URL("./ConsoleLineItem.svelte", import.meta.url),