Persist logical run yield and resume transitions

This commit is contained in:
2026-10-01 16:44:11 +09:00
parent f5d8c94713
commit 0ec2b3ad86
19 changed files with 1047 additions and 75 deletions
File diff suppressed because one or more lines are too long
+9 -3
View File
@@ -136,7 +136,13 @@ export type SessionToolAttachment = { media_type: string,
*/
data_base64: string, };
export type SessionSnapshotEntryData = { "kind": "user_input", segments: Array<Segment>, } | { "kind": "message", role: SessionMessageRole, content: Array<SessionContentPart>, } | { "kind": "tool_call", call_id: string, name: string, arguments: string, } | { "kind": "tool_result", call_id: string, summary: string, content?: string | null, is_error: boolean, attachments?: Array<SessionToolAttachment>, } | { "kind": "system_item", item_kind: string, content: string, data?: unknown, } | { "kind": "run_error", message: string, };
export type RunYieldReason = "compaction";
export type RunResumeSource = "compaction" | "pause";
export type RunFailureKind = "engine" | "compaction";
export type SessionSnapshotEntryData = { "kind": "user_input", segments: Array<Segment>, } | { "kind": "message", role: SessionMessageRole, content: Array<SessionContentPart>, } | { "kind": "tool_call", call_id: string, name: string, arguments: string, } | { "kind": "tool_result", call_id: string, summary: string, content?: string | null, is_error: boolean, attachments?: Array<SessionToolAttachment>, } | { "kind": "system_item", item_kind: string, content: string, data?: unknown, } | { "kind": "run_yielded", reason: RunYieldReason, active_run_turn_count: number, } | { "kind": "run_resumed", source: RunResumeSource, active_run_turn_count: number, } | { "kind": "run_error", message: string, failure?: RunFailureKind | null, };
export type SessionSnapshotEntry = {
/**
@@ -147,7 +153,7 @@ entry_id: string,
/**
* Timestamp copied from the durable log record that commits this entry.
*/
timestamp: number, provenance: SessionEntryProvenance, derived_from?: Array<string>, } & ({ "kind": "user_input", segments: Array<Segment>, } | { "kind": "message", role: SessionMessageRole, content: Array<SessionContentPart>, } | { "kind": "tool_call", call_id: string, name: string, arguments: string, } | { "kind": "tool_result", call_id: string, summary: string, content?: string | null, is_error: boolean, attachments?: Array<SessionToolAttachment>, } | { "kind": "system_item", item_kind: string, content: string, data?: unknown, } | { "kind": "run_error", message: string, });
timestamp: number, provenance: SessionEntryProvenance, derived_from?: Array<string>, } & ({ "kind": "user_input", segments: Array<Segment>, } | { "kind": "message", role: SessionMessageRole, content: Array<SessionContentPart>, } | { "kind": "tool_call", call_id: string, name: string, arguments: string, } | { "kind": "tool_result", call_id: string, summary: string, content?: string | null, is_error: boolean, attachments?: Array<SessionToolAttachment>, } | { "kind": "system_item", item_kind: string, content: string, data?: unknown, } | { "kind": "run_yielded", reason: RunYieldReason, active_run_turn_count: number, } | { "kind": "run_resumed", source: RunResumeSource, active_run_turn_count: number, } | { "kind": "run_error", message: string, failure?: RunFailureKind | null, });
export type SessionConversationTurn = { turn_id: string, entries: Array<SessionSnapshotEntry>, };
@@ -340,7 +346,7 @@ export type SubscriptionFrame = { protocol_version: number, } & ({ "frame": "req
export type Method = { "method": "submit", "params": { submission_request_id: string, input: Array<Segment>, } } | { "method": "notify", "params": { notification_request_id: string, message: string, } } | { "method": "worker_event", "params": WorkerEvent } | { "method": "list_pending_submissions" } | { "method": "cancel_pending_submission", "params": { submission_id: string, expected_revision: number, } } | { "method": "clear_pending_submissions", "params": { expected_revision: number, } } | { "method": "continue_pending", "params": { expected_revision: number, expected_head_id: string, } } | { "method": "resume", "params": { command: WorkerCommandEnvelope, } } | { "method": "cancel", "params": { command: WorkerCommandEnvelope, } } | { "method": "pause", "params": { command: WorkerCommandEnvelope, } } | { "method": "compact", "params": { command: WorkerCommandEnvelope, } } | { "method": "list_rewind_targets" } | { "method": "rewind_to", "params": { target: RewindTargetId, expected_head_entries: number, } } | { "method": "shutdown", "params": { command: WorkerCommandEnvelope, } } | { "method": "list_completions", "params": { kind: CompletionKind, prefix: string, } } | { "method": "list_workers" } | { "method": "restore_worker", "params": { name: string, } } | { "method": "register_peer", "params": { name: string, } };
export type Event = { "event": "submission_accepted", "data": { submission_request_id: string, submission_id: string, disposition: SubmissionDisposition, } } | { "event": "submission_rejected", "data": { submission_request_id: string, message: string, } } | { "event": "pending_submissions_changed", "data": { pending: PendingSubmissionsSnapshot, } } | { "event": "user_message", "data": {
export type Event = { "event": "submission_accepted", "data": { submission_request_id: string, submission_id: string, disposition: SubmissionDisposition, } } | { "event": "submission_rejected", "data": { submission_request_id: string, message: string, } } | { "event": "notification_accepted", "data": { notification_request_id: string, } } | { "event": "notification_rejected", "data": { notification_request_id: string, message: string, } } | { "event": "pending_submissions_changed", "data": { pending: PendingSubmissionsSnapshot, } } | { "event": "user_message", "data": {
/**
* Stable durable history identity. Older mixed-version producers omit it.
*/
@@ -132,12 +132,21 @@ function canonicalSession(logEntries: unknown[]): Event extends {
});
break;
}
case "run_yielded":
case "run_resumed":
entries.push({
entry_id: `legacy-test-${sequence++}`,
provenance: "legacy_unknown",
...entry,
});
break;
case "run_errored":
entries.push({
entry_id: `legacy-test-${sequence++}`,
provenance: "legacy_unknown",
kind: "run_error",
message: entry["message"] ?? "Worker run failed.",
failure: entry["failure"],
});
break;
}
@@ -291,6 +300,101 @@ Deno.test("Worker state events and acknowledgements replace the full state", ()
);
});
Deno.test("durable compaction failure reconciles live alert and error in both views", () => {
const message = "mid-run compaction failed: summary unavailable";
const transition = (
entryId: string,
data: Record<string, unknown>,
): Event => ({
event: "session_entry_committed",
data: {
entry: {
entry_id: entryId,
timestamp: 10,
provenance: "legacy_unknown",
...data,
},
},
} as Event);
const projection = projectConsole([
{
eventId: "yielded",
event: transition("yielded-entry", {
kind: "run_yielded",
reason: "compaction",
active_run_turn_count: 2,
}),
},
{
eventId: "alert",
event: {
event: "alert",
data: {
level: "error",
source: "compactor",
message,
timestamp_ms: 10,
},
},
},
{
eventId: "failed",
event: transition("failed-entry", {
kind: "run_error",
message,
failure: "compaction",
}),
},
{
eventId: "controller-error",
event: {
event: "error",
data: { code: "internal", message: "summary unavailable" },
},
},
]);
const errors = projection.lines.filter((line) => line.kind === "error");
assertEquals(errors.length, 1);
assertEquals(errors[0].title, "Compaction failed");
assertEquals(errors[0].body, message);
assertEquals(errors[0].entryId, "failed-entry");
assertEquals(projectConsoleLines(projection.lines, "normal"), errors);
assertEquals(projectConsoleLines(projection.lines, "overview"), errors);
});
Deno.test("snapshot restores compaction terminal without reviving transition progress", () => {
const projection = projectConsole([{
eventId: "snapshot",
event: snapshotEvent("/repo", [
{
kind: "run_yielded",
timestamp: 1,
reason: "compaction",
active_run_turn_count: 2,
},
{
kind: "run_resumed",
timestamp: 2,
source: "compaction",
active_run_turn_count: 2,
},
{
kind: "run_errored",
timestamp: 3,
interrupted: false,
message: "mid-run compaction failed: summary unavailable",
failure: "compaction",
},
]),
}]);
assertEquals(projection.compaction, null);
assertEquals(projection.lines.length, 1);
assertEquals(projection.lines[0].title, "Compaction failed");
assertEquals(projection.lines[0].kind, "error");
});
Deno.test("snapshot replaces a live error with one durable run_errored row", () => {
const projector = createConsoleProjector();
let projection = projector.append([
@@ -1002,6 +1002,7 @@ export function applyProtocolEvent(
next.usage = usageText(event.data);
break;
case "error":
if (hasDurableRunFailure(next, event.data.message)) break;
next.lines.push(
line(
envelope.eventId,
@@ -1264,12 +1265,55 @@ function upsertStatusLine(
}
}
function runFailureMessagesMatch(left: string, right: string): boolean {
return left.length > 0 && right.length > 0 &&
(left === right || left.endsWith(right) || right.endsWith(left));
}
function isDurableRunFailure(line: ConsoleLine): boolean {
return line.kind === "error" &&
(line.title === "Run error" || line.title === "Compaction failed");
}
function hasDurableRunFailure(
projection: ConsoleProjection,
message: string,
): boolean {
return projection.lines.some((line) =>
isDurableRunFailure(line) && runFailureMessagesMatch(line.body, message)
);
}
function appendDurableRunFailure(
projection: ConsoleProjection,
eventId: string,
message: string,
failure: string | undefined,
): void {
projection.lines = projection.lines.filter((line) => {
if (isDurableRunFailure(line)) return true;
return !(line.kind === "error" && runFailureMessagesMatch(line.body, message));
});
projection.lines.push(
line(
eventId,
"error",
failure === "compaction" ? "Compaction failed" : "Run error",
message,
undefined,
false,
true,
),
);
}
function appendAlertLine(
projection: ConsoleProjection,
eventId: string,
alert: Alert,
): void {
const isError = alert.level === "error";
if (isError && hasDurableRunFailure(projection, alert.message)) return;
projection.lines.push(
line(
eventId,
@@ -2144,17 +2188,17 @@ function applySessionEntry(
applyTaskSystemItem(projection, item);
break;
}
case "run_yielded":
case "run_resumed":
// Durable logical-Run transitions are replayable state, not Console
// progress rows. Runtime compaction progress remains the spinner authority.
break;
case "run_error":
projection.lines.push(
line(
eventId,
"error",
"Run error",
stringField(value, "message") ?? "Worker run failed.",
undefined,
false,
true,
),
appendDurableRunFailure(
projection,
eventId,
stringField(value, "message") ?? "Worker run failed.",
stringField(value, "failure"),
);
break;
default: