fix: retain worker run errors in console history
This commit is contained in:
@@ -2081,6 +2081,13 @@ impl App {
|
||||
} if domain == "yoi.compaction" => {
|
||||
self.apply_compaction_extension(&payload);
|
||||
}
|
||||
session_store::LogEntry::RunErrored { message, .. } => {
|
||||
self.blocks.push(Block::Alert {
|
||||
level: AlertLevel::Error,
|
||||
source: AlertSource::Worker,
|
||||
message,
|
||||
});
|
||||
}
|
||||
// Non-history-bearing variants don't affect the block view.
|
||||
_ => {}
|
||||
}
|
||||
@@ -3261,6 +3268,85 @@ mod completion_flow_tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_and_segment_rotation_retain_one_durable_run_error_block() {
|
||||
let mut app = App::new("test".into());
|
||||
app.handle_worker_event(Event::Error {
|
||||
code: ErrorCode::ProviderError,
|
||||
message: "provider unavailable".into(),
|
||||
});
|
||||
app.handle_worker_event(Event::Status {
|
||||
status: WorkerStatus::Idle,
|
||||
});
|
||||
|
||||
let live_errors = app
|
||||
.blocks
|
||||
.iter()
|
||||
.filter_map(|block| match block {
|
||||
Block::Alert {
|
||||
level: AlertLevel::Error,
|
||||
source: AlertSource::Worker,
|
||||
message,
|
||||
} => Some(message.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(live_errors, ["[ProviderError] provider unavailable"]);
|
||||
|
||||
let run_errored = session_store::LogEntry::RunErrored {
|
||||
ts: 3,
|
||||
interrupted: false,
|
||||
message: "provider unavailable".into(),
|
||||
};
|
||||
app.handle_worker_event(Event::Snapshot {
|
||||
greeting: test_greeting(),
|
||||
entries: vec![serde_json::to_value(run_errored).unwrap()],
|
||||
status: WorkerStatus::Idle,
|
||||
in_flight: Default::default(),
|
||||
});
|
||||
|
||||
let errors = app
|
||||
.blocks
|
||||
.iter()
|
||||
.filter_map(|block| match block {
|
||||
Block::Alert {
|
||||
level: AlertLevel::Error,
|
||||
source: AlertSource::Worker,
|
||||
message,
|
||||
} => Some(message.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(errors, ["provider unavailable"]);
|
||||
|
||||
app.handle_worker_event(Event::Error {
|
||||
code: ErrorCode::ProviderError,
|
||||
message: "retry unavailable".into(),
|
||||
});
|
||||
let rotated_run_error = session_store::LogEntry::RunErrored {
|
||||
ts: 5,
|
||||
interrupted: false,
|
||||
message: "retry unavailable".into(),
|
||||
};
|
||||
app.handle_worker_event(Event::SegmentRotated {
|
||||
entry: serde_json::to_value(rotated_run_error).unwrap(),
|
||||
});
|
||||
|
||||
let rotated_errors = app
|
||||
.blocks
|
||||
.iter()
|
||||
.filter_map(|block| match block {
|
||||
Block::Alert {
|
||||
level: AlertLevel::Error,
|
||||
source: AlertSource::Worker,
|
||||
message,
|
||||
} => Some(message.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(rotated_errors, ["retry unavailable"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_in_flight_blocks_continue_with_live_deltas() {
|
||||
let mut app = App::new("test".into());
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Event } from "$lib/generated/protocol";
|
||||
import {
|
||||
type ConsoleLine,
|
||||
createConsoleProjector,
|
||||
isConsoleProjectionEvent,
|
||||
projectConsole,
|
||||
segmentsToText,
|
||||
selectConsoleTimelineLines,
|
||||
@@ -36,11 +37,11 @@ function consoleLine(id: string, kind: ConsoleLine["kind"]): ConsoleLine {
|
||||
};
|
||||
}
|
||||
|
||||
function snapshotEvent(cwd: string): Event {
|
||||
function snapshotEvent(cwd: string, entries: unknown[] = []): Event {
|
||||
return {
|
||||
event: "snapshot",
|
||||
data: {
|
||||
entries: [],
|
||||
entries,
|
||||
greeting: {
|
||||
worker_name: "Worker",
|
||||
cwd,
|
||||
@@ -57,6 +58,95 @@ function snapshotEvent(cwd: string): Event {
|
||||
};
|
||||
}
|
||||
|
||||
Deno.test("console routing projects live errors but not completion replies", () => {
|
||||
const errorEvent = {
|
||||
event: "error",
|
||||
data: { code: "provider_error", message: "provider unavailable" },
|
||||
} satisfies Event;
|
||||
const completionEvent = {
|
||||
event: "completions",
|
||||
data: { kind: "file", entries: [] },
|
||||
} satisfies Event;
|
||||
|
||||
assert(
|
||||
isConsoleProjectionEvent(errorEvent),
|
||||
"live errors must reach the timeline projector",
|
||||
);
|
||||
assert(
|
||||
!isConsoleProjectionEvent(completionEvent),
|
||||
"completion replies should remain control-only events",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("snapshot and segment rotation retain one durable run_errored row", () => {
|
||||
const projector = createConsoleProjector();
|
||||
let projection = projector.append([
|
||||
{
|
||||
eventId: "live-error",
|
||||
event: {
|
||||
event: "error",
|
||||
data: { code: "provider_error", message: "provider unavailable" },
|
||||
} satisfies Event,
|
||||
},
|
||||
{
|
||||
eventId: "idle-after-error",
|
||||
event: { event: "status", data: { status: "idle" } } satisfies Event,
|
||||
},
|
||||
]);
|
||||
|
||||
assertEquals(projection.status, "idle");
|
||||
const liveErrors = projection.lines.filter((line) => line.kind === "error");
|
||||
assertEquals(liveErrors.length, 1);
|
||||
assertEquals(liveErrors[0].title, "error · provider_error");
|
||||
assertEquals(liveErrors[0].body, "provider unavailable");
|
||||
|
||||
projection = projector.append([{
|
||||
eventId: "reconnected-snapshot",
|
||||
event: snapshotEvent("/repo", [{
|
||||
kind: "run_errored",
|
||||
ts: 3,
|
||||
interrupted: false,
|
||||
message: "provider unavailable",
|
||||
}]),
|
||||
}]);
|
||||
|
||||
const errors = projection.lines.filter((line) => line.kind === "error");
|
||||
assertEquals(errors.length, 1);
|
||||
assertEquals(errors[0].title, "Run error");
|
||||
assertEquals(errors[0].body, "provider unavailable");
|
||||
assertEquals(errors[0].error, true);
|
||||
|
||||
projection = projector.append([
|
||||
{
|
||||
eventId: "second-live-error",
|
||||
event: {
|
||||
event: "error",
|
||||
data: { code: "provider_error", message: "retry unavailable" },
|
||||
} satisfies Event,
|
||||
},
|
||||
{
|
||||
eventId: "segment-rotated",
|
||||
event: {
|
||||
event: "segment_rotated",
|
||||
data: {
|
||||
entry: {
|
||||
kind: "run_errored",
|
||||
ts: 5,
|
||||
interrupted: false,
|
||||
message: "retry unavailable",
|
||||
},
|
||||
},
|
||||
} satisfies Event,
|
||||
},
|
||||
]);
|
||||
|
||||
const rotatedErrors = projection.lines.filter((line) =>
|
||||
line.kind === "error"
|
||||
);
|
||||
assertEquals(rotatedErrors.length, 1);
|
||||
assertEquals(rotatedErrors[0].body, "retry unavailable");
|
||||
});
|
||||
|
||||
Deno.test("workerConsoleHref encodes runtime and worker target authority", () => {
|
||||
assert(
|
||||
workerConsoleHref({
|
||||
|
||||
@@ -142,6 +142,10 @@ export type ConsoleEventInput = {
|
||||
observedAtMs?: number;
|
||||
};
|
||||
|
||||
export function isConsoleProjectionEvent(event: ProtocolEvent): boolean {
|
||||
return event.event !== "completions";
|
||||
}
|
||||
|
||||
export function emptyConsoleProjection(): ConsoleProjection {
|
||||
return {
|
||||
lines: [],
|
||||
@@ -1216,6 +1220,19 @@ function applyLogEntry(
|
||||
case "tool_result":
|
||||
applyLoggedItem(projection, eventId, entry["item"]);
|
||||
break;
|
||||
case "run_errored":
|
||||
projection.lines.push(
|
||||
line(
|
||||
eventId,
|
||||
"error",
|
||||
"Run error",
|
||||
stringField(entry, "message") ?? "Worker run failed.",
|
||||
undefined,
|
||||
false,
|
||||
true,
|
||||
),
|
||||
);
|
||||
break;
|
||||
case "extension":
|
||||
applyExtensionEntry(projection, eventId, entry);
|
||||
break;
|
||||
|
||||
+2
-1
@@ -18,6 +18,7 @@
|
||||
import { fitTextarea } from "$lib/workspace/console/textarea-fit";
|
||||
import {
|
||||
createConsoleProjector,
|
||||
isConsoleProjectionEvent,
|
||||
selectConsoleTimelineLines,
|
||||
type ConsoleEventInput,
|
||||
type ConsoleLine,
|
||||
@@ -266,7 +267,6 @@
|
||||
|
||||
function handleIncomingProtocolEvent(payload: ProtocolEvent) {
|
||||
handleProtocolCommandEvent(payload);
|
||||
if (payload.event === "completions" || payload.event === "error") {
|
||||
if (payload.event === "error") {
|
||||
queueObservationDiagnostic({
|
||||
code: payload.data.code,
|
||||
@@ -274,6 +274,7 @@
|
||||
message: payload.data.message,
|
||||
});
|
||||
}
|
||||
if (!isConsoleProjectionEvent(payload)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user