fix: recover session submissions and failed turn settlement

This commit is contained in:
amosblomqvist
2026-10-25 19:45:17 +02:00
parent b277ee3e0f
commit 7d65fa8754
21 changed files with 944 additions and 94 deletions
+28 -7
View File
@@ -79,6 +79,10 @@ export interface PiTurnSpec {
* event stream itself is verbatim — provider errors ride their
* message's stopReason/errorMessage through untouched). */
onError?: (error: unknown) => string;
/** Report settlement failure while the views are still open. */
onSettleError?: (error: unknown) => string;
/** Release liveness even if the normal finish seam throws. */
onSettleFailure?: (error: unknown, reportedErrorText?: string) => void;
/** The canonical end-of-turn seam: persist + usage + extraction + endTurn.
* Receives the agent's FULL final state (native messages) — the caller
* persists the slice past its own base count. */
@@ -119,11 +123,12 @@ export interface PiTurnHandle {
}
export function runPiTurn(spec: PiTurnSpec): PiTurnHandle {
const { agent, onError, onFinish, usedToolCallIds, updateFlushMs = 100, requestCycle } = spec;
const { agent, onError, onFinish, onSettleError, onSettleFailure, usedToolCallIds, updateFlushMs = 100, requestCycle } = spec;
const uniquifier = usedToolCallIds ? new ToolCallIdUniquifier(usedToolCallIds) : undefined;
let settled = false;
let sawAbort = false;
let reportedErrorText: string | undefined;
// THE PER-TURN STATE LOG (memory-only — the settle-only disk writes
// are unchanged). COMPACTION LAW (owner, 2026-10-09, supersedes the
@@ -221,11 +226,22 @@ export function runPiTurn(spec: PiTurnSpec): PiTurnHandle {
// messages of its own (the prompt is composed per turn), so dropping
// the role wholesale is exact; revisit if prompt-updates ever land.
const settleWith = () => {
const settled_ = [...agent.state.messages].filter((m) => (m as { role?: string }).role !== "system");
onFinish((uniquifier?.remapSettled(settled_) as AgentMessage[]) ?? settled_, sawAbort);
try {
const settled_ = [...agent.state.messages].filter((m) => (m as { role?: string }).role !== "system");
onFinish((uniquifier?.remapSettled(settled_) as AgentMessage[]) ?? settled_, sawAbort);
} catch (err) {
console.error("[turn] settlement failed:", err);
try { onSettleFailure?.(err, reportedErrorText); }
catch (cleanupError) { console.error("[turn] settlement cleanup failed:", cleanupError); }
if (reportedErrorText === undefined) {
reportedErrorText = onSettleError?.(err) ?? "An error occurred.";
send({ type: "error", errorText: reportedErrorText });
}
}
};
const handle = (event: AgentEvent) => {
if (settled) return;
// THE LACAN FIX: ids uniquify HERE, before any view — the one seam
// every consumer downstream shares. No uniquifier → verbatim.
const uniqued = uniquifier ? uniquifier.event(event) : event;
@@ -246,8 +262,9 @@ export function runPiTurn(spec: PiTurnSpec): PiTurnHandle {
if (m?.role === "custom") noteSteerVisible(event.message as SteerMessage);
}
if (event.type === "agent_end") {
closeAll();
// Persist/report before the terminal closes every view to new frames.
settleWith();
closeAll();
}
};
requestCycle?.bindRelease(handle);
@@ -273,11 +290,15 @@ export function runPiTurn(spec: PiTurnSpec): PiTurnHandle {
// stream as stopReason "error"): surface, close, persist. If the
// agent already settled (agent_end reached before the throw),
// the seam has run — never settle twice.
if (settled) return;
if (settled) {
console.error("[turn] error after settlement:", err);
return;
}
console.error("[turn] agent loop failed:", err);
send({ type: "error", errorText: onError?.(err) ?? "An error occurred." });
closeAll();
reportedErrorText = onError?.(err) ?? "An error occurred.";
send({ type: "error", errorText: reportedErrorText });
settleWith();
closeAll();
}
})();