Persist logical run yield and resume transitions
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user