Skip to content

Commit 22b8855

Browse files
committed
Expire error recovery on reinject the same as interrupt
Reinject still stops the run before sending. Clear the one-event handoff there too, and keep reinject rows if a rollback still fires.
1 parent 6a1bb54 commit 22b8855

2 files changed

Lines changed: 45 additions & 6 deletions

File tree

src/tui/runtime-bridge.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,6 +1014,42 @@ describe("same-turn failover after inference.error", () => {
10141014
);
10151015
});
10161016

1017+
test("reinject interrupt keeps the prompt and the classified error", async () => {
1018+
await withTestRenderer(
1019+
async (h) => {
1020+
const shell = createAppShell(h.renderer, {
1021+
terminal: { columns: 80, rows: 24 },
1022+
wireKeys: false,
1023+
run: "idle",
1024+
});
1025+
const bridge = attachSessionBridge(shell, createRecordingPort());
1026+
try {
1027+
bridge.handle({ type: "inference.start", data: {} });
1028+
bridge.handle({
1029+
type: "inference.error",
1030+
data: {
1031+
error: { category: "credential_failure", message: "Forbidden", statusCode: 403 },
1032+
},
1033+
});
1034+
bridge.submit("restart from here", "reinject");
1035+
bridge.handle({ type: "inference.start", data: {} });
1036+
bridge.handle({ type: "inference.text.delta", data: { token: "recovered" } });
1037+
bridge.handle({ type: "inference.done", data: {} });
1038+
bridge.handle({ type: "reactor.done", data: {} });
1039+
1040+
const text = shell.streamLog.map((r) => r.text).join("\n");
1041+
expect(text).toContain("restart from here");
1042+
expect(text).toContain("stop — restarting from your message");
1043+
expect(errorRows(shell)).toContain("Session expired — re-authenticating…");
1044+
} finally {
1045+
bridge.dispose();
1046+
shell.dispose();
1047+
}
1048+
},
1049+
{ width: 80, height: 24 },
1050+
);
1051+
});
1052+
10171053
test("a terminal inference.error with no recovery still surfaces", async () => {
10181054
await withTestRenderer(
10191055
async (h) => {

src/tui/runtime-bridge.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -704,21 +704,22 @@ function syncToolElapsed(shell: AppShell, bag: BridgeBag, nowMs: number): void {
704704
}
705705
}
706706

707-
/**
708-
* Retract everything the failed attempt painted, then forget the row
709-
* bookkeeping that pointed into it — a rolled-back tool call has no row left
710-
* to resolve, and a rolled-back reasoning row is no longer there to fold into.
711-
*/
712707
function isLocallyQueuedUserRow(row: StreamRow): boolean {
713708
return (
714709
row.role === "user" &&
715710
(row.meta === "queue" ||
716711
row.meta === "steer" ||
717712
row.meta === "steering" ||
718-
row.meta === "following-up")
713+
row.meta === "following-up" ||
714+
row.meta === "reinject")
719715
);
720716
}
721717

718+
/**
719+
* Retract everything the failed attempt painted, then forget the row
720+
* bookkeeping that pointed into it — a rolled-back tool call has no row left
721+
* to resolve, and a rolled-back reasoning row is no longer there to fold into.
722+
*/
722723
function rollbackAttempt(shell: AppShell, bag: BridgeBag): void {
723724
const boundary = bag.attemptRow;
724725
bag.attemptRow = null;
@@ -1138,6 +1139,8 @@ export function attachSessionBridge(
11381139
if (shell.session.run !== "busy") return;
11391140
closeOpenRow(shell, bag);
11401141
bag.pendingEchoes.length = 0;
1142+
bag.mapCtx.errorRollbackArmed = false;
1143+
bag.attemptRow = null;
11411144
shell.session = interrupt(shell.session);
11421145
appendStreamRow(shell, {
11431146
role: "system",

0 commit comments

Comments
 (0)