feat: expose compaction as an internal worker
This commit is contained in:
@@ -32,6 +32,14 @@ export type CommandSnapshot = { command_id: string, tool_call_id: string | null,
|
||||
|
||||
export type CommandEvent = { "kind": "started", command_id: string, tool_call_id: string | null, observed_at_ms: number, } | { "kind": "output", command_id: string, stream: CommandStream, start_offset: number, end_offset: number, content: string, observed_at_ms: number, } | { "kind": "terminal", command_id: string, status: CommandStatus, exit_code: number | null, stdout_end_offset: number, stderr_end_offset: number, observed_at_ms: number, };
|
||||
|
||||
export type CompactionLifecycleState = "running" | "done" | "failed" | "interrupted";
|
||||
|
||||
export type CompactionLifecycle = { schema_version: number, compaction_id: string, revision: number, internal_worker?: InternalWorkerRef | null, state: CompactionLifecycleState,
|
||||
/**
|
||||
* Milliseconds since the Unix epoch.
|
||||
*/
|
||||
started_at_ms: number, ended_at_ms?: number | null, summary?: string | null, error?: string | null, new_segment_id?: string | null, };
|
||||
|
||||
export type ScopeRule = {
|
||||
/**
|
||||
* Target path. Must be absolute by the time a `Scope` is built from
|
||||
@@ -63,7 +71,7 @@ export type InFlightBlock = { "kind": "text", text: string, finished?: boolean,
|
||||
|
||||
export type InFlightSnapshot = { blocks?: Array<InFlightBlock>, commands?: Array<CommandSnapshot>, };
|
||||
|
||||
export type InternalWorkerKind = "sub_worker";
|
||||
export type InternalWorkerKind = "sub_worker" | { "service": { kind: string, } };
|
||||
|
||||
export type InternalWorkerRef = { session_id: string, name: string, parent_session_id?: string | null, kind: InternalWorkerKind, };
|
||||
|
||||
@@ -193,4 +201,4 @@ in_flight?: InFlightSnapshot,
|
||||
* Parent-owned Internal Worker sessions visible to this client.
|
||||
* Service-private Internal Workers are deliberately excluded.
|
||||
*/
|
||||
internal_workers?: Array<InternalWorkerSnapshot>, } } | { "event": "internal_worker", "data": { worker: InternalWorkerRef, revision: number, event: Event, } } | { "event": "internal_worker_removed", "data": { worker: InternalWorkerRef, revision: number, } } | { "event": "segment_rotated", "data": { entry: unknown, } } | { "event": "status", "data": { status: WorkerStatus, } } | { "event": "command", "data": { event: CommandEvent, } } | { "event": "completions", "data": { kind: CompletionKind, entries: Array<CompletionEntry>, } } | { "event": "rewind_targets", "data": { head_entries: number, targets: Array<RewindTarget>, } } | { "event": "rewind_applied", "data": { entries: Array<unknown>, input: Array<Segment>, summary: RewindSummary, } } | { "event": "workers_listed", "data": { workers: unknown, } } | { "event": "worker_restored", "data": { result: unknown, } } | { "event": "peer_registered", "data": { result: unknown, } } | { "event": "alert", "data": Alert } | { "event": "memory_worker", "data": MemoryWorkerEvent } | { "event": "compact_start" } | { "event": "compact_done", "data": { new_segment_id: string, } } | { "event": "compact_failed", "data": { error: string, } } | { "event": "shutdown" };
|
||||
internal_workers?: Array<InternalWorkerSnapshot>, } } | { "event": "internal_worker", "data": { worker: InternalWorkerRef, revision: number, event: Event, } } | { "event": "internal_worker_removed", "data": { worker: InternalWorkerRef, revision: number, } } | { "event": "segment_rotated", "data": { entry: unknown, } } | { "event": "status", "data": { status: WorkerStatus, } } | { "event": "command", "data": { event: CommandEvent, } } | { "event": "completions", "data": { kind: CompletionKind, entries: Array<CompletionEntry>, } } | { "event": "rewind_targets", "data": { head_entries: number, targets: Array<RewindTarget>, } } | { "event": "rewind_applied", "data": { entries: Array<unknown>, input: Array<Segment>, summary: RewindSummary, } } | { "event": "workers_listed", "data": { workers: unknown, } } | { "event": "worker_restored", "data": { result: unknown, } } | { "event": "peer_registered", "data": { result: unknown, } } | { "event": "alert", "data": Alert } | { "event": "memory_worker", "data": MemoryWorkerEvent } | { "event": "compact_start", "data": { lifecycle: CompactionLifecycle, } } | { "event": "compact_done", "data": { lifecycle: CompactionLifecycle, } } | { "event": "compact_failed", "data": { lifecycle: CompactionLifecycle, } } | { "event": "shutdown" };
|
||||
|
||||
@@ -8,6 +8,33 @@
|
||||
};
|
||||
|
||||
let { item }: Props = $props();
|
||||
let nowMs = $state(Date.now());
|
||||
|
||||
$effect(() => {
|
||||
if (item.compaction?.state !== 'running') return;
|
||||
nowMs = Date.now();
|
||||
const timer = window.setInterval(() => {
|
||||
nowMs = Date.now();
|
||||
}, 1000);
|
||||
return () => window.clearInterval(timer);
|
||||
});
|
||||
|
||||
function compactionElapsedMs(line: ConsoleLine): number {
|
||||
const compaction = line.compaction;
|
||||
if (!compaction) return 0;
|
||||
return Math.max(0, (compaction.endedAtMs ?? nowMs) - compaction.startedAtMs);
|
||||
}
|
||||
|
||||
function formatElapsed(ms: number): string {
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
if (seconds < 60) return `${seconds}s`;
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
return `${minutes}m ${seconds % 60}s`;
|
||||
}
|
||||
|
||||
function compactionState(line: ConsoleLine): string {
|
||||
return line.compaction?.state.replace('_', ' ') ?? '';
|
||||
}
|
||||
|
||||
function lineClass(line: ConsoleLine): string {
|
||||
return line.error ? 'error' : line.kind;
|
||||
@@ -52,7 +79,29 @@
|
||||
class:error-line={item.error}
|
||||
data-console-line-id={item.id}
|
||||
>
|
||||
{#if shouldRenderHeading(item)}
|
||||
{#if item.compaction}
|
||||
<div class="compaction-heading">
|
||||
<span>Compaction · {compactionState(item)}</span>
|
||||
<span>{formatElapsed(compactionElapsedMs(item))}</span>
|
||||
</div>
|
||||
{#if item.compaction.activity.length > 0}
|
||||
<ul class="compaction-activity">
|
||||
{#each item.compaction.activity as activity}
|
||||
<li>{activity}</li>
|
||||
{/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>
|
||||
</div>
|
||||
@@ -63,7 +112,9 @@
|
||||
<span class={`tool-suffix ${item.toolCall?.state ?? ''}`}>{toolSummary(item).suffix}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if item.kind === 'tool'}
|
||||
{#if item.compaction}
|
||||
<!-- rendered as one lifecycle item above -->
|
||||
{:else if item.kind === 'tool'}
|
||||
{#if bodyTextAfterToolSummary(item)}
|
||||
<p class="console-plain-text">
|
||||
{#if isBashTool(item)}
|
||||
@@ -141,6 +192,43 @@
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.compaction-heading {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-2);
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.78rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.compaction-activity {
|
||||
margin: var(--space-1) 0;
|
||||
padding-left: 1.25rem;
|
||||
color: var(--tui-dark-gray);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.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: 0.7rem;
|
||||
}
|
||||
|
||||
.compaction-error {
|
||||
color: var(--tui-error);
|
||||
}
|
||||
|
||||
.activity-summary {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@@ -647,42 +647,173 @@ Deno.test("projectConsole renders alert events", () => {
|
||||
assertEquals(projection.lines[1].error, true);
|
||||
});
|
||||
|
||||
Deno.test("projectConsole shows compact progress as a status block", () => {
|
||||
Deno.test("projectConsole upserts compaction lifecycle by stable id", () => {
|
||||
const running = {
|
||||
schema_version: 2,
|
||||
compaction_id: "compaction-1",
|
||||
revision: 1,
|
||||
internal_worker: null,
|
||||
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: "compact-1",
|
||||
event: { event: "compact_start" } satisfies Event,
|
||||
event: { event: "compact_start", data: { lifecycle: running } } satisfies Event,
|
||||
},
|
||||
]);
|
||||
|
||||
assertEquals(projection.lines.length, 1);
|
||||
assertEquals(projection.lines[0].id, "status-compact");
|
||||
assertEquals(projection.lines[0].kind, "status");
|
||||
assertEquals(projection.lines[0].body, "Compacting…");
|
||||
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" } satisfies Event,
|
||||
event: { event: "compact_start", data: { lifecycle: running } } satisfies Event,
|
||||
},
|
||||
{
|
||||
eventId: "compact-2",
|
||||
event: {
|
||||
event: "compact_done",
|
||||
data: { new_segment_id: "00000000-0000-0000-0000-000000000001" },
|
||||
data: {
|
||||
lifecycle: {
|
||||
...running,
|
||||
revision: 2,
|
||||
state: "done",
|
||||
ended_at_ms: 4_000,
|
||||
summary: "accepted summary",
|
||||
new_segment_id: "00000000-0000-0000-0000-000000000001",
|
||||
},
|
||||
},
|
||||
} satisfies Event,
|
||||
},
|
||||
]);
|
||||
|
||||
assertEquals(completed.lines.length, 1);
|
||||
assertEquals(completed.lines[0].id, "status-compact");
|
||||
assertEquals(completed.lines[0].body, "Compacted.");
|
||||
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);
|
||||
});
|
||||
|
||||
Deno.test("createConsoleProjector updates only compact status block", () => {
|
||||
Deno.test("compaction service activity stays nested in one lifecycle item", () => {
|
||||
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-tool",
|
||||
event: {
|
||||
event: "internal_worker",
|
||||
data: {
|
||||
worker,
|
||||
revision: 1,
|
||||
event: {
|
||||
event: "tool_call_start",
|
||||
data: { id: "call-1", name: "write_summary" },
|
||||
},
|
||||
},
|
||||
} satisfies Event,
|
||||
},
|
||||
{
|
||||
eventId: "compaction-tool-done",
|
||||
event: {
|
||||
event: "internal_worker",
|
||||
data: {
|
||||
worker,
|
||||
revision: 2,
|
||||
event: {
|
||||
event: "tool_call_done",
|
||||
data: {
|
||||
id: "call-1",
|
||||
name: "write_summary",
|
||||
arguments: JSON.stringify({ text: "draft candidate" }),
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies Event,
|
||||
},
|
||||
]);
|
||||
|
||||
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");
|
||||
assert(
|
||||
consoleWorkerViews(projection).length === 1,
|
||||
"service is not a selectable SubWorker pane",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("snapshot normalizes orphaned running compaction to interrupted", () => {
|
||||
const projection = projectConsole([{
|
||||
eventId: "snapshot",
|
||||
observedAtMs: 9_000,
|
||||
event: snapshotEvent("/repo", [{
|
||||
kind: "extension",
|
||||
ts: 1,
|
||||
domain: "yoi.compaction",
|
||||
payload: {
|
||||
schema_version: 2,
|
||||
compaction_id: "compaction-orphaned",
|
||||
revision: 2,
|
||||
internal_worker: {
|
||||
session_id: "missing-service",
|
||||
name: "Compaction",
|
||||
parent_session_id: "parent",
|
||||
kind: { service: { kind: "compaction" } },
|
||||
},
|
||||
state: "running",
|
||||
started_at_ms: 1_000,
|
||||
},
|
||||
}]),
|
||||
}]);
|
||||
|
||||
assertEquals(projection.lines.length, 1);
|
||||
assertEquals(projection.lines[0].compaction?.state, "interrupted");
|
||||
assertEquals(projection.lines[0].compaction?.endedAtMs, 9_000);
|
||||
assertEquals(projection.lines[0].streaming, false);
|
||||
});
|
||||
|
||||
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,
|
||||
} as const;
|
||||
let projection = projector.append([
|
||||
{
|
||||
eventId: "compact-identity-1",
|
||||
@@ -693,31 +824,37 @@ Deno.test("createConsoleProjector updates only compact status block", () => {
|
||||
},
|
||||
{
|
||||
eventId: "compact-identity-2",
|
||||
event: { event: "compact_start" } satisfies Event,
|
||||
event: { event: "compact_start", data: { lifecycle: base } } satisfies Event,
|
||||
},
|
||||
]);
|
||||
const userLine = projection.lines[0];
|
||||
const compactLine = projection.lines[1];
|
||||
|
||||
projection = projector.append([
|
||||
{
|
||||
eventId: "compact-identity-3",
|
||||
event: {
|
||||
event: "compact_done",
|
||||
data: { new_segment_id: "00000000-0000-0000-0000-000000000001" },
|
||||
data: {
|
||||
lifecycle: {
|
||||
...base,
|
||||
revision: 2,
|
||||
state: "done",
|
||||
ended_at_ms: 2_000,
|
||||
summary: "accepted",
|
||||
new_segment_id: "00000000-0000-0000-0000-000000000001",
|
||||
},
|
||||
},
|
||||
} satisfies Event,
|
||||
},
|
||||
{
|
||||
eventId: "compact-identity-stale",
|
||||
event: { event: "compact_start", data: { lifecycle: base } } satisfies Event,
|
||||
},
|
||||
]);
|
||||
|
||||
assert(
|
||||
projection.lines[0] === userLine,
|
||||
"unrelated message line should keep object identity",
|
||||
);
|
||||
assert(
|
||||
projection.lines[1] !== compactLine,
|
||||
"compact status line should update object identity",
|
||||
);
|
||||
assertEquals(projection.lines[1].body, "Compacted.");
|
||||
assert(projection.lines[0] === userLine, "unrelated line retains identity");
|
||||
assertEquals(projection.lines[1].compaction?.state, "done");
|
||||
assertEquals(projection.lines[1].compaction?.summary, "accepted");
|
||||
});
|
||||
|
||||
Deno.test("projectConsole keeps streaming tool call updates in the same Call block", () => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
CommandEvent,
|
||||
CommandSnapshot,
|
||||
CommandStreamSlice,
|
||||
CompactionLifecycle,
|
||||
Event as ProtocolEvent,
|
||||
InFlightBlock,
|
||||
InFlightToolCallState,
|
||||
@@ -67,12 +68,26 @@ export type ConsoleDiffLine = {
|
||||
|
||||
export type ConsoleViewMode = "overview" | "normal";
|
||||
|
||||
export type ConsoleCompaction = {
|
||||
id: string;
|
||||
revision: number;
|
||||
state: "running" | "done" | "failed" | "interrupted";
|
||||
startedAtMs: number;
|
||||
endedAtMs?: number;
|
||||
summary?: string;
|
||||
candidate?: string;
|
||||
error?: string;
|
||||
internalWorkerSessionId?: string;
|
||||
activity: string[];
|
||||
};
|
||||
|
||||
export type ConsoleLine = {
|
||||
id: string;
|
||||
kind: ConsoleLineKind;
|
||||
title: string;
|
||||
body: string;
|
||||
detail?: string;
|
||||
compaction?: ConsoleCompaction;
|
||||
diff?: ConsoleDiffLine[];
|
||||
eventId?: string | null;
|
||||
source: "event";
|
||||
@@ -114,16 +129,17 @@ export type ConsoleWorkerView = {
|
||||
export function consoleWorkerViews(
|
||||
projection: ConsoleProjection,
|
||||
): ConsoleWorkerView[] {
|
||||
const labels = projection.internalWorkers.map((worker) =>
|
||||
worker.worker.name || "subworker"
|
||||
const children = projection.internalWorkers.filter((worker) =>
|
||||
worker.worker.kind === "sub_worker"
|
||||
);
|
||||
const labels = children.map((worker) => worker.worker.name || "subworker");
|
||||
const labelCounts = new Map<string, number>();
|
||||
for (const label of labels) {
|
||||
labelCounts.set(label, (labelCounts.get(label) ?? 0) + 1);
|
||||
}
|
||||
return [
|
||||
{ sessionId: null, label: "main", console: projection },
|
||||
...projection.internalWorkers.map((worker, index) => {
|
||||
...children.map((worker, index) => {
|
||||
const label = labels[index] ?? "subworker";
|
||||
return {
|
||||
sessionId: worker.worker.session_id,
|
||||
@@ -664,6 +680,105 @@ function projectInternalWorkerSnapshot(
|
||||
return { worker: snapshot.worker, revision: snapshot.revision, console };
|
||||
}
|
||||
|
||||
function compactionCandidate(
|
||||
projection: ConsoleProjection,
|
||||
sessionId: string | undefined,
|
||||
): string | undefined {
|
||||
if (!sessionId) return undefined;
|
||||
const worker = projection.internalWorkers.find(
|
||||
(candidate) => candidate.worker.session_id === sessionId,
|
||||
);
|
||||
const calls = worker?.console.lines
|
||||
.map((line) => line.toolCall)
|
||||
.filter((call): call is ToolCallView => call?.name === "write_summary") ?? [];
|
||||
const latest = calls.at(-1);
|
||||
const raw = latest?.arguments ?? latest?.argsStream;
|
||||
if (!raw) return undefined;
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as { text?: unknown };
|
||||
return typeof parsed.text === "string" ? parsed.text : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function compactionActivity(
|
||||
projection: ConsoleProjection,
|
||||
sessionId: string | undefined
|
||||
): string[] {
|
||||
if (!sessionId) return [];
|
||||
const worker = projection.internalWorkers.find(
|
||||
(candidate) => candidate.worker.session_id === sessionId
|
||||
);
|
||||
if (!worker) return [];
|
||||
return worker.console.lines
|
||||
.filter((line) => line.kind === "tool" || line.kind === "status" || line.kind === "error")
|
||||
.slice(-12)
|
||||
.map((line) =>
|
||||
line.toolCall?.name === "write_summary"
|
||||
? `write_summary — ${line.toolCall.state}`
|
||||
: line.body || line.title
|
||||
)
|
||||
.filter((value, index, values) => value.length > 0 && values.indexOf(value) === index);
|
||||
}
|
||||
|
||||
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,
|
||||
internalWorkerSessionId,
|
||||
activity: compactionActivity(projection, internalWorkerSessionId)
|
||||
};
|
||||
const line: ConsoleLine = {
|
||||
id: lineId,
|
||||
source: "event",
|
||||
kind: compaction.state === "failed" ? "error" : "status",
|
||||
title: "Compaction",
|
||||
body: compaction.summary ?? compaction.error ?? "",
|
||||
streaming: compaction.state === "running",
|
||||
error: compaction.state === "failed",
|
||||
compaction
|
||||
};
|
||||
const index = projection.lines.findIndex((candidate) => candidate.id === lineId);
|
||||
const lines = [...projection.lines];
|
||||
if (index >= 0) lines[index] = line;
|
||||
else lines.push(line);
|
||||
return { ...projection, lines };
|
||||
}
|
||||
|
||||
function refreshCompactionActivity(
|
||||
projection: ConsoleProjection,
|
||||
sessionId: string
|
||||
): ConsoleProjection {
|
||||
let changed = false;
|
||||
const lines = projection.lines.map((line) => {
|
||||
if (line.compaction?.internalWorkerSessionId !== sessionId) return line;
|
||||
changed = true;
|
||||
return {
|
||||
...line,
|
||||
compaction: {
|
||||
...line.compaction,
|
||||
candidate: compactionCandidate(projection, sessionId),
|
||||
activity: compactionActivity(projection, sessionId)
|
||||
}
|
||||
};
|
||||
});
|
||||
return changed ? { ...projection, lines } : projection;
|
||||
}
|
||||
|
||||
export function applyProtocolEvent(
|
||||
projection: ConsoleProjection,
|
||||
envelope: ConsoleEventInput,
|
||||
@@ -809,6 +924,31 @@ export function applyProtocolEvent(
|
||||
projectInternalWorkerSnapshot(worker, envelope.eventId, next.cwd)
|
||||
);
|
||||
next.removedInternalWorkers = {};
|
||||
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),
|
||||
};
|
||||
}
|
||||
if (
|
||||
compaction.state === "running" &&
|
||||
(!sessionId || !next.internalWorkers.some((worker) =>
|
||||
worker.worker.session_id === sessionId
|
||||
))
|
||||
) {
|
||||
line.streaming = false;
|
||||
line.compaction = {
|
||||
...line.compaction!,
|
||||
state: "interrupted",
|
||||
endedAtMs: envelope.observedAtMs ?? Date.now(),
|
||||
};
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "internal_worker": {
|
||||
@@ -841,7 +981,7 @@ export function applyProtocolEvent(
|
||||
};
|
||||
if (existingIndex >= 0) next.internalWorkers[existingIndex] = updated;
|
||||
else next.internalWorkers.push(updated);
|
||||
break;
|
||||
return refreshCompactionActivity(next, event.data.worker.session_id);
|
||||
}
|
||||
case "internal_worker_removed": {
|
||||
const existingIndex = next.internalWorkers.findIndex((worker) =>
|
||||
@@ -905,21 +1045,9 @@ export function applyProtocolEvent(
|
||||
// them to the conversation surface; browser Console should not either.
|
||||
break;
|
||||
case "compact_start":
|
||||
upsertStatusLine(next, "compact", envelope.eventId, "Compacting…", true);
|
||||
break;
|
||||
case "compact_done":
|
||||
upsertStatusLine(next, "compact", envelope.eventId, "Compacted.", false);
|
||||
break;
|
||||
case "compact_failed":
|
||||
upsertStatusLine(
|
||||
next,
|
||||
"compact",
|
||||
envelope.eventId,
|
||||
`Compact failed: ${event.data.error}`,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
break;
|
||||
return applyCompactionLifecycle(next, event.data.lifecycle);
|
||||
case "shutdown":
|
||||
next.status = "shutdown";
|
||||
break;
|
||||
@@ -1880,7 +2008,22 @@ function applyExtensionEntry(
|
||||
return;
|
||||
}
|
||||
const payload = entry["payload"];
|
||||
if (!isRecord(payload) || payload["kind"] !== "compaction_block") {
|
||||
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";
|
||||
|
||||
@@ -926,8 +926,9 @@ Deno.test("Web Console switches main and direct SubWorker views from the Tasks r
|
||||
);
|
||||
assert(
|
||||
consoleModel.includes("consoleWorkerViews") &&
|
||||
consoleModel.includes("projection.internalWorkers.map") &&
|
||||
consoleModel.includes('worker.worker.kind === "sub_worker"') &&
|
||||
consoleModel.includes("children.map") &&
|
||||
consoleModel.includes("resolveConsoleWorkerView"),
|
||||
"Worker view selection should use direct Internal Worker session identities with main fallback",
|
||||
"Worker view selection should expose only direct SubWorker session identities with main fallback",
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user