fix: preserve command stream boundaries and lifecycle evidence

This commit is contained in:
2026-08-21 03:49:45 +09:00
parent a82234a75e
commit cfb173c570
9 changed files with 285 additions and 19 deletions
+2 -2
View File
@@ -28,9 +28,9 @@ export type CommandStream = "stdout" | "stderr";
export type CommandStreamSlice = { start_offset: number, end_offset: number, content: string, truncated: boolean, };
export type CommandSnapshot = { command_id: string, tool_call_id: string | null, status: CommandStatus, stdout: CommandStreamSlice, stderr: CommandStreamSlice, exit_code: number | null, };
export type CommandSnapshot = { command_id: string, tool_call_id: string | null, status: CommandStatus, started_at_ms: number, observed_at_ms: number, last_output_at_ms: number | null, stdout: CommandStreamSlice, stderr: CommandStreamSlice, exit_code: number | null, };
export type CommandEvent = { "kind": "started", command_id: string, tool_call_id: string | null, } | { "kind": "output", command_id: string, stream: CommandStream, start_offset: number, end_offset: number, content: string, } | { "kind": "terminal", command_id: string, status: CommandStatus, exit_code: number | 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 ScopeRule = {
/**
@@ -356,6 +356,7 @@ Deno.test("projectConsole streams distinct Bash stdout and stderr through termin
kind: "started",
command_id: "command-1",
tool_call_id: "bash-stream",
observed_at_ms: 1000,
},
},
} satisfies Event,
@@ -372,6 +373,7 @@ Deno.test("projectConsole streams distinct Bash stdout and stderr through termin
start_offset: 0,
end_offset: 6,
content: "ready\n",
observed_at_ms: 1100,
},
},
} satisfies Event,
@@ -388,6 +390,7 @@ Deno.test("projectConsole streams distinct Bash stdout and stderr through termin
start_offset: 0,
end_offset: 5,
content: "warn\n",
observed_at_ms: 1200,
},
},
} satisfies Event,
@@ -402,6 +405,9 @@ Deno.test("projectConsole streams distinct Bash stdout and stderr through termin
command_id: "command-1",
status: "failed",
exit_code: 7,
stdout_end_offset: 6,
stderr_end_offset: 5,
observed_at_ms: 1300,
},
},
} satisfies Event,
@@ -410,6 +416,7 @@ Deno.test("projectConsole streams distinct Bash stdout and stderr through termin
const [line] = projection.lines.filter((line) => line.kind === "tool");
assert(line.body.includes("Bash — failed (exit 7)"), line.body);
assert(line.body.includes("elapsed 300ms"), line.body);
assert(line.body.includes("stdout:\nready\n"), line.body);
assert(line.body.includes("stderr:\nwarn\n"), line.body);
assertEquals(line.streaming, false);
@@ -432,6 +439,9 @@ Deno.test("snapshot restores bounded in-flight Bash command output", () => {
command_id: "command-2",
tool_call_id: "bash-snapshot",
status: "running",
started_at_ms: 1000,
observed_at_ms: 1250,
last_output_at_ms: 1200,
stdout: {
start_offset: 1024,
end_offset: 1031,
@@ -446,6 +456,10 @@ Deno.test("snapshot restores bounded in-flight Bash command output", () => {
const projection = projectConsole([{ eventId: "snapshot-command", event: snapshot }]);
const [line] = projection.lines.filter((line) => line.kind === "tool");
assert(line.body.includes("Bash — running…"), line.body);
assert(
line.body.includes("elapsed 250ms · last output at +200ms"),
line.body,
);
assert(line.body.includes("[stdout tail; earlier output omitted]"), line.body);
assert(line.body.includes("stdout:\ntail\n"), line.body);
assertEquals(line.streaming, true);
@@ -280,6 +280,9 @@ function applyCommandEvent(
command_id: event.command_id,
tool_call_id: event.tool_call_id,
status: "running",
started_at_ms: event.observed_at_ms,
observed_at_ms: event.observed_at_ms,
last_output_at_ms: null,
stdout: emptyCommandStream(),
stderr: emptyCommandStream(),
exit_code: null,
@@ -297,6 +300,9 @@ function applyCommandEvent(
command_id: event.command_id,
tool_call_id: null,
status: "running",
started_at_ms: event.observed_at_ms,
observed_at_ms: event.observed_at_ms,
last_output_at_ms: event.observed_at_ms,
stdout: event.stream === "stdout" ? stream : emptyCommandStream(),
stderr: event.stream === "stderr" ? stream : emptyCommandStream(),
exit_code: null,
@@ -311,6 +317,7 @@ function applyCommandEvent(
...existing,
status: event.status,
exit_code: event.exit_code,
observed_at_ms: event.observed_at_ms,
});
return;
}
@@ -322,6 +329,8 @@ function applyCommandEvent(
);
upsertCommandSnapshot(projection, eventId, {
...existing,
observed_at_ms: event.observed_at_ms,
last_output_at_ms: event.observed_at_ms,
stdout: event.stream === "stdout" ? updatedStream : existing.stdout,
stderr: event.stream === "stderr" ? updatedStream : existing.stderr,
});
@@ -1183,6 +1192,7 @@ function renderBashTool(toolCall: ToolCallView): string {
return compactLines([
`Bash — ${commandStateSuffix(toolCall)}`,
command ? `$ ${command}` : argsText(toolCall),
commandTiming(toolCall.command),
["done", "error"].includes(toolCall.state)
? cappedDisplaySection(resultText(toolCall), 10)
: renderLiveCommandOutput(toolCall.command),
@@ -1205,6 +1215,25 @@ function commandStateSuffix(toolCall: ToolCallView): string {
return "running…";
}
function commandTiming(command?: CommandSnapshot): string | undefined {
if (!command) return undefined;
const elapsed = Math.max(0, command.observed_at_ms - command.started_at_ms);
if (command.status !== "running") return `elapsed ${durationLabel(elapsed)}`;
if (command.last_output_at_ms === null) {
return `elapsed ${durationLabel(elapsed)} · awaiting first output`;
}
const lastOutputElapsed = Math.max(
0,
command.last_output_at_ms - command.started_at_ms,
);
return `elapsed ${durationLabel(elapsed)} · last output at +${durationLabel(lastOutputElapsed)}`;
}
function durationLabel(milliseconds: number): string {
if (milliseconds < 1000) return `${milliseconds}ms`;
return `${(milliseconds / 1000).toFixed(milliseconds < 10_000 ? 1 : 0)}s`;
}
function renderLiveCommandOutput(command?: CommandSnapshot): string | undefined {
if (!command) return undefined;
return compactLines([