/**
* Return deterministic synthetic events that close an open tail turn. Unmatched
* calls receive error results first, followed by an open `step/end` and an
* interrupted `turn/end`; sequences continue the log and timestamps reuse the
* last real event. A balanced or empty log returns no events.
*
* @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[] {
// ...(28-88 行:扫描日志,记下未闭合的回合、步与未应答的工具调用)
// Close calls before their step: providers reject dangling assistant calls,
// and Map insertion order preserves their transcript order.
for (const [callId, { step, callSeq }] of pendingCalls) {
const started = callSeq !== undefined
const message: ToolResultMessage = freezeMessage({
id: MessageId(`interrupted-tool-result-${callId}-${seq}`),
role: 'user',
source: { kind: 'tool', callId },
content: [{
type: 'tool-result',
toolCallId: callId,
isError: true,
content: [{
type: 'text',
text: started
? 'The tool call was interrupted after it was recorded, but no result was durably recorded. Its outcome is unknown. Decide whether to retry from the tool semantics: retry only if the operation is read-only or idempotent; if it may have side effects, first verify external state or ask the user. Do not retry blindly.'
: 'The tool call was interrupted before the Harness recorded it as started. Retry it if it is still needed.',
}],
}],
// ...(108-125 行:把合成结果作为 tool/result 事件追加)
// Close an open step next — a turn/end while a step is open is an invariant
// violation, so the step's boundary must be synthesized before the turn's.
if (openStep !== null) {
closers.push({ type: 'step/end', seq: seq++, time, data: { turn: openTurn, step: openStep } })
}
closers.push({ type: 'turn/end', seq: seq++, time, data: { turn: openTurn, reason: { kind: 'interrupted' } } })
return closers