fix: render compaction activity in web console
This commit is contained in:
@@ -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") },
|
||||
},
|
||||
},
|
||||
},
|
||||
}]);
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user