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
+47 -1
View File
@@ -5298,7 +5298,12 @@ impl<C: LlmClient + 'static, St: Store> Worker<C, St> {
} }
pub(crate) async fn retry_pending_compaction_cleanup(&self) -> Result<(), WorkerError> { pub(crate) async fn retry_pending_compaction_cleanup(&self) -> Result<(), WorkerError> {
self.release_pending_compaction_service().await let cleanup_pending = self.has_pending_compaction_cleanup();
self.release_pending_compaction_service().await?;
if cleanup_pending {
self.set_compaction_progress(None);
}
Ok(())
} }
pub(crate) async fn finish_pending_compaction_cleanup_for_shutdown( pub(crate) async fn finish_pending_compaction_cleanup_for_shutdown(
@@ -8979,6 +8984,15 @@ mod build_summary_prompt_tests {
let (cleanup_session_id, _events) = registry.install_service_for_test(); let (cleanup_session_id, _events) = registry.install_service_for_test();
registry.fail_service_stops_for_test(&cleanup_session_id, 4); registry.fail_service_stops_for_test(&cleanup_session_id, 4);
worker.internal_worker_registry = Some(Arc::clone(&registry)); worker.internal_worker_registry = Some(Arc::clone(&registry));
let (event_tx, mut event_rx) = broadcast::channel(16);
worker.attach_working_event_tx(event_tx.clone());
let in_flight = InFlightEvents::new(event_tx);
worker.attach_in_flight_events(in_flight.clone());
worker.set_compaction_progress(Some(InFlightCompaction {
phase: CompactionPhase::Summarizing,
started_at_ms: 1,
trigger: CompactionTrigger::Manual,
}));
*worker *worker
.pending_compaction_cleanup .pending_compaction_cleanup
.lock() .lock()
@@ -8995,12 +9009,44 @@ mod build_summary_prompt_tests {
} }
assert!(worker.has_pending_compaction_cleanup()); assert!(worker.has_pending_compaction_cleanup());
assert!(registry.has_service_for_test(&cleanup_session_id)); assert!(registry.has_service_for_test(&cleanup_session_id));
let in_flight_guard = in_flight.snapshot_guard();
assert!(
crate::in_flight::snapshot_from_guard(&in_flight_guard)
.compaction
.is_some(),
"failed cleanup retries must keep reconnect compaction state active"
);
drop(in_flight_guard);
while let Ok(event) = event_rx.try_recv() {
assert!(
!matches!(event, Event::CompactionProgress { compaction: None }),
"terminal progress must not publish before the service stops"
);
}
tokio::time::timeout(Duration::from_secs(1), cleanup) tokio::time::timeout(Duration::from_secs(1), cleanup)
.await .await
.expect("shutdown cleanup must converge after retry") .expect("shutdown cleanup must converge after retry")
.unwrap(); .unwrap();
assert!(!worker.has_pending_compaction_cleanup()); assert!(!worker.has_pending_compaction_cleanup());
assert!(!registry.has_service_for_test(&cleanup_session_id)); assert!(!registry.has_service_for_test(&cleanup_session_id));
let in_flight_guard = in_flight.snapshot_guard();
assert!(
crate::in_flight::snapshot_from_guard(&in_flight_guard)
.compaction
.is_none(),
"successful cleanup retry must clear reconnect compaction state"
);
drop(in_flight_guard);
let mut saw_clear = false;
while let Ok(event) = event_rx.try_recv() {
if matches!(event, Event::CompactionProgress { compaction: None }) {
saw_clear = true;
}
}
assert!(
saw_clear,
"successful cleanup retry must publish terminal progress"
);
} }
#[tokio::test] #[tokio::test]
@@ -12,7 +12,7 @@
let nowMs = $state(Date.now()); let nowMs = $state(Date.now());
$effect(() => { $effect(() => {
if (item.compaction?.state !== 'running') return; if (!item.compaction) return;
nowMs = Date.now(); nowMs = Date.now();
const timer = window.setInterval(() => { const timer = window.setInterval(() => {
nowMs = Date.now(); nowMs = Date.now();
@@ -23,7 +23,7 @@
function compactionElapsedMs(line: ConsoleLine): number { function compactionElapsedMs(line: ConsoleLine): number {
const compaction = line.compaction; const compaction = line.compaction;
if (!compaction) return 0; 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 { function formatElapsed(ms: number): string {
@@ -33,8 +33,12 @@
return `${minutes}m ${seconds % 60}s`; return `${minutes}m ${seconds % 60}s`;
} }
function compactionState(line: ConsoleLine): string { function compactionHeading(line: ConsoleLine): string {
return line.compaction?.state.replace('_', ' ') ?? ''; const compaction = line.compaction;
if (!compaction) return '';
return compaction.phase
? `Compacting · ${compaction.phase.replace('_', ' ')}`
: 'Compacting…';
} }
function lineClass(line: ConsoleLine): string { function lineClass(line: ConsoleLine): string {
@@ -80,7 +84,7 @@
> >
{#if item.compaction} {#if item.compaction}
<div class="compaction-heading"> <div class="compaction-heading">
<span>Compaction · {compactionState(item)}</span> <span>{compactionHeading(item)}</span>
<span>{formatElapsed(compactionElapsedMs(item))}</span> <span>{formatElapsed(compactionElapsedMs(item))}</span>
</div> </div>
{#if item.compaction.activity.length > 0} {#if item.compaction.activity.length > 0}
@@ -90,16 +94,6 @@
{/each} {/each}
</ul> </ul>
{/if} {/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)} {:else if shouldRenderHeading(item)}
<div class="message-heading"> <div class="message-heading">
<span>{item.title}</span> <span>{item.title}</span>
@@ -119,7 +113,7 @@
</div> </div>
{/if} {/if}
{#if item.compaction} {#if item.compaction}
<!-- rendered as one lifecycle item above --> <!-- rendered as one runtime activity item above -->
{:else if item.kind === 'tool'} {:else if item.kind === 'tool'}
{#if toolBodyText(item)} {#if toolBodyText(item)}
<p class="console-plain-text"> <p class="console-plain-text">
@@ -216,24 +210,6 @@
font-size: var(--font-size-compact); 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 { .activity-summary {
font-size: var(--font-size-body); 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 { import {
type ConsoleEventInput, type ConsoleEventInput,
type ConsoleLine, type ConsoleLine,
@@ -22,9 +26,7 @@ declare const Deno: {
function workerState(status: WorkerStatus): WorkerStateSnapshot { function workerState(status: WorkerStatus): WorkerStateSnapshot {
return { return {
last_command_id: 0, last_command_id: 0,
state: status === "idle" state: status === "idle" ? { kind: "idle" } : {
? { kind: "idle" }
: {
kind: "busy", kind: "busy",
state: { state: {
kind: "run", kind: "run",
@@ -61,7 +63,8 @@ function consoleLine(id: string, kind: ConsoleLine["kind"]): ConsoleLine {
function canonicalSession(logEntries: unknown[]): Event extends { function canonicalSession(logEntries: unknown[]): Event extends {
event: "snapshot"; event: "snapshot";
data: infer D; data: infer D;
} ? D extends { session: infer S } ? S : never : never { } ? D extends { session: infer S } ? S : never
: never {
const entries: Record<string, unknown>[] = []; const entries: Record<string, unknown>[] = [];
let sequence = 0; let sequence = 0;
const itemEntry = (item: Record<string, unknown>) => { const itemEntry = (item: Record<string, unknown>) => {
@@ -80,9 +83,15 @@ function canonicalSession(logEntries: unknown[]): Event extends {
case "segment_start": case "segment_start":
case "annotated_segment_start": case "annotated_segment_start":
entries.length = 0; 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>; 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; break;
case "user_input": case "user_input":
@@ -99,14 +108,17 @@ function canonicalSession(logEntries: unknown[]): Event extends {
case "annotated_assistant_item": case "annotated_assistant_item":
case "annotated_tool_result": { case "annotated_tool_result": {
const annotated = entry["entry"] as Record<string, unknown> | undefined; const annotated = entry["entry"] as Record<string, unknown> | undefined;
itemEntry((annotated?.["item"] as Record<string, unknown> | undefined) ?? itemEntry(
(entry["item"] as Record<string, unknown>)); (annotated?.["item"] as Record<string, unknown> | undefined) ??
(entry["item"] as Record<string, unknown>),
);
break; break;
} }
case "system_item": case "system_item":
case "annotated_system_item": { case "annotated_system_item": {
const annotated = entry["entry"] as Record<string, unknown> | undefined; const annotated = entry["entry"] as Record<string, unknown> | undefined;
const item = (annotated?.["item"] as Record<string, unknown> | undefined) ?? const item =
(annotated?.["item"] as Record<string, unknown> | undefined) ??
(entry["item"] as Record<string, unknown>); (entry["item"] as Record<string, unknown>);
entries.push({ entries.push({
entry_id: `legacy-test-${sequence++}`, entry_id: `legacy-test-${sequence++}`,
@@ -168,7 +180,10 @@ Deno.test("large paste segments project compact artifact metadata", () => {
source_entry_id: "entry-1", 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("65536 bytes"), "bounded size metadata is visible");
assert(text.includes("text_plain_utf8"), "media type is visible"); assert(text.includes("text_plain_utf8"), "media type is visible");
assert(text.includes("available"), "availability 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.workerState, freshIdle);
assertEquals(projection.status, "idle"); assertEquals(projection.status, "idle");
assert( 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", "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", 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", () => { Deno.test("snapshot restores bounded in-flight Bash command output", () => {
const snapshot = snapshotEvent("/repo"); 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.state = workerState("running");
snapshot.data.in_flight = { snapshot.data.in_flight = {
blocks: [{ 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"); const [line] = projection.lines.filter((line) => line.kind === "tool");
assertEquals(line.toolCallLabel, "Bash($ slow)"); assertEquals(line.toolCallLabel, "Bash($ slow)");
assertEquals(line.toolStatus, "running…"); 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"); const [line] = projection.lines.filter((line) => line.kind === "tool");
assertEquals(line.title, "Call · CustomTool"); 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.toolStatus, "done");
assertEquals(line.body.split("\n").length, 3); assertEquals(line.body.split("\n").length, 3);
assert( assert(
line.body.includes("out1") && line.body.includes("… +3 more lines"), line.body.includes("out1") && line.body.includes("… +3 more lines"),
"normal display should retain the capped response preview", "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( assert(
line.detail?.includes("arguments:\nfirst: one") === true && line.detail?.includes("arguments:\nfirst: one") === true &&
line.detail?.includes("fourth: four") === 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 toolLines = projection.lines.filter((line) => line.kind === "tool");
const jsonLine = toolLines.find((line) => line.id.includes("json-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(jsonLine, "JSON tool line should be projected");
assert(invalidLine, "invalid JSON tool line should be projected"); assert(invalidLine, "invalid JSON tool line should be projected");
assert( assert(
@@ -924,7 +957,10 @@ Deno.test("projectConsole caps Grep preview but keeps complete detail body", ()
assertEquals(line.toolStatus, "done"); assertEquals(line.toolStatus, "done");
assert(line.body.includes("hit1"), "first result should be shown"); assert(line.body.includes("hit1"), "first result should be shown");
assert(line.body.includes("hit5"), "fifth 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( assert(
line.body.includes("… +1 more results"), line.body.includes("… +1 more results"),
"preview should show the omitted result count", "preview should show the omitted result count",
@@ -1018,7 +1054,7 @@ Deno.test("projectConsole renders alert events", () => {
assertEquals(projection.lines[1].error, true); 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 = { const running = {
schema_version: 2, schema_version: 2,
compaction_id: "compaction-1", compaction_id: "compaction-1",
@@ -1033,23 +1069,14 @@ Deno.test("projectConsole upserts compaction lifecycle by stable id", () => {
} as const; } as const;
const projection = projectConsole([ const projection = projectConsole([
{ {
eventId: "compact-1", eventId: "compact-start",
event: { event: "compact_start", data: { lifecycle: running } } satisfies Event, 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-2", eventId: "compact-done",
event: { event: {
event: "compact_done", event: "compact_done",
data: { data: {
@@ -1064,17 +1091,32 @@ Deno.test("projectConsole upserts compaction lifecycle by stable id", () => {
}, },
} satisfies Event, } 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(projection.lines, []);
assertEquals(completed.lines[0].id, "compaction-compaction-1"); assertEquals(projection.compaction, null);
assertEquals(completed.lines[0].compaction?.state, "done");
assertEquals(completed.lines[0].compaction?.summary, "accepted summary");
assertEquals(completed.lines[0].streaming, false);
}); });
Deno.test("snapshot restores running compaction without staged content", () => { 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 = { snapshot.data.in_flight = {
blocks: [], blocks: [],
compaction: { compaction: {
@@ -1083,37 +1125,223 @@ Deno.test("snapshot restores running compaction without staged content", () => {
trigger: "manual", 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 }]); 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?.phase, "summarizing");
assertEquals(projection.compaction?.trigger, "manual"); 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 = { const worker = {
session_id: "compactor-session", session_id: "compactor-session",
name: "Compaction", name: "Compaction",
parent_session_id: "parent-session", parent_session_id: "parent-session",
kind: { service: { kind: "compaction" } }, kind: { service: { kind: "compaction" } },
} as const; } 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([ const projection = projectConsole([
{ {
eventId: "compaction-start", eventId: "compaction-progress",
event: { event: "compact_start", data: { lifecycle } } satisfies Event, event: {
event: "compaction_progress",
data: {
compaction: {
phase: "summarizing",
started_at_ms: 1_000,
trigger: "manual",
},
},
} satisfies Event,
}, },
{ {
eventId: "compaction-tool", 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.length, 1);
assertEquals(projection.lines[0].id, "compaction-compaction-nested"); assertEquals(projection.lines[0].id, "compaction-runtime");
assertEquals(projection.lines[0].compaction?.activity, ["write_summary — running"]); assertEquals(projection.lines[0].compaction?.activity, [
assertEquals(projection.lines[0].compaction?.candidate, "draft candidate"); "write_summary — running",
]);
assert(
!JSON.stringify(projection.lines[0]).includes("draft candidate"),
"uncommitted summary content is not projected into the parent Console",
);
assert( assert(
consoleWorkerViews(projection).length === 1, consoleWorkerViews(projection).length === 1,
"service is not a selectable SubWorker pane", "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", () => { Deno.test("snapshot excludes storage-only compaction extension records", () => {
const projection = projectConsole([{ const projection = projectConsole([{
eventId: "snapshot", eventId: "snapshot",
@@ -1186,61 +1514,126 @@ Deno.test("snapshot excludes storage-only compaction extension records", () => {
assertEquals(projection.lines.length, 0); assertEquals(projection.lines.length, 0);
}); });
Deno.test("createConsoleProjector ignores stale compaction revisions", () => { Deno.test("segment rotation preserves compaction progress in either event order", () => {
const projector = createConsoleProjector(); const progress = {
const base = { event: "compaction_progress",
schema_version: 2, data: {
compaction_id: "compaction-identity", compaction: {
revision: 1, phase: "committing",
internal_worker: null,
state: "running",
started_at_ms: 1_000, started_at_ms: 1_000,
ended_at_ms: null, trigger: "manual",
summary: null, },
error: null, },
new_segment_id: null, } 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; } as const;
const projector = createConsoleProjector();
let projection = projector.append([ let projection = projector.append([
{ {
eventId: "compact-identity-1", eventId: "first-progress",
event: { event: {
event: "user_message", event: "compaction_progress",
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",
data: { data: {
lifecycle: { compaction: {
...base, phase: "summarizing",
revision: 2, started_at_ms: 1_000,
state: "done", trigger: "manual",
ended_at_ms: 2_000,
summary: "accepted",
new_segment_id: "00000000-0000-0000-0000-000000000001",
}, },
}, },
} satisfies Event, } satisfies Event,
}, },
{ {
eventId: "compact-identity-stale", eventId: "first-read",
event: { event: "compact_start", data: { lifecycle: base } } satisfies Event, 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.length, 1);
assertEquals(projection.lines[1].compaction?.state, "done"); assertEquals(projection.lines[0].compaction?.startedAtMs, 2_000);
assertEquals(projection.lines[1].compaction?.summary, "accepted"); 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", () => { 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([ const projection = projectConsole([
{ {
eventId: "30", eventId: "30",
event: { event: "worker_state", data: { snapshot: workerState("running") } } satisfies Event, event: {
event: "worker_state",
data: { snapshot: workerState("running") },
} satisfies Event,
}, },
{ {
eventId: "31", eventId: "31",
@@ -1629,7 +2025,9 @@ Deno.test("projectConsole renders snapshot entries and in-flight output", () =>
assertEquals(projection.status, "running"); assertEquals(projection.status, "running");
assertEquals( assertEquals(
projection.lines.map((line) => 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", "user:seed user:false",
@@ -1872,7 +2270,8 @@ Deno.test("projectConsole relativizes known tool path displays from snapshot cwd
); );
assert( assert(
toolLines.some((line) => 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", "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", kind: "sub_worker",
}, },
revision: 1, 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", kind: "sub_worker",
}, },
revision: 1, 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", kind: "sub_worker",
}, },
revision: 1, revision: 1,
event: { event: "worker_state", data: { snapshot: workerState("running") } }, event: {
event: "worker_state",
data: { snapshot: workerState("running") },
},
}, },
}, },
}]); }]);
+110 -153
View File
@@ -3,7 +3,6 @@ import type {
CommandEvent, CommandEvent,
CommandSnapshot, CommandSnapshot,
CommandStreamSlice, CommandStreamSlice,
CompactionLifecycle,
Event as ProtocolEvent, Event as ProtocolEvent,
InFlightBlock, InFlightBlock,
InFlightCompaction, InFlightCompaction,
@@ -73,14 +72,8 @@ export type ConsoleDiffLine = {
export type ConsoleViewMode = "overview" | "normal"; export type ConsoleViewMode = "overview" | "normal";
export type ConsoleCompaction = { export type ConsoleCompaction = {
id: string; phase?: InFlightCompaction["phase"];
revision: number;
state: "running" | "done" | "failed" | "interrupted";
startedAtMs: number; startedAtMs: number;
endedAtMs?: number;
summary?: string;
candidate?: string;
error?: string;
internalWorkerSessionId?: string; internalWorkerSessionId?: string;
activity: string[]; activity: string[];
}; };
@@ -514,7 +507,9 @@ function appendSnapshotCommands(
commands: CommandSnapshot[], commands: CommandSnapshot[],
eventId: string, eventId: string,
): void { ): void {
commands.forEach((command) => upsertCommandSnapshot(projection, eventId, command)); commands.forEach((command) =>
upsertCommandSnapshot(projection, eventId, command)
);
} }
function upsertCommandSnapshot( function upsertCommandSnapshot(
@@ -631,7 +626,8 @@ function appendCommandStream(
start_offset: endOffset - tail.length, start_offset: endOffset - tail.length,
end_offset: endOffset, end_offset: endOffset,
content: tail, content: tail,
truncated: existing.truncated || !contiguous || tail.length < combined.length || truncated: existing.truncated || !contiguous ||
tail.length < combined.length ||
startOffset > 0, startOffset > 0,
}; };
} }
@@ -678,107 +674,115 @@ function projectInternalWorkerSnapshot(
return { worker: snapshot.worker, revision: snapshot.revision, console }; return { worker: snapshot.worker, revision: snapshot.revision, console };
} }
function compactionCandidate( const RUNTIME_COMPACTION_LINE_ID = "compaction-runtime";
projection: ConsoleProjection,
sessionId: string | undefined, function isCompactionService(worker: InternalWorkerRef): boolean {
): string | undefined { return worker.kind !== "sub_worker" &&
if (!sessionId) return undefined; worker.kind.service.kind === "compaction";
const worker = projection.internalWorkers.find(
(candidate) => candidate.worker.session_id === sessionId,
);
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;
} }
function activeCompactionServiceSessionId(
projection: ConsoleProjection,
): string | undefined {
const candidates = projection.internalWorkers.filter((worker) =>
isCompactionService(worker.worker)
);
return candidates.length === 1 ? candidates[0]?.worker.session_id : undefined;
} }
function compactionActivity( function compactionActivity(
projection: ConsoleProjection, projection: ConsoleProjection,
sessionId: string | undefined sessionId: string | undefined,
): string[] { ): string[] {
if (!sessionId) return []; if (!sessionId) return [];
const worker = projection.internalWorkers.find( const worker = projection.internalWorkers.find(
(candidate) => candidate.worker.session_id === sessionId (candidate) => candidate.worker.session_id === sessionId,
); );
if (!worker) return []; if (!worker) return [];
return worker.console.lines return worker.console.lines
.filter((line) => line.kind === "tool" || line.kind === "status" || line.kind === "error") .filter((line) => line.kind === "tool")
.slice(-12) .slice(-12)
.map((line) => .map((line) =>
line.toolCall?.name === "write_summary" line.toolCall
? `write_summary — ${line.toolCall.state}` ? `${line.toolCall.name} — ${line.toolCall.state}`
: line.body || line.title : 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( function applyInFlightCompaction(
projection: ConsoleProjection, projection: ConsoleProjection,
progress: InFlightCompaction | null, progress: InFlightCompaction | null,
): ConsoleProjection { ): 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( const internalWorkerSessionId = existing?.internalWorkerSessionId ??
projection: ConsoleProjection, activeCompactionServiceSessionId(projection);
lifecycle: CompactionLifecycle const observedActivity = compactionActivity(
): ConsoleProjection { projection,
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,
internalWorkerSessionId, 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 = { const line: ConsoleLine = {
id: lineId, id: RUNTIME_COMPACTION_LINE_ID,
source: "event", source: "event",
kind: compaction.state === "failed" ? "error" : "status", kind: "status",
title: "Compaction", title: "Compaction",
body: compaction.summary ?? compaction.error ?? "", body: "",
streaming: compaction.state === "running", streaming: true,
error: compaction.state === "failed", compaction,
compaction
}; };
const index = projection.lines.findIndex((candidate) => candidate.id === lineId);
const lines = [...projection.lines]; const lines = [...projection.lines];
if (index >= 0) lines[index] = line; if (lineIndex >= 0) lines[lineIndex] = line;
else lines.push(line); else lines.push(line);
return { ...projection, lines }; return { ...projection, compaction: progress, lines };
} }
function refreshCompactionActivity( function refreshCompactionActivity(
projection: ConsoleProjection, projection: ConsoleProjection,
sessionId: string worker: InternalWorkerRef,
): ConsoleProjection { ): ConsoleProjection {
let changed = false; let changed = false;
const lines = projection.lines.map((line) => { 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; changed = true;
return { return {
...line, ...line,
compaction: { compaction: {
...line.compaction, ...compaction,
candidate: compactionCandidate(projection, sessionId), internalWorkerSessionId,
activity: compactionActivity(projection, sessionId) activity: compactionActivity(projection, internalWorkerSessionId),
} },
}; };
}); });
return changed ? { ...projection, lines } : projection; return changed ? { ...projection, lines } : projection;
@@ -938,26 +942,12 @@ export function applyProtocolEvent(
projectInternalWorkerSnapshot(worker, envelope.eventId, next.cwd) projectInternalWorkerSnapshot(worker, envelope.eventId, next.cwd)
); );
next.removedInternalWorkers = {}; next.removedInternalWorkers = {};
if (event.data.in_flight?.compaction) {
const withCompaction = applyInFlightCompaction( const withCompaction = applyInFlightCompaction(
next, next,
event.data.in_flight.compaction, event.data.in_flight?.compaction ?? null,
); );
next.lines = withCompaction.lines; next.lines = withCompaction.lines;
next.compaction = withCompaction.compaction; 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),
};
}
}
applyWorkerStateSnapshot(next, event.data.state); applyWorkerStateSnapshot(next, event.data.state);
break; break;
} }
@@ -991,7 +981,7 @@ export function applyProtocolEvent(
}; };
if (existingIndex >= 0) next.internalWorkers[existingIndex] = updated; if (existingIndex >= 0) next.internalWorkers[existingIndex] = updated;
else next.internalWorkers.push(updated); else next.internalWorkers.push(updated);
return refreshCompactionActivity(next, event.data.worker.session_id); return refreshCompactionActivity(next, event.data.worker);
} }
case "internal_worker_removed": { case "internal_worker_removed": {
const existingIndex = next.internalWorkers.findIndex((worker) => const existingIndex = next.internalWorkers.findIndex((worker) =>
@@ -1017,12 +1007,15 @@ export function applyProtocolEvent(
break; break;
case "segment_rotated": { case "segment_rotated": {
const retainedErrors = next.lines.filter((line) => line.kind === "error"); const retainedErrors = next.lines.filter((line) => line.kind === "error");
const retainedCompaction = next.lines.filter((line) =>
line.id === RUNTIME_COMPACTION_LINE_ID
);
const segment = snapshotProjectionFromSession( const segment = snapshotProjectionFromSession(
envelope.eventId, envelope.eventId,
event.data.session, event.data.session,
next.cwd, next.cwd,
); );
next.lines = [...segment.lines, ...retainedErrors]; next.lines = [...segment.lines, ...retainedErrors, ...retainedCompaction];
next.tasks = segment.tasks; next.tasks = segment.tasks;
next.taskNextId = segment.taskNextId; next.taskNextId = segment.taskNextId;
break; break;
@@ -1062,7 +1055,9 @@ export function applyProtocolEvent(
case "compact_start": case "compact_start":
case "compact_done": case "compact_done":
case "compact_failed": 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": case "shutdown":
next.status = "shutdown"; next.status = "shutdown";
break; break;
@@ -1432,16 +1427,25 @@ function toolCallSignature(toolCall: ToolCallView): string {
return `Read(${readPath(toolCall)})`; return `Read(${readPath(toolCall)})`;
case "Write": case "Write":
case "Edit": { case "Edit": {
const path = displayPath(stringField(args, "file_path") ?? "?", toolCall.cwd); const path = displayPath(
stringField(args, "file_path") ?? "?",
toolCall.cwd,
);
return `${toolCall.name}(${path})`; return `${toolCall.name}(${path})`;
} }
case "Glob": case "Glob":
return `Glob(${stringField(args, "pattern") ?? genericCallArguments(toolCall)})`; return `Glob(${
stringField(args, "pattern") ?? genericCallArguments(toolCall)
})`;
case "Grep": case "Grep":
return `Grep(${stringField(args, "pattern") ?? genericCallArguments(toolCall)})`; return `Grep(${
stringField(args, "pattern") ?? genericCallArguments(toolCall)
})`;
case "Bash": { case "Bash": {
const command = stringField(args, "command"); const command = stringField(args, "command");
return `Bash(${command ? `$ ${singleLine(command)}` : genericCallArguments(toolCall)})`; return `Bash(${
command ? `$ ${singleLine(command)}` : genericCallArguments(toolCall)
})`;
} }
default: default:
return `${toolCall.name}(${genericCallArguments(toolCall)})`; return `${toolCall.name}(${genericCallArguments(toolCall)})`;
@@ -1462,7 +1466,9 @@ function singleLine(value: string): string {
} }
function toolCallStatus(toolCall: ToolCallView): 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[] { function aggregateReadToolLines(lines: ConsoleLine[]): ConsoleLine[] {
@@ -1661,7 +1667,9 @@ function commandStateSuffix(toolCall: ToolCallView): string {
: `completed (exit ${command.exit_code})`; : `completed (exit ${command.exit_code})`;
} }
if (command.status === "failed") { 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 === "timed_out") return "timed out";
if (command.status === "cancelled") return "cancelled"; if (command.status === "cancelled") return "cancelled";
@@ -1679,7 +1687,9 @@ function commandTiming(command?: CommandSnapshot): string | undefined {
0, 0,
command.last_output_at_ms - command.started_at_ms, 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 { function durationLabel(milliseconds: number): string {
@@ -1687,7 +1697,9 @@ function durationLabel(milliseconds: number): string {
return `${(milliseconds / 1000).toFixed(milliseconds < 10_000 ? 1 : 0)}s`; 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; if (!command) return undefined;
const stdout = compactLines([ const stdout = compactLines([
command.stdout.truncated ? "[… earlier stdout omitted]" : undefined, command.stdout.truncated ? "[… earlier stdout omitted]" : undefined,
@@ -1712,7 +1724,9 @@ function toolCallDetail(toolCall: ToolCallView): string {
return compactLines([ return compactLines([
`id: ${toolCall.id}`, `id: ${toolCall.id}`,
`state: ${stateSuffix(toolCall.state)}`, `state: ${stateSuffix(toolCall.state)}`,
toolCall.command ? `command: ${commandTiming(toolCall.command)}` : undefined, toolCall.command
? `command: ${commandTiming(toolCall.command)}`
: undefined,
toolCall.summary toolCall.summary
? `summary: ${ ? `summary: ${
normalizeKnownToolResult(toolCall.name, toolCall.summary, toolCall.cwd) 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( function applyLoggedUserInput(
projection: ConsoleProjection, projection: ConsoleProjection,
eventId: string, 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 () => { Deno.test("Worker Console overview activity summaries use 14px text", async () => {
const consoleLine = await Deno.readTextFile( const consoleLine = await Deno.readTextFile(
new URL("./ConsoleLineItem.svelte", import.meta.url), new URL("./ConsoleLineItem.svelte", import.meta.url),