@tangle-network/agent-app 0.43.45 → 0.43.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/stream/stream-normalizer.ts","../src/stream/turn-identity.ts","../src/stream/turn-buffer.ts"],"sourcesContent":["import {\n canTransitionInteractionStatus,\n persistedPartToInteraction,\n type ChatInteractionStatus,\n} from '../interactions/contract'\nimport {\n canTransitionPlanStatus,\n persistedPartToPlan,\n planPartKey,\n planToPersistedPart,\n type ChatPlanStatus,\n} from '../plans/index'\n\nexport type JsonRecord = Record<string, unknown>\n\nexport interface StreamEvent {\n type: string\n data?: JsonRecord\n}\n\nexport function asRecord(value: unknown): JsonRecord | undefined {\n return value && typeof value === 'object' && !Array.isArray(value)\n ? value as JsonRecord\n : undefined\n}\n\nexport function asString(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined\n}\n\nexport function resolveToolId(part: JsonRecord): string {\n return String(\n part.id ??\n part.callID ??\n part.callId ??\n part.toolUseId ??\n part.toolCallId ??\n part.tool ??\n part.name ??\n `tool-${Date.now()}`,\n )\n}\n\nexport function resolveToolName(part: JsonRecord): string {\n return String(part.tool ?? part.name ?? 'tool')\n}\n\nexport function normalizeTime(value: unknown): JsonRecord | undefined {\n const record = asRecord(value)\n if (!record) return undefined\n\n const start = Number(record.start ?? record.startedAt ?? record.started_at)\n const end = Number(record.end ?? record.completedAt ?? record.completed_at)\n if (!Number.isFinite(start) && !Number.isFinite(end)) return undefined\n\n return {\n start: Number.isFinite(start) ? start : undefined,\n end: Number.isFinite(end) ? end : undefined,\n }\n}\n\nexport function normalizeToolEvent(event: StreamEvent): StreamEvent {\n if (event.type === 'tool_call' || event.type === 'tool.call') {\n const data = event.data ?? {}\n return {\n type: 'message.part.updated',\n data: {\n part: {\n type: 'tool',\n id: data.id ?? data.callId ?? data.callID ?? data.name,\n tool: data.name ?? data.tool ?? 'tool',\n input: data.arguments ?? data.input,\n status: 'running',\n },\n },\n }\n }\n\n if (event.type === 'tool_result' || event.type === 'tool.result') {\n const data = event.data ?? {}\n const error = asString(data.error)\n return {\n type: 'message.part.updated',\n data: {\n part: {\n type: 'tool',\n id: data.id ?? data.callId ?? data.callID ?? data.name,\n tool: data.name ?? data.tool ?? 'tool',\n output: data.output,\n error,\n status: error ? 'error' : 'completed',\n },\n },\n }\n }\n\n return event\n}\n\nexport function normalizePersistedPart(rawPart: JsonRecord): JsonRecord | null {\n const type = String(rawPart.type ?? '')\n\n if (type === 'text') {\n const id = asString(rawPart.id) ?? asString(rawPart.partId)\n return {\n type: 'text',\n text: asString(rawPart.text) ?? asString(rawPart.content) ?? '',\n // id: per-segment identity from the harness; absent on legacy parts,\n // which collapse to a single keyed segment. Never invented here.\n ...(id ? { id } : {}),\n }\n }\n\n if (type === 'reasoning') {\n const id = asString(rawPart.id) ?? asString(rawPart.partId)\n return {\n type: 'reasoning',\n text: asString(rawPart.text) ?? asString(rawPart.content) ?? '',\n time: normalizeTime(rawPart.time),\n ...(id ? { id } : {}),\n }\n }\n\n if (type === 'file' || type === 'image') {\n const id = asString(rawPart.id) ?? asString(rawPart.partId)\n return {\n type,\n ...(id ? { id } : {}),\n ...(asString(rawPart.filename) ? { filename: asString(rawPart.filename) } : {}),\n ...(asString(rawPart.mediaType) ? { mediaType: asString(rawPart.mediaType) } : {}),\n ...(asString(rawPart.url) ? { url: asString(rawPart.url) } : {}),\n ...(asString(rawPart.path) ? { path: asString(rawPart.path) } : {}),\n ...(type === 'file' && asString(rawPart.content) ? { content: asString(rawPart.content) } : {}),\n }\n }\n\n if (type === 'step-start') {\n return { type: 'step-start' }\n }\n\n // The harness's per-step usage receipt. Dropping it here silently loses the\n // turn's token/cost accounting from the persisted transcript.\n if (type === 'step-finish') {\n const tokens = asRecord(rawPart.tokens)\n const cost = Number(rawPart.cost)\n return {\n type: 'step-finish',\n ...(asString(rawPart.reason) ? { reason: asString(rawPart.reason) } : {}),\n ...(tokens ? { tokens } : {}),\n ...(Number.isFinite(cost) ? { cost } : {}),\n }\n }\n\n if (type === 'subtask') {\n const id = asString(rawPart.id) ?? asString(rawPart.partId)\n return {\n type: 'subtask',\n prompt: asString(rawPart.prompt) ?? '',\n description: asString(rawPart.description) ?? '',\n agent: asString(rawPart.agent) ?? '',\n ...(id ? { id } : {}),\n }\n }\n\n if (type === 'interaction') {\n return persistedPartToInteraction(rawPart) ? rawPart : null\n }\n\n if (type === 'plan') {\n const plan = persistedPartToPlan(rawPart)\n return plan ? { ...rawPart, ...planToPersistedPart(plan) } : null\n }\n\n // System-authored notices pass through verbatim; `/chat-store` owns their\n // final typed validation before persistence.\n if (type === 'notice') {\n return rawPart\n }\n\n if (type === 'tool') {\n const state = asRecord(rawPart.state)\n const output = state?.output ?? rawPart.output\n const error = asString(state?.error ?? rawPart.error)\n const terminalError =\n state?.status === 'error' ||\n state?.status === 'failed' ||\n rawPart.status === 'error' ||\n rawPart.status === 'failed' ||\n Boolean(error)\n const status =\n state?.status === 'completed' || rawPart.status === 'completed'\n ? 'completed'\n : terminalError\n ? 'error'\n : output !== undefined\n ? 'completed'\n : 'running'\n\n return {\n type: 'tool',\n id: resolveToolId(rawPart),\n tool: resolveToolName(rawPart),\n callID:\n rawPart.callID != null || rawPart.callId != null\n ? String(rawPart.callID ?? rawPart.callId)\n : undefined,\n state: {\n status,\n input: state?.input ?? rawPart.input,\n output,\n error,\n metadata: asRecord(state?.metadata) ?? asRecord(rawPart.metadata),\n time: normalizeTime(state?.time ?? rawPart.time),\n },\n }\n }\n\n return null\n}\n\n/** Stream/transcript part key for a promoted (path-bearing) attachment,\n * keyed on its storage path — re-emitting the same path folds into the same\n * segment instead of duplicating it. */\nexport function attachmentPartKey(path: string): string {\n return `attachment:${path}`\n}\n\nexport function getPartKey(part: JsonRecord): string {\n const type = String(part.type ?? 'unknown')\n if (type === 'tool') {\n return `tool:${resolveToolId(part)}`\n }\n if (type === 'plan') return planPartKey(String(part.planId ?? ''))\n if ((type === 'file' || type === 'image') && asString(part.path)) {\n return attachmentPartKey(String(part.path))\n }\n\n // Keyed by the part's OWN type so distinct kinds never merge into each\n // other. Untyped parts fall back to the text lane (legacy shape).\n const lane = type && type !== 'unknown' ? type : 'text'\n return `${lane}:${String(part.id ?? part.partId ?? part.index ?? 'current')}`\n}\n\n/** Shallow overlay that skips `undefined` incoming values, so a later partial\n * update never erases a field an earlier one captured. */\nfunction overlayDefined(base: JsonRecord, patch: JsonRecord): JsonRecord {\n const out: JsonRecord = { ...base }\n for (const [key, value] of Object.entries(patch)) {\n if (value !== undefined) out[key] = value\n }\n return out\n}\n\nexport function mergePersistedPart(existing: JsonRecord | undefined, incoming: JsonRecord, delta?: string): JsonRecord {\n const type = String(incoming.type ?? '')\n if (!existing) {\n if (type === 'text' && delta) {\n return { type: 'text', text: delta }\n }\n return incoming\n }\n\n if (type === 'text' && String(existing.type ?? '') === 'text') {\n const existingText = String(existing.text ?? '')\n const incomingText = String(incoming.text ?? '')\n return {\n ...existing,\n ...incoming,\n // An empty snapshot never erases accumulated text (matches reasoning).\n text: delta ? `${existingText}${delta}` : incomingText || existingText,\n }\n }\n\n if (type === 'reasoning' && String(existing.type ?? '') === 'reasoning') {\n const existingText = String(existing.text ?? '')\n const incomingText = String(incoming.text ?? '')\n return {\n ...existing,\n ...incoming,\n text: delta && incomingText === existingText ? `${existingText}${delta}` : incomingText || existingText,\n time: incoming.time ?? existing.time,\n }\n }\n\n if (type === 'tool' && String(existing.type ?? '') === 'tool') {\n const existingState = asRecord(existing.state) ?? {}\n const incomingState = asRecord(incoming.state) ?? {}\n // Overlay only DEFINED incoming fields: a normalized tool part always\n // carries `output`/`error` keys (undefined when not captured), so a plain\n // spread would clobber a completed tool's output with a later empty update.\n const mergedState = overlayDefined(existingState, incomingState)\n // A partial update with no captured status/output/error normalizes to\n // `running`; never let it downgrade a tool that already settled.\n const existingStatus = String(existingState.status ?? '')\n if (\n (existingStatus === 'completed' || existingStatus === 'error') &&\n String(incomingState.status ?? '') === 'running'\n ) {\n mergedState.status = existingStatus\n }\n return {\n ...overlayDefined(existing, incoming),\n state: mergedState,\n }\n }\n\n if (type === 'interaction' && String(existing.type ?? '') === 'interaction') {\n const merged = overlayDefined(existing, incoming)\n const existingStatus = existing.status as ChatInteractionStatus | undefined\n const incomingStatus = incoming.status as ChatInteractionStatus | undefined\n if (\n existingStatus &&\n incomingStatus &&\n existingStatus !== incomingStatus &&\n !canTransitionInteractionStatus(existingStatus, incomingStatus)\n ) {\n merged.status = existingStatus\n }\n if (incoming.answers === undefined && existing.answers !== undefined) {\n merged.answers = existing.answers\n }\n return merged\n }\n\n if (type === 'plan' && String(existing.type ?? '') === 'plan') {\n const existingRevision = Number(existing.revision)\n const incomingRevision = Number(incoming.revision)\n if (Number.isInteger(existingRevision) && Number.isInteger(incomingRevision)) {\n if (incomingRevision < existingRevision) return existing\n if (incomingRevision > existingRevision) return incoming\n }\n const merged = overlayDefined(existing, incoming)\n const existingStatus = existing.status as ChatPlanStatus | undefined\n const incomingStatus = incoming.status as ChatPlanStatus | undefined\n if (\n existingStatus &&\n incomingStatus &&\n existingStatus !== incomingStatus &&\n !canTransitionPlanStatus(existingStatus, incomingStatus)\n ) {\n merged.status = existingStatus\n }\n return merged\n }\n\n return incoming\n}\n\nexport const MISSING_TOOL_TERMINAL_ERROR = 'Tool did not report a terminal result before the assistant turn completed.'\nexport const MISSING_TOOL_TERMINAL_REASON = 'missing-tool-terminal'\n\n/** Closes a tool part left `running` when a stream ended abnormally: settles\n * it as a terminal `error` and stamps `state.metadata.terminalized` so the\n * synthetic settlement is distinguishable from a real tool failure. Parts\n * that already settled (and non-tool parts) pass through untouched. */\nexport function terminalizeDanglingToolPart(part: JsonRecord): JsonRecord {\n if (String(part.type ?? '') !== 'tool') return part\n\n const state = asRecord(part.state) ?? {}\n if (String(state.status ?? part.status ?? '') !== 'running') return part\n\n const metadata = asRecord(state.metadata) ?? {}\n return {\n ...part,\n state: {\n ...state,\n status: 'error',\n error: asString(state.error ?? part.error) ?? MISSING_TOOL_TERMINAL_ERROR,\n metadata: {\n ...metadata,\n terminalized: true,\n terminalReason: MISSING_TOOL_TERMINAL_REASON,\n },\n },\n }\n}\n\nexport function terminalizeDanglingToolParts(parts: JsonRecord[]): JsonRecord[] {\n return parts.map(terminalizeDanglingToolPart)\n}\n\n/** Settles still-pending interaction parts at persist time. The broker\n * guarantees a resolved question either answered (run unblocked, no cancel\n * event) or cancelled/timed out (cancel event already updated the part), so\n * the success path finalizes remaining pendings as `answered` and the\n * failure/terminalize paths as `expired`. */\nexport function finalizePendingInteractionParts(\n parts: JsonRecord[],\n outcome: Extract<ChatInteractionStatus, 'answered' | 'expired'>,\n): JsonRecord[] {\n return parts.map((part) => {\n if (String(part.type ?? '') !== 'interaction') return part\n if (String(part.status ?? '') !== 'pending') return part\n return { ...part, status: outcome }\n })\n}\n\n/** Collapses text-part artifacts of unstable upstream segment identity: the\n * same text arriving under two keys (id-less delta stream, then an\n * id-bearing snapshot) folds into two segments, and interleaved empty\n * segments survive as blank parts. Consecutive identical text parts merge\n * into one; empty text parts drop when any non-empty text part exists. */\nexport function collapseRedundantTextParts(parts: JsonRecord[]): JsonRecord[] {\n const hasNonEmptyText = parts.some(\n (part) => String(part.type ?? '') === 'text' && String(part.text ?? '').trim().length > 0,\n )\n const collapsed: JsonRecord[] = []\n for (const part of parts) {\n if (String(part.type ?? '') !== 'text') {\n collapsed.push(part)\n continue\n }\n const text = String(part.text ?? '')\n if (hasNonEmptyText && text.trim().length === 0) continue\n const previous = collapsed[collapsed.length - 1]\n if (previous && String(previous.type ?? '') === 'text' && String(previous.text ?? '') === text) continue\n collapsed.push(part)\n }\n return collapsed\n}\n\nfunction assembleAssistantParts(\n partOrder: string[],\n partMap: Map<string, JsonRecord>,\n finalText: string,\n): JsonRecord[] {\n const parts = partOrder\n .map((key) => partMap.get(key))\n .filter((part): part is JsonRecord => Boolean(part))\n\n const textParts = parts.filter((part) => String(part.type ?? '') === 'text')\n\n if (textParts.length === 0) {\n if (finalText.trim()) {\n parts.push({ type: 'text', text: finalText })\n }\n return parts\n }\n\n // Id-less text parts form a single logical stream — the final text is\n // authoritative for it.\n if (!textParts.some((part) => asString(part.id))) {\n return parts.map((part) => {\n if (String(part.type ?? '') !== 'text') return part\n return {\n ...part,\n text: finalText || String(part.text ?? ''),\n }\n })\n }\n\n // Per-id text segments: invariant is concat(text parts) === persisted final\n // text, so segment boundaries survive without duplicating the answer into\n // every segment.\n const joined = textParts.map((part) => String(part.text ?? '')).join('')\n if (finalText === joined || finalText.trimEnd() === joined.trimEnd()) {\n return parts\n }\n\n if (finalText.startsWith(joined)) {\n // Final text extends the streamed segments (e.g. a failure diagnostic\n // appended after the stream) — persist the remainder as a trailing\n // id-less segment.\n return [...parts, { type: 'text', text: finalText.slice(joined.length) }]\n }\n\n // Final text replaced the streamed text outright. Keep non-text chronology;\n // collapse text to one authoritative segment at the last text position.\n const lastTextPart = textParts[textParts.length - 1]\n return parts\n .filter((part) => String(part.type ?? '') !== 'text' || part === lastTextPart)\n .map((part) => (part === lastTextPart ? { ...part, text: finalText } : part))\n}\n\nexport function finalizeAssistantParts(\n partOrder: string[],\n partMap: Map<string, JsonRecord>,\n finalText: string,\n): JsonRecord[] {\n // A stream that ended abnormally can leave tool parts `running` — never\n // persist one; collapsing then removes the duplicate/blank text segments an\n // unstable upstream segment identity produced.\n return collapseRedundantTextParts(terminalizeDanglingToolParts(\n assembleAssistantParts(partOrder, partMap, finalText),\n ))\n}\n\nfunction partStatus(part: JsonRecord | undefined): string {\n const state = asRecord(part?.state)\n return String(state?.status ?? part?.status ?? '')\n}\n\n/** Finalizes, then folds each synthetic tool settlement back into `partMap`\n * and returns just those updates — the shape a streaming loop needs to emit\n * closing `message.part.updated` frames for tools the stream never settled. */\nexport function terminalizeDanglingAssistantToolUpdates(\n partOrder: string[],\n partMap: Map<string, JsonRecord>,\n finalText: string,\n): JsonRecord[] {\n const finalizedParts = finalizeAssistantParts(partOrder, partMap, finalText)\n const updates: JsonRecord[] = []\n\n for (const part of finalizedParts) {\n if (String(part.type ?? '') !== 'tool') continue\n\n const key = getPartKey(part)\n const existing = partMap.get(key)\n if (partStatus(existing) !== 'running' || partStatus(part) === 'running') continue\n\n partMap.set(key, mergePersistedPart(existing, part))\n updates.push(part)\n }\n\n return updates\n}\n\nexport function encodeEvent(encoder: TextEncoder, event: StreamEvent): Uint8Array {\n return encoder.encode(`${JSON.stringify(event)}\\n`)\n}\n","import type { JsonRecord } from './stream-normalizer'\n\nexport interface PersistedChatMessageForTurn {\n id: string\n role: 'user' | 'assistant' | 'system' | 'tool'\n content: string\n parts: Array<Record<string, unknown>> | null\n}\n\nexport interface ResolvedChatTurn {\n turnIndex: number\n shouldInsertUserMessage: boolean\n priorMessages: PersistedChatMessageForTurn[]\n userParts: JsonRecord[]\n}\n\nexport function normalizeClientTurnId(value: unknown): string | undefined {\n if (value === undefined || value === null) return undefined\n if (typeof value !== 'string') throw new Error('turnId must be a string')\n const trimmed = value.trim()\n if (!trimmed) throw new Error('turnId must not be blank')\n if (trimmed.length > 160) throw new Error('turnId is too long')\n if (!/^[A-Za-z0-9:_-]+$/.test(trimmed)) {\n throw new Error('turnId contains unsupported characters')\n }\n return trimmed\n}\n\nexport function buildUserTextParts(text: string, turnId: string | undefined): JsonRecord[] {\n const part: JsonRecord = { type: 'text', text }\n if (turnId) part.turnId = turnId\n return [part]\n}\n\nexport function messageHasTurnId(message: PersistedChatMessageForTurn, turnId: string): boolean {\n for (const part of message.parts ?? []) {\n if (part && typeof part === 'object' && String(part.turnId ?? '') === turnId) {\n return true\n }\n }\n return false\n}\n\nexport function resolveChatTurn(input: {\n existingMessages: PersistedChatMessageForTurn[]\n userContent: string\n turnId?: string\n}): ResolvedChatTurn {\n const { existingMessages, userContent, turnId } = input\n const reusableIndex = findReusableUserMessageIndex(existingMessages, userContent, turnId)\n if (reusableIndex >= 0) {\n return {\n turnIndex: countUserMessages(existingMessages.slice(0, reusableIndex)),\n shouldInsertUserMessage: false,\n priorMessages: existingMessages.slice(0, reusableIndex),\n userParts: buildUserTextParts(userContent, turnId),\n }\n }\n\n return {\n turnIndex: countUserMessages(existingMessages),\n shouldInsertUserMessage: true,\n priorMessages: existingMessages,\n userParts: buildUserTextParts(userContent, turnId),\n }\n}\n\nfunction findReusableUserMessageIndex(\n messages: PersistedChatMessageForTurn[],\n userContent: string,\n turnId: string | undefined,\n): number {\n if (turnId) {\n for (let index = messages.length - 1; index >= 0; index -= 1) {\n const message = messages[index]\n if (message?.role === 'user' && messageHasTurnId(message, turnId)) return index\n }\n }\n\n const latest = messages.at(-1)\n if (latest?.role === 'user' && latest.content === userContent) {\n return messages.length - 1\n }\n\n return -1\n}\n\nfunction countUserMessages(messages: PersistedChatMessageForTurn[]): number {\n return messages.filter((message) => message.role === 'user').length\n}\n","/**\n * Resumable chat turns — the router-path answer to \"streams resume on\n * disconnect\" (issue #27). A turn's loop events are teed into a store as they\n * stream; the turn keeps running under `ctx.waitUntil` when the client drops;\n * a reconnecting client replays the buffered tail by sequence number and\n * keeps following until the turn completes.\n *\n * POST /chat/stream → pumpBufferedTurn(...) + live NDJSON\n * GET /chat/stream/:turnId → replayTurnEvents({ fromSeq }) → NDJSON\n *\n * Storage is a structural seam ({@link TurnEventStore}); a D1 implementation\n * ships here because that's what Cloudflare products have (KV is unsuitable:\n * eventually consistent cross-isolate). Per-token deltas would mean hundreds\n * of rows per turn, so consecutive text/reasoning deltas are coalesced within\n * a flush window before they are persisted — replay yields slightly chunkier\n * deltas with identical concatenation.\n */\n\nexport type TurnStatus = 'running' | 'complete' | 'error'\n\nexport interface BufferedTurnEvent {\n seq: number\n /** The serialized event line (JSON string, no trailing newline). */\n event: string\n}\n\nexport interface TurnEventStore {\n append(turnId: string, events: BufferedTurnEvent[]): Promise<void>\n read(turnId: string, fromSeq: number): Promise<BufferedTurnEvent[]>\n /** Record turn lifecycle. `scopeId` (a thread/session id) is optional and lets\n * {@link TurnEventStore.listRunning} rediscover this turn after a client reload\n * loses the turnId; stores that don't track scope ignore it. */\n setStatus(turnId: string, status: TurnStatus, scopeId?: string): Promise<void>\n getStatus(turnId: string): Promise<TurnStatus | null>\n /** Running turnIds for a scope, newest first — so a reloaded client (clientRunId\n * lost) can find and resume the in-flight turn. Optional: a store records it\n * only if `setStatus` was given a `scopeId`. */\n listRunning?(scopeId: string): Promise<string[]>\n}\n\n// ── coalescing ────────────────────────────────────────────────────────────\n\ntype AnyRecord = Record<string, unknown>\n\nfunction deltaTypeOf(ev: unknown): 'text' | 'reasoning' | null {\n const e = ev as AnyRecord | null\n if (!e || typeof e !== 'object') return null\n const inner = (e.kind === 'event' ? (e.event as AnyRecord | undefined) : e) as AnyRecord | undefined\n if (!inner || typeof inner !== 'object') return null\n if ((inner.type === 'text' || inner.type === 'reasoning') && typeof inner.text === 'string') {\n return inner.type\n }\n return null\n}\n\n/** Merge consecutive text/reasoning deltas of the same type into one event.\n * Concatenation-preserving: replaying the coalesced stream produces the same\n * accumulated text as the original. */\nexport function coalesceDeltas(events: unknown[]): unknown[] {\n const out: unknown[] = []\n for (const ev of events) {\n const type = deltaTypeOf(ev)\n const prev = out[out.length - 1]\n if (type && prev && deltaTypeOf(prev) === type) {\n const read = (x: unknown): AnyRecord =>\n ((x as AnyRecord).kind === 'event' ? (x as AnyRecord).event : x) as AnyRecord\n const merged = JSON.parse(JSON.stringify(prev)) as AnyRecord\n read(merged).text = String(read(prev).text) + String(read(ev).text)\n out[out.length - 1] = merged\n continue\n }\n out.push(ev)\n }\n return out\n}\n\nfunction asPartUpdate(ev: unknown): { partId: unknown; delta: unknown } | null {\n const e = ev as AnyRecord | null\n if (!e || typeof e !== 'object' || e.type !== 'message.part.updated') return null\n const data = e.data as AnyRecord | undefined\n if (!data || typeof data !== 'object') return null\n const part = data.part as AnyRecord | undefined\n const partId = part?.id ?? data.partId ?? part?.partId ?? null\n return { partId, delta: data.delta }\n}\n\n/**\n * Coalesce consecutive `message.part.updated` deltas for the SAME part into one\n * event. agent-runtime products stream `ChatStreamEvent` NDJSON\n * (`{type:'message.part.updated', data:{part, delta}}`); pumped through the\n * buffer with the default tool-loop coalescer, every per-token delta persists as\n * its own row because that coalescer never recognizes the shape. Pass this as\n * {@link PumpBufferedTurnOptions.coalesce} instead.\n *\n * Concatenation-preserving for BOTH consumer styles: the merged event keeps the\n * LATEST event's `data.part` (already the cumulative accumulation) and sets\n * `data.delta` to the concatenation of the merged deltas, so a client that\n * appends `delta` and one that reads the cumulative `part` both reconstruct the\n * identical final text.\n */\nexport function coalesceChatStreamEvents(events: unknown[]): unknown[] {\n const out: unknown[] = []\n for (const ev of events) {\n const cur = asPartUpdate(ev)\n const prevEv = out[out.length - 1]\n const prev = prevEv ? asPartUpdate(prevEv) : null\n if (cur && prev && cur.partId != null && cur.partId === prev.partId) {\n // Base the merged row on the latest event (its `part` is the most complete\n // accumulation); carry forward the summed delta.\n const merged = JSON.parse(JSON.stringify(ev)) as AnyRecord\n ;(merged.data as AnyRecord).delta = String(prev.delta ?? '') + String(cur.delta ?? '')\n out[out.length - 1] = merged\n continue\n }\n out.push(ev)\n }\n return out\n}\n\n// ── buffering core (the tap) ────────────────────────────────────────────────\n\nexport interface BufferedTurnOptions {\n store: TurnEventStore\n turnId: string\n /** Deliver one serialized line to the live client. Throwing here (client\n * disconnected) does NOT stop buffering — events keep persisting. */\n write?: (line: string) => Promise<void> | void\n /** Flush buffered events to the store at most this often. Default 400ms. */\n flushIntervalMs?: number\n /** Per-flush coalescer. Default {@link coalesceDeltas} (tool-loop text/reasoning\n * deltas). agent-runtime products streaming `ChatStreamEvent` pass\n * {@link coalesceChatStreamEvents} so per-token deltas don't each persist as a\n * row. Must be concatenation-preserving. */\n coalesce?: (events: unknown[]) => unknown[]\n /** Optional scope (thread/session id) recorded with the turn status, so\n * {@link TurnEventStore.listRunning} can find this turn after a reload. */\n scopeId?: string\n}\n\n/** A push-driven buffer for a turn whose producer the caller does NOT own. */\nexport interface BufferedTurnTap {\n /** Buffer one event: persist (coalesced, on the flush window) + best-effort\n * live-deliver. Wire to a push source's per-event hook (e.g. agent-runtime\n * `handleChatTurn`'s `hooks.onEvent`). Marks the turn 'running' on first call. */\n onEvent(raw: unknown): Promise<void>\n /** Settle the turn: final flush + set status. Call after the producer resolves\n * ('complete') or rejects ('error'). 'error' flushes what was produced first. */\n done(status?: Extract<TurnStatus, 'complete' | 'error'>): Promise<void>\n}\n\n/**\n * The buffering core. Sequence-numbers every event, delivers it to `write`\n * (best-effort — a disconnected client never stops buffering), and flushes to\n * the store in coalesced batches. Drives both transports:\n *\n * • {@link pumpBufferedTurn} — when you OWN an `AsyncIterable` producer.\n * • this tap (`onEvent`/`done`) — when the producer owns iteration and only\n * hands you a push callback (agent-runtime `handleChatTurn`'s `hooks.onEvent`\n * + the finished body). Durability stays here in the shell; the engine needs\n * no `TurnEventStore` seam.\n */\nexport function createBufferedTurnTap(opts: BufferedTurnOptions): BufferedTurnTap {\n const flushIntervalMs = opts.flushIntervalMs ?? 400\n const coalesce = opts.coalesce ?? coalesceDeltas\n const startedAt = Date.now()\n let seq = 0\n let clientGone = false\n let pending: unknown[] = []\n let lastFlush = Date.now()\n let started = false\n\n async function flush(): Promise<void> {\n if (pending.length === 0) return\n const batch = coalesce(pending)\n pending = []\n const rows = batch.map((ev) => ({ seq: ++seq, event: JSON.stringify(ev) }))\n await opts.store.append(opts.turnId, rows)\n lastFlush = Date.now()\n }\n\n async function ensureStarted(): Promise<void> {\n if (started) return\n started = true\n await opts.store.setStatus(opts.turnId, 'running', opts.scopeId)\n }\n\n return {\n async onEvent(raw) {\n await ensureStarted()\n // Stamp ms-since-turn-start so any stored turn is replayable AND traceable\n // (see ../trace) from the same buffered rows.\n const ev = raw && typeof raw === 'object' ? { ...(raw as Record<string, unknown>), _t: Date.now() - startedAt } : raw\n pending.push(ev)\n if (!clientGone && opts.write) {\n try {\n // Live delivery carries a provisional ordering hint, not the persisted\n // seq (coalescing changes seq assignment); clients resume with the\n // seqs from replay, or 0 for \"everything\".\n await opts.write(JSON.stringify(ev))\n } catch {\n clientGone = true\n }\n }\n if (Date.now() - lastFlush >= flushIntervalMs) await flush()\n },\n async done(status = 'complete') {\n await ensureStarted()\n if (status === 'error') {\n await flush().catch(() => {})\n await opts.store.setStatus(opts.turnId, 'error', opts.scopeId).catch(() => {})\n return\n }\n await flush()\n await opts.store.setStatus(opts.turnId, 'complete', opts.scopeId)\n },\n }\n}\n\n// ── pump (producer side) ──────────────────────────────────────────────────\n\nexport interface PumpBufferedTurnOptions extends BufferedTurnOptions {\n source: AsyncIterable<unknown>\n}\n\n/**\n * Drive a turn to completion regardless of the live client, when you OWN the\n * producer as an `AsyncIterable`. A thin driver over {@link createBufferedTurnTap}.\n * Returns a promise that resolves when the turn finishes — hand it to\n * `ctx.waitUntil` so a disconnect can't kill the turn. Never rejects on\n * client-write failure; a source error marks the turn 'error' (after flushing\n * what was produced) and rethrows.\n */\nexport async function pumpBufferedTurn(opts: PumpBufferedTurnOptions): Promise<void> {\n const tap = createBufferedTurnTap(opts)\n try {\n for await (const raw of opts.source) await tap.onEvent(raw)\n await tap.done('complete')\n } catch (err) {\n await tap.done('error')\n throw err\n }\n}\n\n// ── replay (consumer side) ────────────────────────────────────────────────\n\nexport interface ReplayTurnEventsOptions {\n store: TurnEventStore\n turnId: string\n /** Replay strictly after this sequence number (0 = from the beginning). */\n fromSeq?: number\n /** Poll cadence while the turn is still running. Default 500ms. */\n pollMs?: number\n /** Give up following a 'running' turn after this long. Default 120s. */\n timeoutMs?: number\n}\n\n/**\n * Yield buffered events after `fromSeq`, then keep polling while the turn is\n * still 'running' until it completes, errors, or times out. Terminates with a\n * final `{seq: -1, event: '{\"type\":\"turn_status\",...}'}` marker so clients\n * know why the replay ended.\n */\nexport async function* replayTurnEvents(opts: ReplayTurnEventsOptions): AsyncGenerator<BufferedTurnEvent> {\n const pollMs = opts.pollMs ?? 500\n const timeoutMs = opts.timeoutMs ?? 120_000\n let cursor = opts.fromSeq ?? 0\n const deadline = Date.now() + timeoutMs\n\n for (;;) {\n const batch = await opts.store.read(opts.turnId, cursor)\n for (const row of batch) {\n cursor = Math.max(cursor, row.seq)\n yield row\n }\n const status = await opts.store.getStatus(opts.turnId)\n if (status !== 'running') {\n yield { seq: -1, event: JSON.stringify({ type: 'turn_status', status: status ?? 'unknown' }) }\n return\n }\n if (Date.now() >= deadline) {\n yield { seq: -1, event: JSON.stringify({ type: 'turn_status', status: 'timeout' }) }\n return\n }\n await new Promise((r) => setTimeout(r, pollMs))\n }\n}\n\n// ── D1 store ──────────────────────────────────────────────────────────────\n\n/** Minimal structural D1 contract (Cloudflare `D1Database` satisfies it). */\nexport interface D1LikeForTurns {\n prepare(sql: string): {\n bind(...values: unknown[]): {\n run(): Promise<unknown>\n all<T = Record<string, unknown>>(): Promise<{ results: T[] }>\n first<T = Record<string, unknown>>(): Promise<T | null>\n }\n }\n}\n\n/** Schema for the D1 store — append to the product's migrations. */\nexport const TURN_EVENTS_MIGRATION_SQL = `\nCREATE TABLE IF NOT EXISTS turn_events (\n turnId TEXT NOT NULL,\n seq INTEGER NOT NULL,\n event TEXT NOT NULL,\n PRIMARY KEY (turnId, seq)\n);\nCREATE TABLE IF NOT EXISTS turn_status (\n turnId TEXT PRIMARY KEY,\n status TEXT NOT NULL,\n scopeId TEXT,\n updatedAt TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS idx_turn_status_scope ON turn_status (scopeId, status);\n`\n\n/** For deployments whose `turn_status` table predates `scopeId`/`listRunning` —\n * run once to add the column (the CREATE above already includes it for new\n * deployments). SQLite ignores a duplicate-add error if already applied. */\nexport const TURN_STATUS_SCOPE_MIGRATION_SQL = `ALTER TABLE turn_status ADD COLUMN scopeId TEXT;`\n\nexport function createD1TurnEventStore(db: D1LikeForTurns): TurnEventStore {\n return {\n async append(turnId, events) {\n if (!events.length) return\n // One multi-row insert per flush window keeps write volume bounded.\n const placeholders = events.map(() => '(?, ?, ?)').join(', ')\n const values = events.flatMap((e) => [turnId, e.seq, e.event])\n await db.prepare(`INSERT OR IGNORE INTO turn_events (turnId, seq, event) VALUES ${placeholders}`).bind(...values).run()\n },\n async read(turnId, fromSeq) {\n const { results } = await db\n .prepare('SELECT seq, event FROM turn_events WHERE turnId = ? AND seq > ? ORDER BY seq ASC')\n .bind(turnId, fromSeq)\n .all<{ seq: number; event: string }>()\n return results\n },\n async setStatus(turnId, status, scopeId) {\n // COALESCE preserves a scopeId set on the initial 'running' write when a\n // later 'complete'/'error' write passes none.\n await db\n .prepare(\n 'INSERT INTO turn_status (turnId, status, scopeId, updatedAt) VALUES (?, ?, ?, ?) ON CONFLICT(turnId) DO UPDATE SET status = excluded.status, scopeId = COALESCE(excluded.scopeId, turn_status.scopeId), updatedAt = excluded.updatedAt',\n )\n .bind(turnId, status, scopeId ?? null, new Date().toISOString())\n .run()\n },\n async getStatus(turnId) {\n const row = await db.prepare('SELECT status FROM turn_status WHERE turnId = ?').bind(turnId).first<{ status: TurnStatus }>()\n return row?.status ?? null\n },\n async listRunning(scopeId) {\n const { results } = await db\n .prepare(\"SELECT turnId FROM turn_status WHERE scopeId = ? AND status = 'running' ORDER BY updatedAt DESC\")\n .bind(scopeId)\n .all<{ turnId: string }>()\n return results.map((r) => r.turnId)\n },\n }\n}\n\n/** In-memory store for tests and keyless local dev. */\nexport function createMemoryTurnEventStore(): TurnEventStore {\n const events = new Map<string, BufferedTurnEvent[]>()\n const status = new Map<string, TurnStatus>()\n const scopes = new Map<string, string>()\n const order: string[] = []\n return {\n async append(turnId, rows) {\n const list = events.get(turnId) ?? []\n list.push(...rows)\n events.set(turnId, list)\n },\n async read(turnId, fromSeq) {\n return (events.get(turnId) ?? []).filter((e) => e.seq > fromSeq)\n },\n async setStatus(turnId, s, scopeId) {\n status.set(turnId, s)\n if (scopeId) scopes.set(turnId, scopeId)\n if (!order.includes(turnId)) order.push(turnId)\n },\n async getStatus(turnId) {\n return status.get(turnId) ?? null\n },\n async listRunning(scopeId) {\n // Newest first, mirroring the D1 store's `ORDER BY updatedAt DESC`.\n return [...order].reverse().filter((t) => status.get(t) === 'running' && scopes.get(t) === scopeId)\n },\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,99 @@
1
+ // src/chat-routes/stale-turn-lock.ts
2
+ var DEFAULT_STALE_TURN_LOCK_GRACE_MS = 5 * 60 * 1e3;
3
+ var DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS = 60 * 1e3;
4
+ function messageOf(err) {
5
+ return err instanceof Error ? err.message : String(err);
6
+ }
7
+ async function reconcileStaleTurnLock(options) {
8
+ let sandbox;
9
+ try {
10
+ sandbox = await options.probeSandbox();
11
+ } catch (err) {
12
+ return forceReleaseUnreachable(options, {
13
+ unreachableReason: "SANDBOX_PROBE_FAILED",
14
+ unreachableDetail: messageOf(err)
15
+ });
16
+ }
17
+ if (sandbox.status !== "running") {
18
+ return forceReleaseUnreachable(options, {
19
+ unreachableReason: sandbox.status === "absent" ? "SANDBOX_ABSENT" : "SANDBOX_NOT_RUNNING",
20
+ ...sandbox.status === "not-running" && sandbox.state !== void 0 ? { sandboxState: sandbox.state } : {}
21
+ });
22
+ }
23
+ const observedAt = (options.now ?? Date.now)();
24
+ let session;
25
+ try {
26
+ session = await options.probeSession();
27
+ } catch (err) {
28
+ return forceReleaseUnreachable(options, {
29
+ unreachableReason: "SESSION_PROBE_FAILED",
30
+ unreachableDetail: messageOf(err)
31
+ });
32
+ }
33
+ if (!session.reachable) {
34
+ return forceReleaseUnreachable(options, {
35
+ unreachableReason: "SESSION_UNREACHABLE",
36
+ ...session.reason !== void 0 ? { sessionProbeError: session.reason } : {}
37
+ });
38
+ }
39
+ const diagnostics = {
40
+ sandboxReachable: true,
41
+ sessionTerminal: session.terminal,
42
+ ...session.diagnostics ?? {}
43
+ };
44
+ if (!session.terminal) return { released: false, diagnostics };
45
+ const terminalGraceMs = options.terminalGraceMs ?? DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS;
46
+ const lockAgeMs = observedAt - options.lockStartedAt;
47
+ if (lockAgeMs < terminalGraceMs) {
48
+ const log = options.log ?? ((message, meta) => console.warn(message, meta));
49
+ const withheld = {
50
+ ...diagnostics,
51
+ lockAgeMs,
52
+ terminalReleaseGraceMs: terminalGraceMs,
53
+ terminalReleaseWithheld: "LOCK_WITHIN_TERMINAL_GRACE_PERIOD"
54
+ };
55
+ log("[chat-routes] stale turn lock held: terminal verdict but lock is younger than the registration window", {
56
+ ...options.context ?? {},
57
+ ...withheld
58
+ });
59
+ return { released: false, diagnostics: withheld };
60
+ }
61
+ const released = await options.release({ observedAt });
62
+ return {
63
+ released,
64
+ diagnostics: { ...diagnostics, lockAgeMs, terminalReleaseGraceMs: terminalGraceMs, released, observedAt }
65
+ };
66
+ }
67
+ async function forceReleaseUnreachable(options, reason) {
68
+ const log = options.log ?? ((message, meta) => console.warn(message, meta));
69
+ const graceMs = options.graceMs ?? DEFAULT_STALE_TURN_LOCK_GRACE_MS;
70
+ const at = (options.now ?? Date.now)();
71
+ const lockAgeMs = at - options.lockStartedAt;
72
+ const diagnostics = {
73
+ ...reason,
74
+ sandboxReachable: false,
75
+ lockAgeMs,
76
+ forceReleaseGraceMs: graceMs
77
+ };
78
+ if (lockAgeMs < graceMs) {
79
+ log("[chat-routes] stale turn lock held: sandbox unreachable but lock is inside the grace period", {
80
+ ...options.context ?? {},
81
+ ...diagnostics
82
+ });
83
+ return { released: false, diagnostics: { ...diagnostics, forceReleaseWithheld: "LOCK_WITHIN_GRACE_PERIOD" } };
84
+ }
85
+ const released = await options.release({ observedAt: at });
86
+ log("[chat-routes] force-released stale turn lock: sandbox unreachable, no turn can be executing", {
87
+ ...options.context ?? {},
88
+ ...diagnostics,
89
+ released
90
+ });
91
+ return { released, diagnostics: { ...diagnostics, forceReleased: released } };
92
+ }
93
+
94
+ export {
95
+ DEFAULT_STALE_TURN_LOCK_GRACE_MS,
96
+ DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS,
97
+ reconcileStaleTurnLock
98
+ };
99
+ //# sourceMappingURL=chunk-BQV42AFK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/chat-routes/stale-turn-lock.ts"],"sourcesContent":["/**\n * Recovery policy for a `ChatTurnLock` whose holder died.\n *\n * `createChatTurnRoutes` takes the lock as a seam (`acquire`/`release`) and a\n * lock is a single-flight guard: while it is held, a second turn on the same\n * scope is refused. Products give it a TTL measured in tens of minutes, so a\n * turn that dies without releasing wedges chat for that whole window. Every\n * app on the seam inherits that wedge, which is why the way OUT of it is\n * policy this package owns rather than something each app rediscovers.\n *\n * The policy takes PROBES, not clients: it imports no sandbox SDK, opens no\n * connection, and knows nothing about how a product finds its box or talks to\n * a sidecar. That is what makes the rules testable and what keeps the concrete\n * probes — which box key, which session id, which sidecar endpoint — in the\n * product.\n *\n * The rules, in precedence order:\n *\n * 1. The session probe answered and the execution is TERMINAL ⇒ release, once\n * the lock is past a short grace period. The authority on \"is this turn\n * still running\" is whatever is actually running it; a terminal verdict is\n * proof the lock outlived its turn — but only if the verdict is about THIS\n * turn, which is what the grace buys (see\n * {@link DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS}).\n * 2. The session probe answered and the execution is LIVE ⇒ hold, always.\n * Nothing below may override this. The lock is doing exactly its job.\n * 3. The probes could not reach that authority at all — the sandbox could not\n * be listed, is gone, is not running, or its session probe failed ⇒ fall\n * back on the physical argument: an execution runs INSIDE the box, so a box\n * that is not there is running nothing, and the lock is releasable. Without\n * this fallback the recovery would depend on the very subsystem whose\n * failure produced the stale lock.\n *\n * Rule 3 is gated on a grace period because it is an inference, not an\n * observation — see {@link DEFAULT_STALE_TURN_LOCK_GRACE_MS}.\n */\n\n/** Where the box is, as far as the caller can see. `state` on `not-running`\n * is the platform's own status string, carried through for the log. */\nexport type StaleTurnLockSandboxProbeResult =\n | { status: 'running' }\n | { status: 'absent' }\n | { status: 'not-running'; state?: string }\n\n/** What the thing running the turn says about it. `terminal: false` means an\n * execution is LIVE — the strongest signal in the policy. `diagnostics` rides\n * through to the result and the logs unread. */\nexport type StaleTurnLockSessionProbeResult =\n | { reachable: true; terminal: boolean; diagnostics?: Record<string, unknown> }\n | { reachable: false; reason?: string }\n\n/**\n * Minimum age a lock must reach before the \"sandbox unreachable ⇒ nothing can\n * be running\" fallback may force-release it.\n *\n * The lock is acquired BEFORE the box is ensured, so during a cold workspace's\n * first turn there is a real window in which the lock is held and no box exists\n * yet — indistinguishable, from a peek, from a box that vanished. The grace\n * period has to outlast that window (create + bootstrap + whatever the product\n * hydrates) or a concurrent request steals the lock from a turn that is merely\n * still provisioning. Five minutes clears observed cold starts with room to\n * spare while cutting the worst case from a TTL-length wedge down to five\n * minutes. Raising it makes recovery slower; lowering it risks stealing a lock\n * mid-provision.\n */\nexport const DEFAULT_STALE_TURN_LOCK_GRACE_MS = 5 * 60 * 1000\n\n/**\n * Minimum age a lock must reach before a TERMINAL session verdict may release\n * it.\n *\n * The session probe is keyed on the THREAD, not on the execution the lock\n * holds: a sidecar that has nothing running reports `terminal` with\n * `activeExecutionId: null`, so there is no id to match the lock against. The\n * lock, meanwhile, is acquired BEFORE the box is ensured and before the\n * execution registers with the sidecar. Between those two moments a second\n * request that reconciles the lock asks the sidecar about a turn it has not\n * heard of yet and gets back the PREVIOUS turn's terminal state — proof about\n * the wrong execution. Releasing on that verdict hands the second request a\n * lock the first one is still using, which is two concurrent turns on a scope\n * whose single-flight guard just voted for itself.\n *\n * One minute covers the acquire → box-ensure → sidecar-registration window on\n * a warm box (the cold-box case is Rule 3's, and has its own, much longer\n * grace). Deliberately NOT\n * {@link DEFAULT_STALE_TURN_LOCK_GRACE_MS}: this branch has a positive\n * observation behind it, so it should recover fast, and stretching it to five\n * minutes would leave a genuinely dead turn wedged for the whole window that\n * the session probe exists to shortcut. Raising it delays recovery from a\n * crashed turn; lowering it narrows the registration window it protects.\n */\nexport const DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS = 60 * 1000\n\nexport interface ReconcileStaleTurnLockOptions {\n /** When the held lock was acquired (epoch ms). The grace period is measured\n * from here, so it must be the LOCK's start, not the turn's. */\n lockStartedAt: number\n /** Is the box there and running? Never provisions — a peek, not an ensure.\n * A throw is treated as unreachable, same as `absent`. */\n probeSandbox(): Promise<StaleTurnLockSandboxProbeResult>\n /** Ask the running box whether the execution is still live. Only called when\n * `probeSandbox` reported `running`. A throw is treated as unreachable. */\n probeSession(): Promise<StaleTurnLockSessionProbeResult>\n /** Release the lock, fenced by the instant the releasing evidence was\n * observed. `fence.observedAt` is snapshotted BEFORE the probe that\n * justified the release, so a store that can compare it against the held\n * lock's start refuses to delete a SUCCESSOR lock acquired while the probe\n * was in flight. A store that cannot make that comparison may ignore the\n * fence, but must not substitute its own `Date.now()` — that timestamp is\n * by construction newer than any successor and makes the check vacuous.\n *\n * Returns whether the release actually landed — `false` when the lock was\n * already gone (someone else got there first), which is reported, never\n * treated as a release. */\n release(fence: { observedAt: number }): boolean | Promise<boolean>\n /** Override {@link DEFAULT_STALE_TURN_LOCK_GRACE_MS} (Rule 3's fallback). */\n graceMs?: number\n /** Override {@link DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS} (Rule 1's release). */\n terminalGraceMs?: number\n /** Identity fields merged into every log line (workspace, thread, execution\n * id — whatever makes the entry findable in the product's logs). */\n context?: Record<string, unknown>\n /** Defaults to `console.warn`. Both the withheld and the force-released\n * branches log; a force-release is never silent. */\n log?(message: string, meta: Record<string, unknown>): void\n /** Injectable clock, for tests. */\n now?(): number\n}\n\nexport interface ReconcileStaleTurnLockResult {\n released: boolean\n /** Why the policy decided what it did — the probe's own diagnostics on the\n * reachable path, the unreachable reason and lock age on the fallback. */\n diagnostics: Record<string, unknown>\n}\n\nfunction messageOf(err: unknown): string {\n return err instanceof Error ? err.message : String(err)\n}\n\n/**\n * Decide whether a held lock is stale and, if so, release it.\n *\n * Never provisions and never mutates anything but the lock: a reconciliation\n * attempt on a cold workspace leaves it cold.\n */\nexport async function reconcileStaleTurnLock(\n options: ReconcileStaleTurnLockOptions,\n): Promise<ReconcileStaleTurnLockResult> {\n let sandbox: StaleTurnLockSandboxProbeResult\n try {\n sandbox = await options.probeSandbox()\n } catch (err) {\n return forceReleaseUnreachable(options, {\n unreachableReason: 'SANDBOX_PROBE_FAILED',\n unreachableDetail: messageOf(err),\n })\n }\n if (sandbox.status !== 'running') {\n return forceReleaseUnreachable(options, {\n unreachableReason: sandbox.status === 'absent' ? 'SANDBOX_ABSENT' : 'SANDBOX_NOT_RUNNING',\n ...(sandbox.status === 'not-running' && sandbox.state !== undefined\n ? { sandboxState: sandbox.state }\n : {}),\n })\n }\n\n // Snapshot the clock BEFORE the probe and use that one instant for BOTH the\n // grace decision and the release fence: the release is only valid against the\n // lock as it was when the state was observed, not after an arbitrarily slow\n // round trip. Re-reading the clock after the probe would let a slow round\n // trip age the lock past the grace on paper, and would hand the store a fence\n // newer than a successor lock acquired meanwhile.\n const observedAt = (options.now ?? Date.now)()\n let session: StaleTurnLockSessionProbeResult\n try {\n session = await options.probeSession()\n } catch (err) {\n return forceReleaseUnreachable(options, {\n unreachableReason: 'SESSION_PROBE_FAILED',\n unreachableDetail: messageOf(err),\n })\n }\n if (!session.reachable) {\n return forceReleaseUnreachable(options, {\n unreachableReason: 'SESSION_UNREACHABLE',\n ...(session.reason !== undefined ? { sessionProbeError: session.reason } : {}),\n })\n }\n\n const diagnostics: Record<string, unknown> = {\n sandboxReachable: true,\n sessionTerminal: session.terminal,\n ...(session.diagnostics ?? {}),\n }\n // The authority answered and says the execution is live: the lock is doing\n // its job. Nothing below this point may override that.\n if (!session.terminal) return { released: false, diagnostics }\n\n // Terminal, but about WHICH execution? The probe is thread-keyed, so a lock\n // younger than the registration window may be reading the previous turn's\n // verdict — hold until it is old enough that the verdict has to be its own.\n const terminalGraceMs = options.terminalGraceMs ?? DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS\n const lockAgeMs = observedAt - options.lockStartedAt\n if (lockAgeMs < terminalGraceMs) {\n const log = options.log ?? ((message, meta) => console.warn(message, meta))\n const withheld = {\n ...diagnostics,\n lockAgeMs,\n terminalReleaseGraceMs: terminalGraceMs,\n terminalReleaseWithheld: 'LOCK_WITHIN_TERMINAL_GRACE_PERIOD',\n }\n log('[chat-routes] stale turn lock held: terminal verdict but lock is younger than the registration window', {\n ...(options.context ?? {}),\n ...withheld,\n })\n return { released: false, diagnostics: withheld }\n }\n\n const released = await options.release({ observedAt })\n return {\n released,\n diagnostics: { ...diagnostics, lockAgeMs, terminalReleaseGraceMs: terminalGraceMs, released, observedAt },\n }\n}\n\n/**\n * The fallback: the session authority could not attest to the execution's\n * state because the box is gone, stopped, or unreachable. Release the lock\n * once it is old enough that it cannot belong to a turn still provisioning its\n * box — and say so either way, with the reason, the lock's age, and the grace\n * period it was measured against.\n *\n * One clock read (`at`) serves as the age gate AND the release fence here too,\n * for the same reason the terminal branch uses `observedAt` for both.\n */\nasync function forceReleaseUnreachable(\n options: ReconcileStaleTurnLockOptions,\n reason: Record<string, unknown> & { unreachableReason: string },\n): Promise<ReconcileStaleTurnLockResult> {\n const log = options.log ?? ((message, meta) => console.warn(message, meta))\n const graceMs = options.graceMs ?? DEFAULT_STALE_TURN_LOCK_GRACE_MS\n const at = (options.now ?? Date.now)()\n const lockAgeMs = at - options.lockStartedAt\n const diagnostics: Record<string, unknown> = {\n ...reason,\n sandboxReachable: false,\n lockAgeMs,\n forceReleaseGraceMs: graceMs,\n }\n\n if (lockAgeMs < graceMs) {\n log('[chat-routes] stale turn lock held: sandbox unreachable but lock is inside the grace period', {\n ...(options.context ?? {}),\n ...diagnostics,\n })\n return { released: false, diagnostics: { ...diagnostics, forceReleaseWithheld: 'LOCK_WITHIN_GRACE_PERIOD' } }\n }\n\n const released = await options.release({ observedAt: at })\n log('[chat-routes] force-released stale turn lock: sandbox unreachable, no turn can be executing', {\n ...(options.context ?? {}),\n ...diagnostics,\n released,\n })\n return { released, diagnostics: { ...diagnostics, forceReleased: released } }\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.ts CHANGED
@@ -19,7 +19,8 @@ export { ObjectBody, ObjectKeyParts, ObjectStore, PutObjectOptions, R2LikeBucket
19
19
  export { B as BULK_DELETE_MAX_THREADS, C as ChatStoreInputError, t as threadTitleFromMessage } from './core-7qIM7svy.js';
20
20
  export { C as ChatFilePart, a as ChatImagePart, b as ChatInteractionPart, c as ChatMentionKind, d as ChatMentionPart, e as ChatMessagePart, f as ChatNoticePart, g as ChatPartTime, h as ChatPlanPart, i as ChatReasoningPart, j as ChatStepFinishPart, k as ChatStepStartPart, l as ChatSubtaskPart, m as ChatTextPart, n as ChatToolPart, o as ChatToolState, p as ChatToolStatus, q as ChatUsageTokens, S as StorableHarnessPartKind, r as isChatInteractionPart, s as isChatMentionPart, t as isChatPlanPart, u as isChatStepFinishPart, v as isChatTextPart, w as isChatToolPart, x as mentionInputToPart, y as mentionPartsFromMessageParts, z as toChatMessageParts } from './parts-1_3y2JmR.js';
21
21
  export { DeriveKeyOptions, createFieldCrypto, decodeHexKey, decryptAesGcm, decryptBytes, decryptWithKey, deriveKey, encryptAesGcm, encryptBytes, encryptWithKey } from './crypto/index.js';
22
- export { BufferedTurnEvent, BufferedTurnOptions, BufferedTurnTap, D1LikeForTurns, JsonRecord, PersistedChatMessageForTurn, PumpBufferedTurnOptions, ReplayTurnEventsOptions, ResolvedChatTurn, StreamEvent, TURN_EVENTS_MIGRATION_SQL, TURN_STATUS_SCOPE_MIGRATION_SQL, TurnEventStore, TurnStatus, asRecord, asString, buildUserTextParts, coalesceChatStreamEvents, coalesceDeltas, createBufferedTurnTap, createD1TurnEventStore, createMemoryTurnEventStore, encodeEvent, finalizeAssistantParts, getPartKey, mergePersistedPart, messageHasTurnId, normalizeClientTurnId, normalizePersistedPart, normalizeTime, normalizeToolEvent, pumpBufferedTurn, replayTurnEvents, resolveChatTurn, resolveToolId, resolveToolName } from './stream/index.js';
22
+ export { JsonRecord, MISSING_TOOL_TERMINAL_ERROR, MISSING_TOOL_TERMINAL_REASON, PersistedChatMessageForTurn, ResolvedChatTurn, StreamEvent, asRecord, asString, attachmentPartKey, buildUserTextParts, collapseRedundantTextParts, encodeEvent, finalizeAssistantParts, finalizePendingInteractionParts, getPartKey, mergePersistedPart, messageHasTurnId, normalizeClientTurnId, normalizePersistedPart, normalizeTime, normalizeToolEvent, resolveChatTurn, resolveToolId, resolveToolName, terminalizeDanglingAssistantToolUpdates, terminalizeDanglingToolPart, terminalizeDanglingToolParts } from './stream/index.js';
23
+ export { B as BufferedTurnEvent, a as BufferedTurnOptions, b as BufferedTurnTap, D as D1LikeForTurns, P as PumpBufferedTurnOptions, R as ReplayTurnEventsOptions, T as TURN_EVENTS_MIGRATION_SQL, c as TURN_STATUS_SCOPE_MIGRATION_SQL, d as TurnEventStore, e as TurnStatus, f as coalesceChatStreamEvents, g as coalesceDeltas, h as createBufferedTurnTap, i as createD1TurnEventStore, j as createMemoryTurnEventStore, p as pumpBufferedTurn, r as replayTurnEvents } from './turn-buffer-C9mEgoop.js';
23
24
  export { HubExecClient, HubExecClientOptions, HubExecErrorCode, HubExecResult, HubInvokeDeps, HubInvokeInput, HubInvokeOutcome, ParsedIntegrationAction, invokeIntegrationHub, resolveIntegrationAction } from './integrations/index.js';
24
25
  export { C as ChatInteraction, a as ChatInteractionField, b as ChatInteractionStatus, c as ChatSelectField, d as ComposerAnswerDelivery, I as INTERACTION_CANCEL_EVENT, e as INTERACTION_EVENT, f as INTERACTION_RESOLVED_EVENT, g as InteractionAnswerValue, h as InteractionAnswers, i as InteractionCancelData, j as InteractionPersistedPart, k as InteractionRequestWire, N as NoticeKind, l as NoticePersistedPart, P as ParseInteractionAnswersResult, m as ParseInteractionResult, n as canTransitionInteractionStatus, o as cancelStatusFor, p as composerAnswerData, q as composerAnswerDeliveries, r as dedupeQuestionInteractionsByContent, s as fieldAcceptsFreeText, t as interactionFromWireRequest, u as interactionPartKey, v as interactionToPersistedPart, w as isRenderableInteractionKind, x as isSafeInteractionFieldKey, y as isTerminalInteractionStatus, z as noticePart, A as noticePartKey, B as parseInteractionAnswers, D as parseInteractionCancel, E as parseInteractionRequest, F as persistedPartToInteraction, G as questionInteractionContentSignature, H as stampInteractionAnswers } from './contract-KfqJh_au.js';
25
26
  export { BeforeInteractionAnswerArgs, DurableInteractionRouteArgs, DurableInteractionRoutePersistence, InteractionAnswerBodyValidation, InteractionAnswerRoute, InteractionAnswerRouteOptions, InteractionClientOutcome, InteractionConnectionResolution, InteractionRouteLogger, ResolveInteractionConnectionArgs, SidecarInteractionsConnection, SidecarInteractionsError, SidecarInteractionsResult, createInteractionAnswerRoute, listSessionInteractions, mapInteractionRespondFailure, respondToSessionInteraction, validateInteractionAnswerBody } from './interactions/index.js';
package/dist/index.js CHANGED
@@ -244,18 +244,23 @@ import {
244
244
  threadTitleFromMessage
245
245
  } from "./chunk-5EQCITY3.js";
246
246
  import {
247
+ MISSING_TOOL_TERMINAL_ERROR,
248
+ MISSING_TOOL_TERMINAL_REASON,
247
249
  TURN_EVENTS_MIGRATION_SQL,
248
250
  TURN_STATUS_SCOPE_MIGRATION_SQL,
249
251
  asRecord,
250
252
  asString,
253
+ attachmentPartKey,
251
254
  buildUserTextParts,
252
255
  coalesceChatStreamEvents,
253
256
  coalesceDeltas,
257
+ collapseRedundantTextParts,
254
258
  createBufferedTurnTap,
255
259
  createD1TurnEventStore,
256
260
  createMemoryTurnEventStore,
257
261
  encodeEvent,
258
262
  finalizeAssistantParts,
263
+ finalizePendingInteractionParts,
259
264
  getPartKey,
260
265
  mergePersistedPart,
261
266
  messageHasTurnId,
@@ -267,8 +272,11 @@ import {
267
272
  replayTurnEvents,
268
273
  resolveChatTurn,
269
274
  resolveToolId,
270
- resolveToolName
271
- } from "./chunk-B5JD3DXD.js";
275
+ resolveToolName,
276
+ terminalizeDanglingAssistantToolUpdates,
277
+ terminalizeDanglingToolPart,
278
+ terminalizeDanglingToolParts
279
+ } from "./chunk-4AUQIAYU.js";
272
280
  import {
273
281
  createInteractionAnswerRoute,
274
282
  listSessionInteractions,
@@ -544,6 +552,8 @@ export {
544
552
  MAX_CAPTION_BATCH,
545
553
  MCP_PROTOCOL_VERSIONS,
546
554
  MIN_SEQUENCE_CLIP_FRAMES,
555
+ MISSING_TOOL_TERMINAL_ERROR,
556
+ MISSING_TOOL_TERMINAL_REASON,
547
557
  MISSION_CONTROL_CHANNEL_ID,
548
558
  MissionConcurrencyError,
549
559
  PLAN_SUBMITTED_EVENT,
@@ -594,6 +604,7 @@ export {
594
604
  assertSceneMediaSrc,
595
605
  assertSequenceMediaUrl,
596
606
  attachReasoningEffort,
607
+ attachmentPartKey,
597
608
  authenticateToolRequest,
598
609
  bearerSubprotocolToken,
599
610
  bearerToken,
@@ -640,6 +651,7 @@ export {
640
651
  coalesceChatStreamEvents,
641
652
  coalesceDeltas,
642
653
  coerceHarness,
654
+ collapseRedundantTextParts,
643
655
  collectSlots,
644
656
  composeMissionFlowTrace,
645
657
  composerAnswerData,
@@ -729,6 +741,7 @@ export {
729
741
  fetchModelCatalog,
730
742
  fieldAcceptsFreeText,
731
743
  finalizeAssistantParts,
744
+ finalizePendingInteractionParts,
732
745
  findCanvasMcpTool,
733
746
  findCustomTool,
734
747
  findElement,
@@ -894,6 +907,9 @@ export {
894
907
  syncSandboxMemberRole,
895
908
  tangleExecutionKeyHttpError,
896
909
  terminalTokenFromRequest,
910
+ terminalizeDanglingAssistantToolUpdates,
911
+ terminalizeDanglingToolPart,
912
+ terminalizeDanglingToolParts,
897
913
  themeColor,
898
914
  themeToCssVars,
899
915
  threadTitleFromMessage,
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Recovery policy for a `ChatTurnLock` whose holder died.
3
+ *
4
+ * `createChatTurnRoutes` takes the lock as a seam (`acquire`/`release`) and a
5
+ * lock is a single-flight guard: while it is held, a second turn on the same
6
+ * scope is refused. Products give it a TTL measured in tens of minutes, so a
7
+ * turn that dies without releasing wedges chat for that whole window. Every
8
+ * app on the seam inherits that wedge, which is why the way OUT of it is
9
+ * policy this package owns rather than something each app rediscovers.
10
+ *
11
+ * The policy takes PROBES, not clients: it imports no sandbox SDK, opens no
12
+ * connection, and knows nothing about how a product finds its box or talks to
13
+ * a sidecar. That is what makes the rules testable and what keeps the concrete
14
+ * probes — which box key, which session id, which sidecar endpoint — in the
15
+ * product.
16
+ *
17
+ * The rules, in precedence order:
18
+ *
19
+ * 1. The session probe answered and the execution is TERMINAL ⇒ release, once
20
+ * the lock is past a short grace period. The authority on "is this turn
21
+ * still running" is whatever is actually running it; a terminal verdict is
22
+ * proof the lock outlived its turn — but only if the verdict is about THIS
23
+ * turn, which is what the grace buys (see
24
+ * {@link DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS}).
25
+ * 2. The session probe answered and the execution is LIVE ⇒ hold, always.
26
+ * Nothing below may override this. The lock is doing exactly its job.
27
+ * 3. The probes could not reach that authority at all — the sandbox could not
28
+ * be listed, is gone, is not running, or its session probe failed ⇒ fall
29
+ * back on the physical argument: an execution runs INSIDE the box, so a box
30
+ * that is not there is running nothing, and the lock is releasable. Without
31
+ * this fallback the recovery would depend on the very subsystem whose
32
+ * failure produced the stale lock.
33
+ *
34
+ * Rule 3 is gated on a grace period because it is an inference, not an
35
+ * observation — see {@link DEFAULT_STALE_TURN_LOCK_GRACE_MS}.
36
+ */
37
+ /** Where the box is, as far as the caller can see. `state` on `not-running`
38
+ * is the platform's own status string, carried through for the log. */
39
+ type StaleTurnLockSandboxProbeResult = {
40
+ status: 'running';
41
+ } | {
42
+ status: 'absent';
43
+ } | {
44
+ status: 'not-running';
45
+ state?: string;
46
+ };
47
+ /** What the thing running the turn says about it. `terminal: false` means an
48
+ * execution is LIVE — the strongest signal in the policy. `diagnostics` rides
49
+ * through to the result and the logs unread. */
50
+ type StaleTurnLockSessionProbeResult = {
51
+ reachable: true;
52
+ terminal: boolean;
53
+ diagnostics?: Record<string, unknown>;
54
+ } | {
55
+ reachable: false;
56
+ reason?: string;
57
+ };
58
+ /**
59
+ * Minimum age a lock must reach before the "sandbox unreachable ⇒ nothing can
60
+ * be running" fallback may force-release it.
61
+ *
62
+ * The lock is acquired BEFORE the box is ensured, so during a cold workspace's
63
+ * first turn there is a real window in which the lock is held and no box exists
64
+ * yet — indistinguishable, from a peek, from a box that vanished. The grace
65
+ * period has to outlast that window (create + bootstrap + whatever the product
66
+ * hydrates) or a concurrent request steals the lock from a turn that is merely
67
+ * still provisioning. Five minutes clears observed cold starts with room to
68
+ * spare while cutting the worst case from a TTL-length wedge down to five
69
+ * minutes. Raising it makes recovery slower; lowering it risks stealing a lock
70
+ * mid-provision.
71
+ */
72
+ declare const DEFAULT_STALE_TURN_LOCK_GRACE_MS: number;
73
+ /**
74
+ * Minimum age a lock must reach before a TERMINAL session verdict may release
75
+ * it.
76
+ *
77
+ * The session probe is keyed on the THREAD, not on the execution the lock
78
+ * holds: a sidecar that has nothing running reports `terminal` with
79
+ * `activeExecutionId: null`, so there is no id to match the lock against. The
80
+ * lock, meanwhile, is acquired BEFORE the box is ensured and before the
81
+ * execution registers with the sidecar. Between those two moments a second
82
+ * request that reconciles the lock asks the sidecar about a turn it has not
83
+ * heard of yet and gets back the PREVIOUS turn's terminal state — proof about
84
+ * the wrong execution. Releasing on that verdict hands the second request a
85
+ * lock the first one is still using, which is two concurrent turns on a scope
86
+ * whose single-flight guard just voted for itself.
87
+ *
88
+ * One minute covers the acquire → box-ensure → sidecar-registration window on
89
+ * a warm box (the cold-box case is Rule 3's, and has its own, much longer
90
+ * grace). Deliberately NOT
91
+ * {@link DEFAULT_STALE_TURN_LOCK_GRACE_MS}: this branch has a positive
92
+ * observation behind it, so it should recover fast, and stretching it to five
93
+ * minutes would leave a genuinely dead turn wedged for the whole window that
94
+ * the session probe exists to shortcut. Raising it delays recovery from a
95
+ * crashed turn; lowering it narrows the registration window it protects.
96
+ */
97
+ declare const DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS: number;
98
+ interface ReconcileStaleTurnLockOptions {
99
+ /** When the held lock was acquired (epoch ms). The grace period is measured
100
+ * from here, so it must be the LOCK's start, not the turn's. */
101
+ lockStartedAt: number;
102
+ /** Is the box there and running? Never provisions — a peek, not an ensure.
103
+ * A throw is treated as unreachable, same as `absent`. */
104
+ probeSandbox(): Promise<StaleTurnLockSandboxProbeResult>;
105
+ /** Ask the running box whether the execution is still live. Only called when
106
+ * `probeSandbox` reported `running`. A throw is treated as unreachable. */
107
+ probeSession(): Promise<StaleTurnLockSessionProbeResult>;
108
+ /** Release the lock, fenced by the instant the releasing evidence was
109
+ * observed. `fence.observedAt` is snapshotted BEFORE the probe that
110
+ * justified the release, so a store that can compare it against the held
111
+ * lock's start refuses to delete a SUCCESSOR lock acquired while the probe
112
+ * was in flight. A store that cannot make that comparison may ignore the
113
+ * fence, but must not substitute its own `Date.now()` — that timestamp is
114
+ * by construction newer than any successor and makes the check vacuous.
115
+ *
116
+ * Returns whether the release actually landed — `false` when the lock was
117
+ * already gone (someone else got there first), which is reported, never
118
+ * treated as a release. */
119
+ release(fence: {
120
+ observedAt: number;
121
+ }): boolean | Promise<boolean>;
122
+ /** Override {@link DEFAULT_STALE_TURN_LOCK_GRACE_MS} (Rule 3's fallback). */
123
+ graceMs?: number;
124
+ /** Override {@link DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS} (Rule 1's release). */
125
+ terminalGraceMs?: number;
126
+ /** Identity fields merged into every log line (workspace, thread, execution
127
+ * id — whatever makes the entry findable in the product's logs). */
128
+ context?: Record<string, unknown>;
129
+ /** Defaults to `console.warn`. Both the withheld and the force-released
130
+ * branches log; a force-release is never silent. */
131
+ log?(message: string, meta: Record<string, unknown>): void;
132
+ /** Injectable clock, for tests. */
133
+ now?(): number;
134
+ }
135
+ interface ReconcileStaleTurnLockResult {
136
+ released: boolean;
137
+ /** Why the policy decided what it did — the probe's own diagnostics on the
138
+ * reachable path, the unreachable reason and lock age on the fallback. */
139
+ diagnostics: Record<string, unknown>;
140
+ }
141
+ /**
142
+ * Decide whether a held lock is stale and, if so, release it.
143
+ *
144
+ * Never provisions and never mutates anything but the lock: a reconciliation
145
+ * attempt on a cold workspace leaves it cold.
146
+ */
147
+ declare function reconcileStaleTurnLock(options: ReconcileStaleTurnLockOptions): Promise<ReconcileStaleTurnLockResult>;
148
+
149
+ export { DEFAULT_STALE_TURN_LOCK_GRACE_MS as D, type ReconcileStaleTurnLockOptions as R, type StaleTurnLockSandboxProbeResult as S, DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS as a, type ReconcileStaleTurnLockResult as b, type StaleTurnLockSessionProbeResult as c, reconcileStaleTurnLock as r };