docs: trim generated prose
This commit is contained in:
@@ -1,37 +1,5 @@
|
||||
/**
|
||||
* Crash-recovery repair for an interrupted session log.
|
||||
*
|
||||
* A persistence backend flushes only at `turn/end`, so a crash can leave a
|
||||
* durable log whose final turn never closed: real, fully-written events sit
|
||||
* after the last `turn/end` with no closing boundary. A single turn can be huge
|
||||
* in a long-horizon task (many steps, large tool output), so those events MUST
|
||||
* be preserved — truncating the turn would silently destroy real work. Instead,
|
||||
* on reload the backend CLOSES the orphaned turn by appending the minimal
|
||||
* synthetic boundary events:
|
||||
*
|
||||
* 1. an error `tool/result` for every `tool-call` in the interrupted turn that
|
||||
* never got its matching `tool/result` (so the rehydrated history is a
|
||||
* VALID provider transcript — see below),
|
||||
* 2. a `step/end` if a step was still open, then
|
||||
* 3. a `turn/end` carrying the merge-extensible `{ kind: 'interrupted' }` reason.
|
||||
*
|
||||
* The marker records that the turn was cut short by a crash, not completed by
|
||||
* the model. See the session-persistence RFC.
|
||||
*
|
||||
* Why the synthetic tool results matter: `deriveMessages()` renders the
|
||||
* `tool-call` blocks inside a durable `assistant/message` but only emits a
|
||||
* matching tool-result when a `tool/result` EVENT exists. A crash between the
|
||||
* assistant message and its tool results (the loop runs the tools AFTER logging
|
||||
* the assistant message, so a process killed mid-tool leaves the calls without
|
||||
* results) would otherwise reload a history with a dangling assistant tool-call
|
||||
* — which every provider rejects as an invalid transcript on the next request.
|
||||
* Synthesizing an error result per orphaned call keeps resume safe.
|
||||
*
|
||||
* This module computes those synthetic closers from an event list; backends
|
||||
* return them inline from `load` (so the reconstructed session is balanced and
|
||||
* immediately usable) and persist them during that mutating load before any
|
||||
* later append continues the log.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-session/repair
|
||||
*/
|
||||
|
||||
@@ -39,36 +7,19 @@ import type { CallId } from '@deepseek-ai/dsh-llm'
|
||||
import type { SessionEvent } from './types.ts'
|
||||
|
||||
/**
|
||||
* Scan `events` for an open turn/step at the tail and return the synthetic
|
||||
* boundary events that close them, with `seq` continuing the log and `time`
|
||||
* copied from the last real event (the closers stand in for the crash moment;
|
||||
* reusing the last timestamp keeps them deterministic and never invents a
|
||||
* "future" time). Returns an empty array when the log is already balanced
|
||||
* (ends on a `turn/end`, or is empty) — the common, non-crash case.
|
||||
* Scan `events` for an open turn/step at the tail and return the synthetic boundary events
|
||||
* that close them, with `seq` continuing the log and `time` copied from the last real event
|
||||
* (the closers stand in for the crash moment; reusing the last timestamp keeps them
|
||||
* deterministic and never invents a "future" time).
|
||||
*
|
||||
* The closers, in order: an error `tool/result` for each unmatched `tool-call`
|
||||
* in the interrupted turn, then a `step/end` if a step is open, then the
|
||||
* `turn/end {interrupted}`. The tool-results come first so a step that issued
|
||||
* tool calls is balanced (every call has a result) before its `step/end`.
|
||||
*
|
||||
* Only the LAST turn can be open: the invariants plugin guarantees a `turn/end`
|
||||
* before any later `turn/start`, so an interior open turn is impossible in a
|
||||
* valid committed log. Likewise at most one step is open within that turn.
|
||||
* @param events - the loaded durable log to scan (a valid committed prefix, possibly with a crash tail).
|
||||
* @returns the synthetic closer events to append after `events`, in order; empty when the log is already balanced.
|
||||
*/
|
||||
export function interruptedTurnClosers(events: readonly SessionEvent[]): SessionEvent[] {
|
||||
let openTurn: number | null = null
|
||||
let openStep: number | null = null
|
||||
// Track tool calls vs. their results WITHIN the currently-open turn only: a
|
||||
// call is "pending" until its matching tool/result arrives. Reset at every
|
||||
// turn boundary so a committed earlier turn (already balanced) never leaks a
|
||||
// phantom pending call into the interrupted-turn repair.
|
||||
// Track pending tool calls with their callSeq (the seq of the `tool/call`
|
||||
// event, captured for surface sourceEventSeqs provenance on the synthetic
|
||||
// result). CallSeq is set from `tool/call` events; the assistant/message
|
||||
// block scan may register a call first (it appears earlier in the log), and
|
||||
// the later `tool/call` event fills in the seq.
|
||||
// Track tool calls vs. their results WITHIN the currently-open turn only: a call is "pending"
|
||||
// until its matching tool/result arrives.
|
||||
const pendingCalls = new Map<CallId, { step: number; callSeq?: number }>()
|
||||
for (const event of events) {
|
||||
switch (event.type) {
|
||||
@@ -97,10 +48,8 @@ export function interruptedTurnClosers(events: readonly SessionEvent[]): Session
|
||||
}
|
||||
break
|
||||
case 'tool/call':
|
||||
// Capture the tool/call event seq for surface provenance on the
|
||||
// synthesized tool/result. The entry may already exist (registered by
|
||||
// the assistant/message above) or may be new (if the assistant/message
|
||||
// came from a prior step that was already closed).
|
||||
// Capture the tool/call event seq for surface provenance on the synthesized
|
||||
// tool/result.
|
||||
{
|
||||
const entry = pendingCalls.get(event.data.callId)
|
||||
if (entry) {
|
||||
@@ -129,10 +78,9 @@ export function interruptedTurnClosers(events: readonly SessionEvent[]): Session
|
||||
const time = last.time
|
||||
const closers: SessionEvent[] = []
|
||||
|
||||
// Synthesize an error tool/result for each tool-call left unanswered by the
|
||||
// crash, so deriveMessages() yields a valid provider transcript on resume (a
|
||||
// dangling assistant tool-call is rejected by every provider). Insertion
|
||||
// order follows the Map (insertion = log order of the assistant messages).
|
||||
// Synthesize an error tool/result for each tool-call left unanswered by the crash, so
|
||||
// deriveMessages() yields a valid provider transcript on resume (a dangling assistant
|
||||
// tool-call is rejected by every provider).
|
||||
for (const [callId, { step, callSeq }] of pendingCalls) {
|
||||
closers.push({
|
||||
type: 'tool/result',
|
||||
|
||||
Reference in New Issue
Block a user