fix: render compaction activity in web console
This commit is contained in:
@@ -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(®istry));
|
worker.internal_worker_registry = Some(Arc::clone(®istry));
|
||||||
|
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,15 +26,13 @@ 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",
|
||||||
: {
|
state: {
|
||||||
kind: "busy",
|
kind: "run",
|
||||||
state: {
|
state: status === "paused" ? "paused" : "running",
|
||||||
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 {
|
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,15 +108,18 @@ 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 =
|
||||||
(entry["item"] as Record<string, unknown>);
|
(annotated?.["item"] as Record<string, unknown> | undefined) ??
|
||||||
|
(entry["item"] as Record<string, unknown>);
|
||||||
entries.push({
|
entries.push({
|
||||||
entry_id: `legacy-test-${sequence++}`,
|
entry_id: `legacy-test-${sequence++}`,
|
||||||
provenance: "legacy_unknown",
|
provenance: "legacy_unknown",
|
||||||
@@ -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,
|
started_at_ms: 1_000,
|
||||||
state: "running",
|
trigger: "manual",
|
||||||
started_at_ms: 1_000,
|
},
|
||||||
ended_at_ms: null,
|
},
|
||||||
summary: null,
|
} satisfies Event;
|
||||||
error: null,
|
const rotation = {
|
||||||
new_segment_id: null,
|
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") },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}]);
|
}]);
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
function isCompactionService(worker: InternalWorkerRef): boolean {
|
||||||
|
return worker.kind !== "sub_worker" &&
|
||||||
|
worker.kind.service.kind === "compaction";
|
||||||
|
}
|
||||||
|
|
||||||
|
function activeCompactionServiceSessionId(
|
||||||
projection: ConsoleProjection,
|
projection: ConsoleProjection,
|
||||||
sessionId: string | undefined,
|
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
if (!sessionId) return undefined;
|
const candidates = projection.internalWorkers.filter((worker) =>
|
||||||
const worker = projection.internalWorkers.find(
|
isCompactionService(worker.worker)
|
||||||
(candidate) => candidate.worker.session_id === sessionId,
|
|
||||||
);
|
);
|
||||||
const calls = worker?.console.lines
|
return candidates.length === 1 ? candidates[0]?.worker.session_id : undefined;
|
||||||
.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 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 ?? null,
|
||||||
event.data.in_flight.compaction,
|
);
|
||||||
);
|
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),
|
||||||
|
|||||||
Reference in New Issue
Block a user