fix(session): synthesize tool results for interrupted tool calls on crash recovery (review #33)
The loop logs the assistant/message (carrying tool-call blocks) BEFORE running the tools, so a crash mid-tool leaves durable tool calls with no matching tool/result. interruptedTurnClosers only added step/end + turn/end, so a resumed session's deriveMessages() replayed a dangling assistant tool-call — which every provider rejects as an invalid transcript on the next request. interruptedTurnClosers now scans the interrupted turn for tool-call blocks without a matching tool/result and synthesizes an error tool/result for each (before the step/end), so the rehydrated history is a valid transcript. Adds a dedicated repair.spec.ts and a shared-contract case proving both backends pair every orphaned call with a result. Docs (ADR 0018, both persistence READMEs, load() JSDoc) updated. Also fixes the echo-agent README session-cleanup path: demo:echo runs from the repo root, so sessions land in <repo-root>/.sessions/_no-cwd/, not examples/echo-agent/.sessions/ (review #33).
This commit is contained in:
@@ -90,14 +90,17 @@ export abstract class SessionPersistence extends Service {
|
||||
* `turn/end`. Those events are PRESERVED — a single turn can be huge in a
|
||||
* long-horizon task, so truncating it would destroy real work — and `load`
|
||||
* CLOSES the orphaned turn by durably appending the minimal synthetic boundary
|
||||
* events (a `step/end` if a step was open, then a `turn/end` carrying the
|
||||
* `{ kind: 'interrupted' }` reason). The returned `events` therefore end on a
|
||||
* balanced `turn/end` and are immediately usable as a session seed. Only a
|
||||
* never-fully-written TORN tail fragment (a half-written final record) is
|
||||
* discarded. Returned events are contiguous (`events[i].seq === i`); a parse
|
||||
* error or a `seq` gap in the COMMITTED region (at or before the last real
|
||||
* `turn/end`) makes the session unloadable (reject). Rejects an unknown format
|
||||
* `version`. See ADR 0018 for the crash-recovery contract.
|
||||
* events: an error `tool/result` for every `tool-call` the crash left
|
||||
* unanswered (so the rehydrated history is a valid provider transcript — a
|
||||
* dangling assistant tool-call is otherwise rejected), then a `step/end` if a
|
||||
* step was open, then a `turn/end` carrying the `{ kind: 'interrupted' }`
|
||||
* reason. The returned `events` therefore end on a balanced `turn/end` and are
|
||||
* immediately usable as a session seed. Only a never-fully-written TORN tail
|
||||
* fragment (a half-written final record) is discarded. Returned events are
|
||||
* contiguous (`events[i].seq === i`); a parse error or a `seq` gap in the
|
||||
* COMMITTED region (at or before the last real `turn/end`) makes the session
|
||||
* unloadable (reject). Rejects an unknown format `version`. See ADR 0018 for
|
||||
* the crash-recovery contract.
|
||||
*/
|
||||
abstract load(id: SessionId): Promise<{ meta: SessionMeta; events: SessionEvent[] }>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user