@tangle-network/agent-app 0.43.43 → 0.43.45

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/chat-routes/turn-routes.ts","../../src/chat-routes/sandbox-producer.ts","../../src/chat-routes/durable-projection.ts","../../src/chat-routes/upload.ts"],"sourcesContent":["/**\n * `createChatTurnRoutes` — the assembled server chat vertical (issue #188\n * Phase 1). One factory composing the pieces every product re-wired by hand:\n *\n * body parse/validate → `/web` `parseJsonObjectBody` + `./wire`\n * turn identity → `/stream` `resolveChatTurn` + agent-runtime\n * `deriveExecutionId`\n * producer → injected seam (sandbox lane via\n * `createSandboxChatProducer`; router lane is the\n * product's own `ChatTurnProducer`)\n * turn engine → agent-runtime `handleChatTurn` (verbatim)\n * durability → `/stream` turn-buffer tap, wired BY DEFAULT\n * (tee + drain keeps the turn running after a\n * client drop; replay serves the buffered tail)\n * persistence → injected `/chat-store`-shaped store\n * (user row on send, assistant row on completion)\n * interactions answer → `/interactions` `createInteractionAnswerRoute`\n *\n * Handlers are web-standard `Request → Response` (Workers, Node 18+, Deno) —\n * no router import. Auth/access is one injected `authorize` seam, composable\n * with `/app-auth` guards but not coupled to them.\n *\n * Six optional product seams let a complex turn-orchestrator compose the\n * vertical instead of hand-rolling a generator — each omittable to the exact\n * behavior above: `turnLock` (single-flight acquire/release around the turn),\n * `contextGate` (pre-producer domain-readiness short-circuit), `beforeTurn`\n * (observe + augment the producer input), `lifecycle` (deterministic\n * start/complete/error telemetry), `heartbeat` (keepalive during silent\n * producer waits), plus `onRawEvent` (the raw producer events, for telemetry).\n * `handleChatTurn` stays the engine — the seams only wrap its input, its\n * producer stream, and its settle.\n *\n * Seam stability: `lifecycle` and `heartbeat` are generic and stable.\n * `turnLock`, `contextGate`, `beforeTurn`, and `onRawEvent` are `@experimental`\n * — proven by a single consumer (gtm's chat vertical, #200) and may change once\n * a second consumer exercises them. They stay FLAT top-level options (not\n * grouped under a `hooks` object): that grouping would break the shipped\n * consumer's call for no mechanism gain, and this package's exports are\n * additive-only.\n */\n\nimport { deriveExecutionId, handleChatTurn } from '@tangle-network/agent-runtime'\nimport type { ChatTurnIdentity, ChatTurnProducer } from '@tangle-network/agent-runtime'\nimport { toChatMessageParts, type ChatMessagePart } from '../chat-store/parts'\nimport {\n createInteractionAnswerRoute,\n type InteractionAnswerRoute,\n type InteractionAnswerRouteOptions,\n} from '../interactions/route'\nimport {\n coalesceDeltas,\n createBufferedTurnTap,\n normalizeClientTurnId,\n replayTurnEvents,\n resolveChatTurn,\n type PersistedChatMessageForTurn,\n type TurnEventStore,\n} from '../stream/index'\nimport { parseJsonObjectBody } from '../web/index'\nimport {\n assertPromptPartsWithinCap,\n ChatTurnInputError,\n parseChatTurnParts,\n type ChatTurnFilePartInput,\n type ChatTurnPartInput,\n type ChatTurnRequestPayload,\n} from './wire'\n\n// ── seams ───────────────────────────────────────────────────────────────────\n\n/** Usage receipt persisted onto the assistant message (the flattened\n * `step-finish` shape `/chat-store`'s columns mirror). */\nexport interface ChatTurnUsage {\n inputTokens?: number\n outputTokens?: number\n reasoningTokens?: number\n cacheReadTokens?: number\n cacheWriteTokens?: number\n costUsd?: number\n}\n\n/** What the route persists — a structural subset of `/chat-store`'s\n * `ChatStore`, so `createChatStore(db, tables)` satisfies it directly and a\n * product with its own persistence adapts without importing drizzle. */\nexport interface ChatTurnMessageStore {\n listMessages(threadId: string): Promise<Array<{\n id: string\n role: 'user' | 'assistant' | 'system' | 'tool'\n content: string\n parts?: ChatMessagePart[] | null\n }>>\n appendMessage(input: {\n threadId: string\n role: 'user' | 'assistant'\n content: string\n parts?: ChatMessagePart[]\n model?: string | null\n inputTokens?: number | null\n outputTokens?: number | null\n reasoningTokens?: number | null\n cacheReadTokens?: number | null\n cacheWriteTokens?: number | null\n costUsd?: number | null\n }): Promise<unknown>\n}\n\n/** `ChatTurnProducer` plus the persisted projection the assembly reads after\n * drain. `createSandboxChatProducer` returns this; a router-lane producer\n * may omit the optional members (finalText persists as a single text part). */\nexport interface ChatTurnRouteProducer extends ChatTurnProducer {\n assistantParts?(): Array<Record<string, unknown>>\n usage?(): ChatTurnUsage\n model?: string\n}\n\nexport type ChatTurnAuthorization<TContext> =\n | {\n ok: true\n tenantId: string\n userId: string\n context: TContext\n /** When `false`, skip the `role:'user'` message insert for this turn — for\n * a product-dispatched / synthetic turn (e.g. a follow-up the product\n * raised itself) that must not surface a new user row. Composes with —\n * never overrides — the engine's retry-dedup: `authorize` runs before\n * turn identity is resolved, so it cannot tell a retry from a fresh turn;\n * a turn already deduped stays deduped. Omit / `true` → today's behavior.\n * @experimental Single-consumer; shape may change. */\n insertUserMessage?: boolean\n }\n | { ok: false; response: Response }\n\nexport interface ChatTurnAuthorizeArgs {\n request: Request\n intent: 'turn' | 'replay' | 'running'\n /** Parsed, validated POST body (turn intent only). */\n body?: ChatTurnRequestPayload\n /** The buffered turn id being replayed (replay intent only). */\n turnId?: string\n /** The thread whose running turns are being discovered (running intent only). */\n threadId?: string\n}\n\nexport interface ChatTurnProduceArgs<TContext> {\n request: Request\n body: ChatTurnRequestPayload\n identity: ChatTurnIdentity\n context: TContext\n /** The message to send: plain text, or parts when the client attached\n * files (a text part is prepended from `content` when present). */\n prompt: string | ChatTurnPartInput[]\n /** Stable id for cross-process reconnect (`deriveExecutionId`). */\n executionId: string\n /** The turn-buffer id announced to the client for replay. */\n turnStreamId: string\n priorMessages: PersistedChatMessageForTurn[]\n}\n\n/** One event as it crosses the route: the producer's own vocabulary, or an\n * injected keepalive. Same shape the engine forwards verbatim. */\ntype ChatRouteEvent = { type: string; data?: Record<string, unknown> }\n\n/** Best-effort human-readable cause from a terminal `error` /\n * `session.run.failed` event's `data`. */\nfunction failureReasonOf(data: Record<string, unknown> | undefined): string | undefined {\n if (!data) return undefined\n const message = data.message ?? data.error ?? data.reason\n if (typeof message === 'string' && message.length > 0) return message\n return undefined\n}\n\n/** Keepalive emitted while the producer is quiet (long tool calls, first-token\n * wait) so client watchdogs stay re-armed. One is emitted each time\n * `intervalMs` elapses with no producer event; the window resets on every real\n * event, so a chatty producer never triggers one. The product owns the event\n * shape (`type` + `data`). Omit → no keepalives (today's behavior). */\nexport interface ChatTurnHeartbeat {\n intervalMs: number\n event(info: { elapsedMs: number; tick: number }): ChatRouteEvent\n}\n\n/** Patch a `beforeTurn` hook returns to augment the producer's input. Omitted\n * fields keep the route-assembled value; the product's `produce` still owns\n * the system prompt. */\nexport interface ChatTurnInputPatch {\n prompt?: string | ChatTurnPartInput[]\n priorMessages?: PersistedChatMessageForTurn[]\n}\n\n/** Pre-turn readiness verdict — proceed, or short-circuit with the product's\n * own `Response` (e.g. a canned assistant reply asking for missing context).\n * Distinct from `authorize`: this gates domain readiness, not access. */\nexport type ChatTurnGateResult =\n | { proceed: true }\n | { proceed: false; response: Response }\n\n/** Single-flight lock verdict — acquired (with an opaque handle passed back to\n * `release`), or already held (short-circuit with the product's 409-style\n * `Response`). */\nexport type ChatTurnLockResult =\n | { acquired: true; handle?: unknown }\n | { acquired: false; response: Response }\n\n/** Async acquire/release wrapped around the turn. `acquire` runs before any\n * side effect; `release` runs once when the turn settles — including on a\n * short-circuit or a throw. */\nexport interface ChatTurnLock<TContext> {\n acquire(args: ChatTurnProduceArgs<TContext>): ChatTurnLockResult | Promise<ChatTurnLockResult>\n release(handle: unknown): void | Promise<void>\n}\n\ninterface ChatTurnLifecycleBase<TContext> {\n identity: ChatTurnIdentity\n executionId: string\n turnStreamId: string\n context: TContext\n}\nexport interface ChatTurnLifecycleStart<TContext> extends ChatTurnLifecycleBase<TContext> {\n startedAt: number\n}\nexport interface ChatTurnLifecycleComplete<TContext> extends ChatTurnLifecycleBase<TContext> {\n finalText: string\n usage: ChatTurnUsage\n durationMs: number\n}\nexport interface ChatTurnLifecycleError<TContext> extends ChatTurnLifecycleBase<TContext> {\n error: unknown\n durationMs: number\n}\n\n/** Deterministic run telemetry: `onTurnStart` fires before the producer runs;\n * exactly one of `onTurnComplete` / `onTurnError` fires after the turn\n * settles, always after `onTurnStart`. Failure is derived from the turn's own\n * `error` / `session.run.failed` events (or a drain throw), not the engine's\n * lifecycle envelope. Hook errors are swallowed — telemetry never fails a\n * turn. */\nexport interface ChatTurnLifecycle<TContext> {\n onTurnStart?(info: ChatTurnLifecycleStart<TContext>): void | Promise<void>\n onTurnComplete?(info: ChatTurnLifecycleComplete<TContext>): void | Promise<void>\n onTurnError?(info: ChatTurnLifecycleError<TContext>): void | Promise<void>\n}\n\nexport interface CreateChatTurnRoutesOptions<TContext = void> {\n /** Names the product in `deriveExecutionId` so retries land on the same\n * substrate execution. */\n projectId: string\n /** Authenticate + authorize the caller for a turn or a replay. The only\n * product-supplied access step: session auth, thread/workspace access,\n * seat/balance gates, rate limits all live here. */\n authorize(args: ChatTurnAuthorizeArgs): Promise<ChatTurnAuthorization<TContext>>\n /** Thread/message persistence (`/chat-store`'s store or a product adapter). */\n store: ChatTurnMessageStore\n /** Turn-event buffer (`createD1TurnEventStore(env.DB)` in production,\n * `createMemoryTurnEventStore()` in tests). Wired by default — every turn\n * is buffered and replayable. */\n turnStore: TurnEventStore\n /** Build the turn's event stream. Sandbox lane: `streamSandboxPrompt(...)`\n * wrapped in `createSandboxChatProducer`. Router/openai-compat lane: the\n * product's own producer. May be async (box resolution). */\n produce(args: ChatTurnProduceArgs<TContext>): ChatTurnRouteProducer | Promise<ChatTurnRouteProducer>\n /** Single-flight lock acquired before any side effect and released once when\n * the turn settles (including short-circuit/throw). Omit → no lock.\n * @experimental Single-consumer (gtm, #200); shape may change. */\n turnLock?: ChatTurnLock<TContext>\n /** Pre-turn readiness gate that can short-circuit with a product `Response`\n * before the producer runs (the user row is already persisted). Runs after\n * `turnLock.acquire`, before `beforeTurn`. Omit → always proceed.\n * @experimental Single-consumer (gtm, #200); shape may change. */\n contextGate?(args: ChatTurnProduceArgs<TContext>): ChatTurnGateResult | Promise<ChatTurnGateResult>\n /** Observe the assembled producer input and optionally augment it (rewrite\n * the prompt / prior messages) before the producer runs. Omit → no change.\n * @experimental Single-consumer (gtm, #200); shape may change. */\n beforeTurn?(args: ChatTurnProduceArgs<TContext>): ChatTurnInputPatch | void | Promise<ChatTurnInputPatch | void>\n /** Deterministic run telemetry (start / complete / error) with identity and\n * timing. Omit → no telemetry. */\n lifecycle?: ChatTurnLifecycle<TContext>\n /** Keepalive injected while the producer is quiet. Omit → no keepalives. */\n heartbeat?: ChatTurnHeartbeat\n /** Observe each event the producer emits, before the engine frames it and\n * before any heartbeat injection (the raw sidecar-producer events, for\n * telemetry). Never alters the stream; errors are swallowed. Distinct from\n * `onEvent`, which sees the engine-framed stream incl. lifecycle envelopes.\n * @experimental Single-consumer (gtm, #200); shape may change. */\n onRawEvent?(event: ChatRouteEvent, context: TContext): void | Promise<void>\n /** Pre-persist transform of the final text (e.g. `/redact`'s `redactPII`).\n * Live stream is never altered. */\n transformFinalText?(text: string): string | Promise<string>\n /** Post-processing after a turn settles (billing, titles, audit). Fires with\n * `failed:true` + `failureReason` when the turn carried a terminal error\n * event (model 402 / rate-limit / server error) instead of a clean\n * completion, so products skip the deduct and render an error row rather\n * than billing an empty turn and marking it done. A turn that THROWS never\n * reaches this hook (the engine skips it on a producer throw). Errors are\n * swallowed by the engine — they never fail a streamed turn. */\n onTurnComplete?(input: {\n identity: ChatTurnIdentity\n finalText: string\n context: TContext\n failed: boolean\n failureReason?: string\n }): Promise<void>\n /** Per-event side channel (product broadcast). The turn-buffer tap is\n * already wired; this runs in addition. */\n onEvent?(event: { type: string; data?: Record<string, unknown> }, context: TContext): void | Promise<void>\n /** Trace flush handed to `waitUntil` (OTLP export). */\n traceFlush?(context: TContext): Promise<void>\n /** Compose the interaction-answer endpoints (`/interactions`). Omit when the\n * product has no sidecar ask channel. */\n interactions?: InteractionAnswerRouteOptions\n /** Byte budget for inline prompt parts. Default `INLINE_PARTS_MAX_BYTES`. */\n maxInlinePartBytes?: number\n /** Per-flush coalescer for the turn buffer. Default `coalesceDeltas` (this\n * assembly streams the client vocabulary's `{type:'text'|'reasoning',\n * text}` lines, which it merges). A producer streaming raw\n * `message.part.updated` events passes `coalesceChatStreamEvents`. */\n coalesceTurnEvents?: (events: unknown[]) => unknown[]\n replay?: { pollMs?: number; timeoutMs?: number }\n log?: (message: string, meta?: Record<string, unknown>) => void\n}\n\nexport interface ChatTurnRoutes {\n /** POST — run one turn, streaming NDJSON. First line is\n * `{type:'turn', turnId}` (the replay handle); the rest is the engine's\n * event protocol. Pass the platform's `waitUntil` so the turn keeps\n * running (and buffering) after a client disconnect. */\n turn(request: Request, ctx?: { waitUntil?(p: Promise<unknown>): void }): Promise<Response>\n /** GET — replay a buffered turn from `?fromSeq=` (0 = everything), then\n * follow it live until it completes. */\n replay(request: Request, params: { turnId: string }): Promise<Response>\n /** GET `?threadId=` — the reconnect-discovery endpoint: the turn ids still\n * running on a thread, so a client that reloaded mid-turn can re-attach to\n * the live stream via {@link replay} instead of losing it. Returns `[]` when\n * the turn store cannot enumerate running turns (`listRunning` unimplemented). */\n running(request: Request): Promise<Response>\n /** list/answer endpoints from `/interactions`; null when not configured. */\n interactions: InteractionAnswerRoute | null\n}\n\n// ── body validation ────────────────────────────────────────────────────────\n\nfunction errorResponse(err: ChatTurnInputError): Response {\n return Response.json({ code: err.code, error: err.message }, { status: err.status })\n}\n\ninterface ParsedTurnBody {\n payload: ChatTurnRequestPayload\n content: string\n fileParts: ChatTurnFilePartInput[]\n turnId: string | undefined\n}\n\nfunction validateTurnBody(body: Record<string, unknown>, maxInlinePartBytes: number | undefined): ParsedTurnBody {\n const threadId = typeof body.threadId === 'string' ? body.threadId.trim() : ''\n if (!threadId) throw new ChatTurnInputError('Missing threadId')\n const rawContent = body.content ?? body.message ?? ''\n if (typeof rawContent !== 'string') throw new ChatTurnInputError('content must be a string')\n const content = rawContent.trim()\n const fileParts = parseChatTurnParts(body.parts)\n if (!content && fileParts.length === 0) {\n throw new ChatTurnInputError('Missing content (send text, parts, or both)')\n }\n assertPromptPartsWithinCap(fileParts, maxInlinePartBytes)\n let turnId: string | undefined\n try {\n turnId = normalizeClientTurnId(body.turnId)\n } catch (err) {\n throw new ChatTurnInputError(err instanceof Error ? err.message : 'Invalid turnId')\n }\n return {\n payload: { ...body, threadId, content } as ChatTurnRequestPayload,\n content,\n fileParts,\n turnId,\n }\n}\n\n/** File parts persist onto the user message verbatim — the wire shape is the\n * persisted `ChatFilePart`/`ChatImagePart` vocabulary already. The typed\n * projection is `/chat-store`'s (same boundary as the assistant hop). */\nfunction userPartsWithFiles(\n userParts: Array<Record<string, unknown>>,\n fileParts: ChatTurnFilePartInput[],\n): ChatMessagePart[] {\n return toChatMessageParts([...userParts, ...fileParts.map((part) => ({ ...part }))])\n}\n\n// ── producer-stream wrappers (heartbeat + raw tap) ───────────────────────────\n\n/** Fire `onRawEvent` for each producer event, before the engine frames it.\n * Best-effort — a telemetry throw is logged, never propagated. */\nasync function* tapRawEvents(\n source: AsyncIterable<ChatRouteEvent>,\n onRawEvent: (event: ChatRouteEvent) => void | Promise<void>,\n log: (message: string, meta?: Record<string, unknown>) => void,\n): AsyncGenerator<ChatRouteEvent, void, unknown> {\n for await (const event of source) {\n try {\n await onRawEvent(event)\n } catch (err) {\n log('[chat-routes] onRawEvent failed', { error: err instanceof Error ? err.message : String(err) })\n }\n yield event\n }\n}\n\n/** Inject a keepalive whenever `intervalMs` elapses with no source event. The\n * silent window (elapsed + tick) resets on every real event, so a producer\n * that keeps emitting never triggers a heartbeat. Closes the source on early\n * return, matching a `for await` over it. */\nasync function* withStreamHeartbeat(\n source: AsyncIterable<ChatRouteEvent>,\n intervalMs: number,\n makeEvent: (info: { elapsedMs: number; tick: number }) => ChatRouteEvent,\n): AsyncGenerator<ChatRouteEvent, void, unknown> {\n const iterator = source[Symbol.asyncIterator]()\n try {\n let pending = iterator.next()\n let windowStart = Date.now()\n let tick = 0\n for (;;) {\n let timer: ReturnType<typeof setTimeout> | undefined\n let winner: 'event' | 'heartbeat'\n try {\n const heartbeat = new Promise<'heartbeat'>((resolve) => {\n timer = setTimeout(() => resolve('heartbeat'), intervalMs)\n })\n winner = await Promise.race([pending.then(() => 'event' as const), heartbeat])\n } finally {\n // Clear the pending timer on EVERY exit — including a `pending`\n // rejection — so a rejected source never orphans a setTimeout that\n // keeps the runtime alive for up to `intervalMs`.\n if (timer !== undefined) clearTimeout(timer)\n }\n if (winner === 'heartbeat') {\n tick += 1\n yield makeEvent({ elapsedMs: Date.now() - windowStart, tick })\n continue\n }\n const result = await pending\n if (result.done) return\n yield result.value\n pending = iterator.next()\n windowStart = Date.now()\n tick = 0\n }\n } finally {\n await iterator.return?.()\n }\n}\n\n// ── the factory ────────────────────────────────────────────────────────────\n\nexport function createChatTurnRoutes<TContext = void>(\n options: CreateChatTurnRoutesOptions<TContext>,\n): ChatTurnRoutes {\n const log = options.log ?? ((message, meta) => console.error(message, meta ?? ''))\n\n async function turn(request: Request, ctx?: { waitUntil?(p: Promise<unknown>): void }): Promise<Response> {\n const [rawBody, badBody] = await parseJsonObjectBody(request)\n if (badBody) return badBody\n\n let parsed: ParsedTurnBody\n try {\n parsed = validateTurnBody(rawBody, options.maxInlinePartBytes)\n } catch (err) {\n if (err instanceof ChatTurnInputError) return errorResponse(err)\n throw err\n }\n const { payload, content, fileParts, turnId } = parsed\n\n const auth = await options.authorize({ request, intent: 'turn', body: payload })\n if (!auth.ok) return auth.response\n const { tenantId, userId, context } = auth\n\n // Turn identity: reuse the just-persisted user row on a retry (same\n // turnId or identical trailing content) instead of double-inserting.\n const existingMessages = (await options.store.listMessages(payload.threadId)).map((m) => ({\n id: m.id,\n role: m.role,\n content: m.content,\n parts: (m.parts ?? null) as PersistedChatMessageForTurn['parts'],\n }))\n const chatTurn = resolveChatTurn({ existingMessages, userContent: content, turnId })\n\n const identity: ChatTurnIdentity = {\n tenantId,\n sessionId: payload.threadId,\n userId,\n turnIndex: chatTurn.turnIndex,\n }\n const executionId = deriveExecutionId({\n projectId: options.projectId,\n sessionId: payload.threadId,\n turnIndex: chatTurn.turnIndex,\n })\n const turnStreamId = crypto.randomUUID()\n\n const prompt: string | ChatTurnPartInput[] =\n fileParts.length === 0\n ? content\n : content\n ? [{ type: 'text', text: content }, ...fileParts]\n : [...fileParts]\n\n // The producer input every pre-turn seam reads (and `beforeTurn` may\n // rewrite). Mutated in place before the producer's deferred first pull.\n let produceArgs: ChatTurnProduceArgs<TContext> = {\n request,\n body: payload,\n identity,\n context,\n prompt,\n executionId,\n turnStreamId,\n priorMessages: chatTurn.priorMessages,\n }\n\n // Single-flight lock: acquire before any side effect. `release` runs\n // exactly once — in the drain's `finally` on a normal turn, or right here\n // on a short-circuit / throw.\n let lockAcquired = false\n let lockHandle: unknown\n let lockReleased = false\n const releaseLock = async (): Promise<void> => {\n if (!lockAcquired || lockReleased) return\n lockReleased = true\n try {\n await options.turnLock!.release(lockHandle)\n } catch (err) {\n log('[chat-routes] turnLock.release failed', {\n turnId: turnStreamId,\n error: err instanceof Error ? err.message : String(err),\n })\n }\n }\n if (options.turnLock) {\n const acquired = await options.turnLock.acquire(produceArgs)\n if (!acquired.acquired) return acquired.response\n lockAcquired = true\n lockHandle = acquired.handle\n }\n\n // Turn state, hoisted so the pre-stream `catch` can settle the lifecycle\n // (fire `onTurnError`, close the span) even when a seam throws\n // synchronously before the drain — the drain would otherwise be the only\n // path that runs the terminal hook.\n let producer: ChatTurnRouteProducer | undefined\n let runFailed = false\n // Data of the event that marked the run failed — handed to `onTurnError`\n // when no drain throw supplies a richer cause.\n let lastFailureData: Record<string, unknown> | undefined\n let turnStartedAtMs = 0\n let turnStarted = false\n let lifecycleSettled = false\n\n // Exactly one terminal lifecycle hook, after the turn settles (idempotent).\n // Failure is this route's own verdict (`runFailed` from error/failed\n // events, or a drain/sync throw), not the engine's envelope.\n const fireTerminalLifecycle = async (failed: boolean, terminalError: unknown): Promise<void> => {\n if (lifecycleSettled) return\n lifecycleSettled = true\n const lifecycle = options.lifecycle\n if (!lifecycle) return\n const durationMs = Date.now() - turnStartedAtMs\n try {\n if (failed) {\n await lifecycle.onTurnError?.({\n identity, executionId, turnStreamId, context, durationMs,\n error: terminalError ?? lastFailureData ?? new Error('chat turn failed'),\n })\n } else {\n await lifecycle.onTurnComplete?.({\n identity, executionId, turnStreamId, context, durationMs,\n finalText: producer?.finalText() ?? '',\n usage: producer?.usage?.() ?? {},\n })\n }\n } catch (err) {\n log('[chat-routes] lifecycle terminal hook failed', {\n turnId: turnStreamId,\n error: err instanceof Error ? err.message : String(err),\n })\n }\n }\n\n try {\n // The product (via `authorize`) may suppress the user-row insert for a\n // dispatched/synthetic turn. AND-composition: it can only subtract, never\n // resurrect a turn the engine already deduped as a retry.\n const insertUserMessage = chatTurn.shouldInsertUserMessage && (auth.insertUserMessage ?? true)\n if (insertUserMessage) {\n await options.store.appendMessage({\n threadId: payload.threadId,\n role: 'user',\n content,\n parts: userPartsWithFiles(chatTurn.userParts, fileParts),\n })\n }\n\n // Domain-readiness gate: may short-circuit with the product's own\n // response before the producer runs. The user row above is kept (a real\n // user turn); the gate's response is the assistant side of it.\n if (options.contextGate) {\n const gate = await options.contextGate(produceArgs)\n if (!gate.proceed) {\n await releaseLock()\n return gate.response\n }\n }\n\n // Observe + optionally augment the assembled producer input.\n if (options.beforeTurn) {\n const patch = await options.beforeTurn(produceArgs)\n if (patch) produceArgs = { ...produceArgs, ...patch }\n }\n\n // Durability tap: every engine event buffers (coalesced) so a dropped\n // client replays the tail. Live delivery rides the Response body, not the\n // tap, so `write` is intentionally absent.\n const tap = createBufferedTurnTap({\n store: options.turnStore,\n turnId: turnStreamId,\n scopeId: payload.threadId,\n coalesce: options.coalesceTurnEvents ?? coalesceDeltas,\n })\n const turnMarker = { type: 'turn', turnId: turnStreamId }\n await tap.onEvent(turnMarker)\n\n turnStartedAtMs = Date.now()\n turnStarted = true\n if (options.lifecycle?.onTurnStart) {\n try {\n await options.lifecycle.onTurnStart({\n identity, executionId, turnStreamId, context, startedAt: turnStartedAtMs,\n })\n } catch (err) {\n log('[chat-routes] lifecycle.onTurnStart failed', {\n turnId: turnStreamId,\n error: err instanceof Error ? err.message : String(err),\n })\n }\n }\n\n const result = handleChatTurn({\n identity,\n waitUntil: ctx?.waitUntil,\n log,\n hooks: {\n // The engine wants a synchronous producer; box resolution is async —\n // defer it into the generator's first pull.\n produce: () => ({\n stream: (async function* () {\n producer = await options.produce(produceArgs)\n let source: AsyncIterable<ChatRouteEvent> = producer.stream\n if (options.onRawEvent) {\n source = tapRawEvents(source, (event) => options.onRawEvent!(event, context), log)\n }\n if (options.heartbeat) {\n source = withStreamHeartbeat(source, options.heartbeat.intervalMs, options.heartbeat.event)\n }\n for await (const event of source) yield event\n })(),\n finalText: () => producer?.finalText() ?? '',\n }),\n onEvent: async (event) => {\n if (event.type === 'session.run.failed' || event.type === 'error') {\n runFailed = true\n lastFailureData = event.data\n }\n await tap.onEvent(event)\n if (options.onEvent) await options.onEvent(event, context)\n },\n ...(options.transformFinalText ? { transformFinalText: options.transformFinalText } : {}),\n persistAssistantMessage: async ({ finalText }) => {\n // The typed boundary: stream-normalizer records → stored vocabulary\n // (validating projection owned by /chat-store — no cast here). The\n // scalar `finalText` arrives already transformed by the engine; the\n // producer's text PARTS are raw, so the same transform must run over\n // each text segment before persistence or a redaction (legal PII)\n // leaks at rest through message.parts.\n const rawParts = producer?.assistantParts ? producer.assistantParts() : undefined\n const projected =\n rawParts && options.transformFinalText\n ? await Promise.all(\n rawParts.map(async (part) =>\n String((part as { type?: unknown }).type ?? '') === 'text'\n ? { ...part, text: await options.transformFinalText!(String((part as { text?: unknown }).text ?? '')) }\n : part,\n ),\n )\n : rawParts\n const parts = projected ? toChatMessageParts(projected) : undefined\n if (!finalText.trim() && (!parts || parts.length === 0)) return\n const usage = producer?.usage?.() ?? {}\n await options.store.appendMessage({\n threadId: payload.threadId,\n role: 'assistant',\n content: finalText,\n ...(parts && parts.length > 0 ? { parts } : {}),\n ...(producer?.model ? { model: producer.model } : {}),\n ...(usage.inputTokens !== undefined ? { inputTokens: usage.inputTokens } : {}),\n ...(usage.outputTokens !== undefined ? { outputTokens: usage.outputTokens } : {}),\n ...(usage.reasoningTokens !== undefined ? { reasoningTokens: usage.reasoningTokens } : {}),\n ...(usage.cacheReadTokens !== undefined ? { cacheReadTokens: usage.cacheReadTokens } : {}),\n ...(usage.cacheWriteTokens !== undefined ? { cacheWriteTokens: usage.cacheWriteTokens } : {}),\n ...(usage.costUsd !== undefined ? { costUsd: usage.costUsd } : {}),\n })\n },\n ...(options.onTurnComplete\n ? {\n // Wired into the engine's completion hook, which fires only when\n // the stream ended without throwing. A terminal error EVENT\n // (not a throw) still lands here — so surface `runFailed` so the\n // product skips billing an errored turn instead of marking it\n // complete with empty text.\n onTurnComplete: ({ identity: turnIdentity, finalText }: { identity: ChatTurnIdentity; finalText: string }) =>\n options.onTurnComplete!({\n identity: turnIdentity,\n finalText,\n context,\n failed: runFailed,\n ...(runFailed ? { failureReason: failureReasonOf(lastFailureData) } : {}),\n }),\n }\n : {}),\n ...(options.traceFlush ? { traceFlush: () => options.traceFlush!(context) } : {}),\n },\n })\n\n // Tee: one branch to the live client, one drained under waitUntil so the\n // turn (and its buffering via onEvent) runs to completion after a client\n // drop — the engine body executes as it is pulled.\n const [clientBody, drainBody] = result.body.tee()\n const drained = (async () => {\n const reader = drainBody.getReader()\n let drainError: unknown\n try {\n for (;;) {\n const { done } = await reader.read()\n if (done) break\n }\n } catch (err) {\n drainError = err\n log('[chat-routes] turn drain failed', {\n turnId: turnStreamId,\n error: err instanceof Error ? err.message : String(err),\n })\n }\n const failed = runFailed || drainError !== undefined\n try {\n await tap.done(failed ? 'error' : 'complete')\n } catch (err) {\n log('[chat-routes] turn buffer finalize failed', {\n turnId: turnStreamId,\n error: err instanceof Error ? err.message : String(err),\n })\n }\n await fireTerminalLifecycle(failed, drainError)\n await releaseLock()\n })()\n if (ctx?.waitUntil) ctx.waitUntil(drained)\n else void drained.catch(() => {})\n\n // Announce the replay handle before the engine's first event.\n const encoder = new TextEncoder()\n const marker = new ReadableStream<Uint8Array>({\n start(controller) {\n controller.enqueue(encoder.encode(`${JSON.stringify(turnMarker)}\\n`))\n controller.close()\n },\n })\n const body = concatStreams([marker, clientBody])\n\n return new Response(body, {\n headers: {\n 'Content-Type': result.contentType,\n 'Cache-Control': 'no-cache',\n },\n })\n } catch (err) {\n // A throw before the turn began streaming (user-insert, gate, beforeTurn,\n // lifecycle-start, tap setup, engine construction, tee). If the turn had\n // already started, settle the lifecycle with `onTurnError` (close the\n // span) — the drain never ran to do it. Then release the lock, propagate.\n if (turnStarted) await fireTerminalLifecycle(true, err)\n await releaseLock()\n throw err\n }\n }\n\n async function replay(request: Request, params: { turnId: string }): Promise<Response> {\n const turnId = params.turnId?.trim()\n if (!turnId) return Response.json({ error: 'Missing turnId' }, { status: 400 })\n const auth = await options.authorize({ request, intent: 'replay', turnId })\n if (!auth.ok) return auth.response\n\n const fromSeqRaw = new URL(request.url).searchParams.get('fromSeq')\n const fromSeq = fromSeqRaw ? Math.max(0, Math.trunc(Number(fromSeqRaw)) || 0) : 0\n\n const encoder = new TextEncoder()\n const events = replayTurnEvents({\n store: options.turnStore,\n turnId,\n fromSeq,\n ...(options.replay?.pollMs !== undefined ? { pollMs: options.replay.pollMs } : {}),\n ...(options.replay?.timeoutMs !== undefined ? { timeoutMs: options.replay.timeoutMs } : {}),\n })\n const body = new ReadableStream<Uint8Array>({\n async pull(controller) {\n const { done, value } = await events.next()\n if (done) {\n controller.close()\n return\n }\n controller.enqueue(encoder.encode(`${value.event}\\n`))\n },\n cancel() {\n void events.return(undefined)\n },\n })\n return new Response(body, {\n headers: {\n 'Content-Type': 'application/x-ndjson',\n 'Cache-Control': 'no-cache',\n },\n })\n }\n\n async function running(request: Request): Promise<Response> {\n const threadId = new URL(request.url).searchParams.get('threadId')?.trim()\n if (!threadId) return Response.json({ error: 'Missing threadId' }, { status: 400 })\n const auth = await options.authorize({ request, intent: 'running', threadId })\n if (!auth.ok) return auth.response\n // `listRunning` is optional on the store; a store that cannot enumerate\n // running turns simply reports none — the client falls back to the persisted\n // transcript, never to a hang.\n const ids = (await options.turnStore.listRunning?.(threadId)) ?? []\n return Response.json({ running: ids })\n }\n\n return {\n turn,\n replay,\n running,\n interactions: options.interactions ? createInteractionAnswerRoute(options.interactions) : null,\n }\n}\n\n/** Sequential concat of byte streams (marker line, then the engine body). */\nfunction concatStreams(streams: ReadableStream<Uint8Array>[]): ReadableStream<Uint8Array> {\n let index = 0\n let reader: ReadableStreamDefaultReader<Uint8Array> | null = null\n return new ReadableStream<Uint8Array>({\n async pull(controller) {\n for (;;) {\n if (!reader) {\n const next = streams[index++]\n if (!next) {\n controller.close()\n return\n }\n reader = next.getReader()\n }\n const { done, value } = await reader.read()\n if (done) {\n reader = null\n continue\n }\n controller.enqueue(value)\n return\n }\n },\n async cancel(reason) {\n await reader?.cancel(reason)\n for (const stream of streams.slice(index)) await stream.cancel(reason)\n },\n })\n}\n","/**\n * Sandbox lane: bridge a raw sandbox event stream (`streamSandboxPrompt`) into\n * the `ChatTurnProducer` shape agent-runtime's `handleChatTurn` consumes AND\n * the client vocabulary `/web-react`'s `dispatchChatStreamLine` already parses\n * (`text` / `reasoning` / `tool_call` / `tool_result` / `usage` /\n * `interaction`). Legal and tax each hand-rolled this mapping differently;\n * this is that middle, composed from `/stream`'s normalizers — no new loop\n * logic, no SDK import (the event source is an injected `AsyncIterable`).\n *\n * Alongside the live mapping it accumulates the PERSISTED projection — the\n * `message.parts` rows `/chat-store` stores — via `normalizePersistedPart` /\n * `mergePersistedPart` / `finalizeAssistantParts`, plus the usage receipt from\n * `step-finish` parts. `createChatTurnRoutes` reads both after drain.\n */\n\nimport {\n cancelStatusFor,\n interactionPartKey,\n interactionToPersistedPart,\n isRenderableInteractionKind,\n parseInteractionCancel,\n parseInteractionRequest,\n} from '../interactions/contract'\nimport {\n parsePlanSubmittedEvent,\n planToPersistedPart,\n} from '../plans/index'\nimport {\n asRecord,\n asString,\n finalizeAssistantParts,\n getPartKey,\n mergePersistedPart,\n normalizePersistedPart,\n normalizeToolEvent,\n type JsonRecord,\n type StreamEvent,\n} from '../stream/index'\nimport type { ChatTurnRouteProducer, ChatTurnUsage } from './turn-routes'\n\nexport interface SandboxChatProducerOptions {\n /** The raw sandbox event stream (e.g. `streamSandboxPrompt(...)`). */\n events: AsyncIterable<unknown>\n /** Recorded on the persisted assistant message. */\n model?: string\n /** Which ask kinds the product renders a card for. Anything else is\n * auto-declined (see `declineInteraction`) so the run never hangs in the\n * broker waiting on a card no client will show. Default: question/plan. */\n isRenderableInteraction?: (kind: string) => boolean\n /** Resolve a non-renderable ask (wire `respondToSessionInteraction` with the\n * session's sidecar connection). Without it, non-renderable asks are only\n * logged — the run stays blocked until the broker times out. */\n declineInteraction?: (id: string) => Promise<void>\n log?: (message: string, meta?: Record<string, unknown>) => void\n}\n\ninterface TextTracker {\n /** Full accumulated text per part key, to derive suffix deltas from\n * snapshot-only harness events. */\n seen: Map<string, string>\n}\n\n/** Delta to emit for one text/reasoning part update: prefer the harness's\n * explicit delta; otherwise diff the snapshot against what was already\n * emitted for that part (snapshot-only harnesses re-send the whole text). */\nfunction textDelta(tracker: TextTracker, key: string, part: JsonRecord, rawDelta: unknown): string {\n const explicit = typeof rawDelta === 'string' ? rawDelta : undefined\n const previous = tracker.seen.get(key) ?? ''\n if (explicit !== undefined) {\n tracker.seen.set(key, previous + explicit)\n return explicit\n }\n const snapshot = asString(part.text) ?? asString(part.content) ?? ''\n if (!snapshot) return ''\n if (snapshot.startsWith(previous)) {\n tracker.seen.set(key, snapshot)\n return snapshot.slice(previous.length)\n }\n // The snapshot replaced the text outright — emit it whole; the persisted\n // projection stays correct because finalText is authoritative at finalize.\n tracker.seen.set(key, snapshot)\n return snapshot\n}\n\nfunction usageFromStepFinish(part: JsonRecord, usage: ChatTurnUsage): void {\n const tokens = asRecord(part.tokens)\n if (tokens) {\n const cache = asRecord(tokens.cache)\n const add = (current: number | undefined, value: unknown): number | undefined => {\n const n = Number(value)\n if (!Number.isFinite(n)) return current\n return (current ?? 0) + n\n }\n usage.inputTokens = add(usage.inputTokens, tokens.input)\n usage.outputTokens = add(usage.outputTokens, tokens.output)\n usage.reasoningTokens = add(usage.reasoningTokens, tokens.reasoning)\n if (cache) {\n usage.cacheReadTokens = add(usage.cacheReadTokens, cache.read)\n usage.cacheWriteTokens = add(usage.cacheWriteTokens, cache.write)\n }\n }\n const cost = Number(part.cost)\n if (Number.isFinite(cost)) usage.costUsd = (usage.costUsd ?? 0) + cost\n}\n\nexport function createSandboxChatProducer(options: SandboxChatProducerOptions): ChatTurnRouteProducer {\n const log = options.log ?? ((message, meta) => console.error(message, meta ?? ''))\n const renderable = options.isRenderableInteraction ?? isRenderableInteractionKind\n\n let fullText = ''\n const partOrder: string[] = []\n const partMap = new Map<string, JsonRecord>()\n const tracker: TextTracker = { seen: new Map() }\n const usage: ChatTurnUsage = {}\n /** Tool ids already announced as `tool_call` / settled as `tool_result`. */\n const announcedTools = new Set<string>()\n const settledTools = new Set<string>()\n /** Id-less step boundaries: one occurrence per key, never merged. */\n let stepCounter = 0\n\n function recordPersistedPart(part: JsonRecord, delta: string | undefined, keyOverride?: string): void {\n const persisted = normalizePersistedPart(part)\n if (!persisted) return\n const key = keyOverride ?? getPartKey(persisted)\n if (!partMap.has(key)) partOrder.push(key)\n partMap.set(key, mergePersistedPart(partMap.get(key), persisted, delta))\n }\n\n async function* stream(): AsyncGenerator<StreamEvent, void, unknown> {\n for await (const raw of options.events) {\n const record = asRecord(raw)\n if (!record || typeof record.type !== 'string') continue\n // Fold bare tool_call/tool_result shapes into the canonical part event;\n // everything else keeps its original record (verbatim forwarding must\n // not strip fields outside `data`).\n const normalized = normalizeToolEvent({ type: record.type, data: asRecord(record.data) })\n const event = normalized.type === 'message.part.updated' ? normalized : (record as unknown as StreamEvent)\n\n if (event.type === 'message.part.updated') {\n const part = asRecord(event.data?.part)\n if (!part) continue\n const rawDelta = event.data?.delta\n const partType = String(part.type ?? '')\n\n if (partType === 'text' || partType === 'reasoning') {\n const key = getPartKey(part)\n const delta = textDelta(tracker, key, part, rawDelta)\n recordPersistedPart(part, delta || undefined)\n if (delta) {\n if (partType === 'text') fullText += delta\n yield { type: partType, text: delta } as StreamEvent & { text: string }\n }\n continue\n }\n\n if (partType === 'tool') {\n recordPersistedPart(part, undefined)\n const persisted = partMap.get(getPartKey(part))\n const state = asRecord(persisted?.state)\n const toolId = String(persisted?.id ?? '')\n const toolName = String(persisted?.tool ?? 'tool')\n if (toolId && !announcedTools.has(toolId)) {\n announcedTools.add(toolId)\n yield {\n type: 'tool_call',\n call: { toolCallId: toolId, toolName, args: asRecord(state?.input) ?? {} },\n } as StreamEvent\n }\n const status = String(state?.status ?? '')\n if (toolId && (status === 'completed' || status === 'error') && !settledTools.has(toolId)) {\n settledTools.add(toolId)\n yield {\n type: 'tool_result',\n toolCallId: toolId,\n toolName,\n outcome: {\n ok: status === 'completed',\n ...(state?.output !== undefined ? { result: state.output } : {}),\n ...(asString(state?.error) ? { message: asString(state?.error) } : {}),\n },\n } as StreamEvent\n }\n continue\n }\n\n if (partType === 'step-finish') {\n usageFromStepFinish(part, usage)\n // Persist the per-step receipt too (unique key per occurrence: the\n // parts have no id and two receipts must never merge into one).\n recordPersistedPart(part, undefined, `step-finish:#${stepCounter++}`)\n const promptTokens = usage.inputTokens ?? 0\n const completionTokens = usage.outputTokens ?? 0\n if (promptTokens || completionTokens) {\n yield { type: 'usage', usage: { promptTokens, completionTokens } } as StreamEvent\n }\n continue\n }\n\n if (partType === 'step-start') {\n recordPersistedPart(part, undefined, `step-start:#${stepCounter}`)\n continue\n }\n\n // Remaining storable kinds (file/image/subtask) have no live\n // vocabulary line; they persist so the transcript keeps them.\n recordPersistedPart(part, undefined)\n continue\n }\n\n if (event.type === 'interaction') {\n const parsed = parseInteractionRequest(asRecord(record.data))\n if (!parsed.succeeded) {\n log('[chat-routes] dropping malformed interaction event', { error: parsed.error })\n continue\n }\n if (renderable(parsed.value.kind)) {\n recordPersistedPart(\n interactionToPersistedPart(parsed.value, 'pending'),\n undefined,\n interactionPartKey(parsed.value.id),\n )\n yield event\n continue\n }\n // Non-renderable ask: the run is blocked in the broker until someone\n // answers. Decline it so the turn proceeds instead of hanging.\n if (options.declineInteraction) {\n try {\n await options.declineInteraction(parsed.value.id)\n } catch (err) {\n log('[chat-routes] failed to auto-decline interaction', {\n id: parsed.value.id,\n error: err instanceof Error ? err.message : String(err),\n })\n }\n } else {\n log('[chat-routes] non-renderable interaction with no declineInteraction wired', {\n id: parsed.value.id,\n kind: parsed.value.kind,\n })\n }\n continue\n }\n\n if (event.type === 'interaction.cancel') {\n const parsed = parseInteractionCancel(asRecord(record.data))\n if (!parsed.succeeded) {\n log('[chat-routes] dropping malformed interaction.cancel event', { error: parsed.error })\n continue\n }\n const key = interactionPartKey(parsed.value.id)\n const existing = partMap.get(key)\n if (existing?.type === 'interaction' && existing.status === 'pending') {\n recordPersistedPart({\n ...existing,\n status: cancelStatusFor(parsed.value.reason),\n ...(parsed.value.reason ? { cancelReason: parsed.value.reason } : {}),\n }, undefined, key)\n }\n yield event\n continue\n }\n\n if (event.type === 'plan.submitted') {\n const parsed = parsePlanSubmittedEvent(record)\n if (!parsed.succeeded) {\n log('[chat-routes] dropping malformed plan.submitted event', { error: parsed.error })\n continue\n }\n recordPersistedPart(planToPersistedPart(parsed.value), undefined)\n yield event\n continue\n }\n\n if (event.type === 'result') {\n const finalText = asString(event.data?.finalText)\n if (finalText) fullText = finalText\n const resultUsage = asRecord(event.data?.usage)\n if (resultUsage) {\n const input = Number(resultUsage.inputTokens)\n const output = Number(resultUsage.outputTokens)\n if (Number.isFinite(input)) usage.inputTokens = input\n if (Number.isFinite(output)) usage.outputTokens = output\n }\n continue\n }\n\n // Everything else (error, lifecycle) forwards\n // verbatim — the client parser ignores unknown types.\n yield event\n }\n\n }\n\n return {\n stream: stream(),\n finalText: () => fullText,\n assistantParts: () => finalizeAssistantParts(partOrder, partMap, fullText),\n usage: () => usage,\n ...(options.model ? { model: options.model } : {}),\n }\n}\n","import { getPartKey, mergePersistedPart, type StreamEvent } from '../stream/index'\nimport type { ChatTurnRouteProducer } from './turn-routes'\n\nexport interface ChatRouteDurableProjection {\n observe(event: unknown): void | Promise<void>\n materialize(): Array<Record<string, unknown>> | Promise<Array<Record<string, unknown>>>\n}\n\nexport type ChatRouteDurableProjectionLogger =\n (message: string, meta?: Record<string, unknown>) => void\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error)\n}\n\n/** Adds durable lifecycle projection to any producer lane without moving its\n * transport into agent-app. The projection is observed inline and its\n * materialized parts replace same-key pending snapshots after the stream\n * drains. Projection persistence is best-effort for the live lane: a store\n * outage must not terminate an otherwise healthy sandbox stream. Failures are\n * reported through the optional logger so products can retain diagnostics. */\nexport function withDurableChatProjection(\n producer: ChatTurnRouteProducer,\n projection: ChatRouteDurableProjection,\n log: ChatRouteDurableProjectionLogger = (message, meta) => console.error(message, meta ?? ''),\n): ChatTurnRouteProducer {\n let projected: Array<Record<string, unknown>> = []\n async function* stream(): AsyncGenerator<StreamEvent, void, unknown> {\n for await (const event of producer.stream) {\n try {\n await projection.observe(event)\n } catch (error) {\n log('[chat-routes] durable projection observe failed', {\n eventType: event.type,\n error: errorMessage(error),\n })\n }\n yield event\n }\n try {\n projected = await projection.materialize()\n } catch (error) {\n log('[chat-routes] durable projection materialize failed', {\n error: errorMessage(error),\n })\n }\n }\n return {\n ...producer,\n stream: stream(),\n assistantParts: () => {\n const parts = producer.assistantParts?.() ?? []\n const order: string[] = []\n const byKey = new Map<string, Record<string, unknown>>()\n for (const part of [...parts, ...projected]) {\n const key = getPartKey(part)\n if (!byKey.has(key)) order.push(key)\n byKey.set(key, mergePersistedPart(byKey.get(key), part))\n }\n return order.map((key) => byKey.get(key)!)\n },\n }\n}\n","/**\n * `createUploadRoute` — the multimodal middle. Accepts multipart file uploads\n * and returns `PromptInputPart`-shaped descriptors the client echoes back on\n * send (`ChatTurnRequestPayload.parts`):\n *\n * ≤ inlineMaxBytes (700 KiB default) → inline `data:` URI part — rides the\n * turn body directly, no sandbox round trip.\n * > inlineMaxBytes → written into the sandbox workspace (base64 through the\n * structural `write` seam — `box.fs` satisfies it) and referenced by\n * `path`. Mandatory two-step: the gateway caps request bodies at ~1 MiB,\n * so a large file can never ride the prompt POST.\n *\n * The sink is structural (no sandbox-SDK import); products pass `box.fs`.\n *\n * @remarks Sole consumer today is the `--chat` scaffold (`create-agent-app\n * --chat` → `template-chat/src/chat.ts`), the reference multimodal path. The\n * fleet apps (gtm/tax/legal/insurance) each keep their OWN upload route into a\n * durable vault (KV, or AES-GCM-encrypted R2) — a different persistence model\n * from this route's inline-`data:`-or-ephemeral-sandbox-workspace split, so\n * they don't (and shouldn't) route through it. This stays the scaffold's proven\n * upload pattern, not a fleet primitive; keep that distinction in mind before\n * widening its surface.\n */\n\nimport type { ChatTurnFilePartInput } from './wire'\n\n/** 700 KiB: base64 inflates ~4/3, so an inline part stays comfortably under\n * the ~1 MiB gateway body cap alongside the JSON envelope. */\nexport const UPLOAD_INLINE_MAX_BYTES = 700 * 1024\n\n/** 8 MiB default ceiling per file — one base64 `write` call handles it. Raise\n * it only with a sink that can take the bigger single write. */\nexport const UPLOAD_MAX_FILE_BYTES = 8 * 1024 * 1024\n\n/** Structural match of the sandbox SDK's `box.fs` write surface (v0.10.5+:\n * `encoding: 'base64'` is the worker-safe binary path). */\nexport interface SandboxUploadSink {\n write(path: string, content: string, options?: { encoding?: 'utf8' | 'base64' }): Promise<unknown>\n}\n\nexport type UploadAuthorization =\n | {\n ok: true\n /** Where large files land. Absent/null: only inline uploads are\n * accepted and an over-inline-cap file is rejected with 413. */\n sink?: SandboxUploadSink | null\n /** Per-request override of the workspace directory large files go to. */\n uploadDir?: string\n }\n | { ok: false; response: Response }\n\nexport interface CreateUploadRouteOptions {\n /** Authenticate the caller and resolve the sandbox file sink (usually\n * `ensureWorkspaceSandbox(...)` → `box.fs`). */\n authorize(args: { request: Request }): Promise<UploadAuthorization>\n /** Inline-vs-sandbox threshold. Default {@link UPLOAD_INLINE_MAX_BYTES}. */\n inlineMaxBytes?: number\n /** Hard per-file cap. Default {@link UPLOAD_MAX_FILE_BYTES}. */\n maxFileBytes?: number\n /** Workspace directory for path-ref files. Default `'uploads'`. */\n uploadDir?: string\n}\n\n/** One uploaded file, ready for the composer chip and the turn body. */\nexport interface UploadedChatFile {\n id: string\n name: string\n size: number\n mediaType: string\n /** True when the part carries the bytes inline (`data:` URI). */\n inline: boolean\n /** Echo this back verbatim in `ChatTurnRequestPayload.parts`. */\n part: ChatTurnFilePartInput\n}\n\n/** Path-safe file name: basename only, conservative charset, length-capped. */\nexport function sanitizeUploadFilename(name: string): string {\n const base = name.split(/[\\\\/]/).pop() ?? 'file'\n const safe = base.replace(/[^A-Za-z0-9._-]+/g, '_').replace(/^\\.+/, '_')\n return (safe || 'file').slice(0, 120)\n}\n\nconst BASE64_CHUNK = 0x8000\n\nexport function bytesToBase64(bytes: Uint8Array): string {\n let binary = ''\n for (let offset = 0; offset < bytes.length; offset += BASE64_CHUNK) {\n binary += String.fromCharCode(...bytes.subarray(offset, offset + BASE64_CHUNK))\n }\n return btoa(binary)\n}\n\nfunction uploadError(status: number, code: string, error: string): Response {\n return Response.json({ code, error }, { status })\n}\n\nexport function createUploadRoute(options: CreateUploadRouteOptions): (request: Request) => Promise<Response> {\n const inlineMaxBytes = options.inlineMaxBytes ?? UPLOAD_INLINE_MAX_BYTES\n const maxFileBytes = options.maxFileBytes ?? UPLOAD_MAX_FILE_BYTES\n\n return async function upload(request: Request): Promise<Response> {\n const auth = await options.authorize({ request })\n if (!auth.ok) return auth.response\n const sink = auth.sink ?? null\n const uploadDir = (auth.uploadDir ?? options.uploadDir ?? 'uploads').replace(/\\/+$/, '')\n\n let form: FormData\n try {\n form = await request.formData()\n } catch {\n return uploadError(400, 'INVALID_UPLOAD', 'Expected a multipart/form-data body with file fields')\n }\n const files: File[] = []\n form.forEach((value) => {\n if (value instanceof File) files.push(value)\n })\n if (files.length === 0) {\n return uploadError(400, 'INVALID_UPLOAD', 'No files in the upload body')\n }\n\n const uploaded: UploadedChatFile[] = []\n for (const file of files) {\n const name = sanitizeUploadFilename(file.name)\n const mediaType = file.type || 'application/octet-stream'\n const partType: ChatTurnFilePartInput['type'] = mediaType.startsWith('image/') ? 'image' : 'file'\n\n if (file.size > maxFileBytes) {\n return uploadError(\n 413,\n 'FILE_TOO_LARGE',\n `${name} is ${file.size}B, over the ${maxFileBytes}B per-file cap`,\n )\n }\n\n const id = crypto.randomUUID()\n if (file.size <= inlineMaxBytes) {\n const base64 = bytesToBase64(new Uint8Array(await file.arrayBuffer()))\n uploaded.push({\n id,\n name,\n size: file.size,\n mediaType,\n inline: true,\n part: {\n type: partType,\n filename: name,\n mediaType,\n url: `data:${mediaType};base64,${base64}`,\n },\n })\n continue\n }\n\n if (!sink) {\n return uploadError(\n 413,\n 'SANDBOX_REQUIRED',\n `${name} is ${file.size}B, over the ${inlineMaxBytes}B inline cap, and no sandbox is available to hold it`,\n )\n }\n const path = `${uploadDir}/${id}-${name}`\n const base64 = bytesToBase64(new Uint8Array(await file.arrayBuffer()))\n await sink.write(path, base64, { encoding: 'base64' })\n uploaded.push({\n id,\n name,\n size: file.size,\n mediaType,\n inline: false,\n part: { type: partType, filename: name, mediaType, path },\n })\n }\n\n return Response.json({ files: uploaded })\n }\n}\n"],"mappings":"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","names":["base64"]}
1
+ {"version":3,"sources":["../../src/chat-routes/turn-routes.ts","../../src/chat-routes/stale-turn-lock.ts","../../src/chat-routes/sandbox-producer.ts","../../src/chat-routes/durable-projection.ts","../../src/chat-routes/upload.ts"],"sourcesContent":["/**\n * `createChatTurnRoutes` — the assembled server chat vertical (issue #188\n * Phase 1). One factory composing the pieces every product re-wired by hand:\n *\n * body parse/validate → `/web` `parseJsonObjectBody` + `./wire`\n * turn identity → `/stream` `resolveChatTurn` + agent-runtime\n * `deriveExecutionId`\n * producer → injected seam (sandbox lane via\n * `createSandboxChatProducer`; router lane is the\n * product's own `ChatTurnProducer`)\n * turn engine → agent-runtime `handleChatTurn` (verbatim)\n * durability → `/stream` turn-buffer tap, wired BY DEFAULT\n * (tee + drain keeps the turn running after a\n * client drop; replay serves the buffered tail)\n * persistence → injected `/chat-store`-shaped store\n * (user row on send, assistant row on completion)\n * interactions answer → `/interactions` `createInteractionAnswerRoute`\n *\n * Handlers are web-standard `Request → Response` (Workers, Node 18+, Deno) —\n * no router import. Auth/access is one injected `authorize` seam, composable\n * with `/app-auth` guards but not coupled to them.\n *\n * Six optional product seams let a complex turn-orchestrator compose the\n * vertical instead of hand-rolling a generator — each omittable to the exact\n * behavior above: `turnLock` (single-flight acquire/release around the turn),\n * `contextGate` (pre-producer domain-readiness short-circuit), `beforeTurn`\n * (observe + augment the producer input), `lifecycle` (deterministic\n * start/complete/error telemetry), `heartbeat` (keepalive during silent\n * producer waits), plus `onRawEvent` (the raw producer events, for telemetry).\n * `handleChatTurn` stays the engine — the seams only wrap its input, its\n * producer stream, and its settle.\n *\n * Seam stability: `lifecycle` and `heartbeat` are generic and stable.\n * `turnLock`, `contextGate`, `beforeTurn`, and `onRawEvent` are `@experimental`\n * — proven by a single consumer (gtm's chat vertical, #200) and may change once\n * a second consumer exercises them. They stay FLAT top-level options (not\n * grouped under a `hooks` object): that grouping would break the shipped\n * consumer's call for no mechanism gain, and this package's exports are\n * additive-only.\n */\n\nimport { deriveExecutionId, handleChatTurn } from '@tangle-network/agent-runtime'\nimport type { ChatTurnIdentity, ChatTurnProducer } from '@tangle-network/agent-runtime'\nimport { mentionInputToPart, toChatMessageParts, type ChatMessagePart } from '../chat-store/parts'\nimport {\n createInteractionAnswerRoute,\n type InteractionAnswerRoute,\n type InteractionAnswerRouteOptions,\n} from '../interactions/route'\nimport {\n coalesceDeltas,\n createBufferedTurnTap,\n normalizeClientTurnId,\n replayTurnEvents,\n resolveChatTurn,\n type PersistedChatMessageForTurn,\n type TurnEventStore,\n} from '../stream/index'\nimport { parseJsonObjectBody } from '../web/index'\nimport {\n assertPromptPartsWithinCap,\n ChatTurnInputError,\n parseChatTurnParts,\n parseFileMentions,\n type ChatTurnFilePartInput,\n type ChatTurnPartInput,\n type ChatTurnRequestPayload,\n type FileMention,\n} from './wire'\n\n// ── seams ───────────────────────────────────────────────────────────────────\n\n/** Usage receipt persisted onto the assistant message (the flattened\n * `step-finish` shape `/chat-store`'s columns mirror). */\nexport interface ChatTurnUsage {\n inputTokens?: number\n outputTokens?: number\n reasoningTokens?: number\n cacheReadTokens?: number\n cacheWriteTokens?: number\n costUsd?: number\n}\n\n/** What the route persists — a structural subset of `/chat-store`'s\n * `ChatStore`, so `createChatStore(db, tables)` satisfies it directly and a\n * product with its own persistence adapts without importing drizzle. */\nexport interface ChatTurnMessageStore {\n listMessages(threadId: string): Promise<Array<{\n id: string\n role: 'user' | 'assistant' | 'system' | 'tool'\n content: string\n parts?: ChatMessagePart[] | null\n }>>\n appendMessage(input: {\n threadId: string\n role: 'user' | 'assistant'\n content: string\n parts?: ChatMessagePart[]\n model?: string | null\n inputTokens?: number | null\n outputTokens?: number | null\n reasoningTokens?: number | null\n cacheReadTokens?: number | null\n cacheWriteTokens?: number | null\n costUsd?: number | null\n }): Promise<unknown>\n}\n\n/** `ChatTurnProducer` plus the persisted projection the assembly reads after\n * drain. `createSandboxChatProducer` returns this; a router-lane producer\n * may omit the optional members (finalText persists as a single text part). */\nexport interface ChatTurnRouteProducer extends ChatTurnProducer {\n assistantParts?(): Array<Record<string, unknown>>\n usage?(): ChatTurnUsage\n model?: string\n}\n\nexport type ChatTurnAuthorization<TContext> =\n | {\n ok: true\n tenantId: string\n userId: string\n context: TContext\n /** When `false`, skip the `role:'user'` message insert for this turn — for\n * a product-dispatched / synthetic turn (e.g. a follow-up the product\n * raised itself) that must not surface a new user row. Composes with —\n * never overrides — the engine's retry-dedup: `authorize` runs before\n * turn identity is resolved, so it cannot tell a retry from a fresh turn;\n * a turn already deduped stays deduped. Omit / `true` → today's behavior.\n * @experimental Single-consumer; shape may change. */\n insertUserMessage?: boolean\n }\n | { ok: false; response: Response }\n\nexport interface ChatTurnAuthorizeArgs {\n request: Request\n intent: 'turn' | 'replay' | 'running'\n /** Parsed, validated POST body (turn intent only). */\n body?: ChatTurnRequestPayload\n /** The buffered turn id being replayed (replay intent only). */\n turnId?: string\n /** The thread whose running turns are being discovered (running intent only). */\n threadId?: string\n}\n\nexport interface ChatTurnProduceArgs<TContext> {\n request: Request\n body: ChatTurnRequestPayload\n identity: ChatTurnIdentity\n context: TContext\n /** The message to send: plain text, or parts when the client attached\n * files (a text part is prepended from `content` when present). */\n prompt: string | ChatTurnPartInput[]\n /** Stable id for cross-process reconnect (`deriveExecutionId`). */\n executionId: string\n /** The turn-buffer id announced to the client for replay. */\n turnStreamId: string\n priorMessages: PersistedChatMessageForTurn[]\n}\n\n/** One event as it crosses the route: the producer's own vocabulary, or an\n * injected keepalive. Same shape the engine forwards verbatim. */\ntype ChatRouteEvent = { type: string; data?: Record<string, unknown> }\n\n/** Best-effort human-readable cause from a terminal `error` /\n * `session.run.failed` event's `data`. */\nfunction failureReasonOf(data: Record<string, unknown> | undefined): string | undefined {\n if (!data) return undefined\n const message = data.message ?? data.error ?? data.reason\n if (typeof message === 'string' && message.length > 0) return message\n return undefined\n}\n\n/** Keepalive emitted while the producer is quiet (long tool calls, first-token\n * wait) so client watchdogs stay re-armed. One is emitted each time\n * `intervalMs` elapses with no producer event; the window resets on every real\n * event, so a chatty producer never triggers one. The product owns the event\n * shape (`type` + `data`). Omit → no keepalives (today's behavior). */\nexport interface ChatTurnHeartbeat {\n intervalMs: number\n event(info: { elapsedMs: number; tick: number }): ChatRouteEvent\n}\n\n/** Patch a `beforeTurn` hook returns to augment the producer's input. Omitted\n * fields keep the route-assembled value; the product's `produce` still owns\n * the system prompt. */\nexport interface ChatTurnInputPatch {\n prompt?: string | ChatTurnPartInput[]\n priorMessages?: PersistedChatMessageForTurn[]\n}\n\n/** Pre-turn readiness verdict — proceed, or short-circuit with the product's\n * own `Response` (e.g. a canned assistant reply asking for missing context).\n * Distinct from `authorize`: this gates domain readiness, not access. */\nexport type ChatTurnGateResult =\n | { proceed: true }\n | { proceed: false; response: Response }\n\n/** Single-flight lock verdict — acquired (with an opaque handle passed back to\n * `release`), or already held (short-circuit with the product's 409-style\n * `Response`). */\nexport type ChatTurnLockResult =\n | { acquired: true; handle?: unknown }\n | { acquired: false; response: Response }\n\n/** Async acquire/release wrapped around the turn. `acquire` runs before any\n * side effect; `release` runs once when the turn settles — including on a\n * short-circuit or a throw. */\nexport interface ChatTurnLock<TContext> {\n acquire(args: ChatTurnProduceArgs<TContext>): ChatTurnLockResult | Promise<ChatTurnLockResult>\n release(handle: unknown): void | Promise<void>\n}\n\ninterface ChatTurnLifecycleBase<TContext> {\n identity: ChatTurnIdentity\n executionId: string\n turnStreamId: string\n context: TContext\n}\nexport interface ChatTurnLifecycleStart<TContext> extends ChatTurnLifecycleBase<TContext> {\n startedAt: number\n}\nexport interface ChatTurnLifecycleComplete<TContext> extends ChatTurnLifecycleBase<TContext> {\n finalText: string\n usage: ChatTurnUsage\n durationMs: number\n}\nexport interface ChatTurnLifecycleError<TContext> extends ChatTurnLifecycleBase<TContext> {\n error: unknown\n durationMs: number\n}\n\n/** Deterministic run telemetry: `onTurnStart` fires before the producer runs;\n * exactly one of `onTurnComplete` / `onTurnError` fires after the turn\n * settles, always after `onTurnStart`. Failure is derived from the turn's own\n * `error` / `session.run.failed` events (or a drain throw), not the engine's\n * lifecycle envelope. Hook errors are swallowed — telemetry never fails a\n * turn. */\nexport interface ChatTurnLifecycle<TContext> {\n onTurnStart?(info: ChatTurnLifecycleStart<TContext>): void | Promise<void>\n onTurnComplete?(info: ChatTurnLifecycleComplete<TContext>): void | Promise<void>\n onTurnError?(info: ChatTurnLifecycleError<TContext>): void | Promise<void>\n}\n\nexport interface CreateChatTurnRoutesOptions<TContext = void> {\n /** Names the product in `deriveExecutionId` so retries land on the same\n * substrate execution. */\n projectId: string\n /** Authenticate + authorize the caller for a turn or a replay. The only\n * product-supplied access step: session auth, thread/workspace access,\n * seat/balance gates, rate limits all live here. */\n authorize(args: ChatTurnAuthorizeArgs): Promise<ChatTurnAuthorization<TContext>>\n /** Thread/message persistence (`/chat-store`'s store or a product adapter). */\n store: ChatTurnMessageStore\n /** Turn-event buffer (`createD1TurnEventStore(env.DB)` in production,\n * `createMemoryTurnEventStore()` in tests). Wired by default — every turn\n * is buffered and replayable. */\n turnStore: TurnEventStore\n /** Build the turn's event stream. Sandbox lane: `streamSandboxPrompt(...)`\n * wrapped in `createSandboxChatProducer`. Router/openai-compat lane: the\n * product's own producer. May be async (box resolution). */\n produce(args: ChatTurnProduceArgs<TContext>): ChatTurnRouteProducer | Promise<ChatTurnRouteProducer>\n /** Single-flight lock acquired before any side effect and released once when\n * the turn settles (including short-circuit/throw). Omit → no lock.\n * @experimental Single-consumer (gtm, #200); shape may change. */\n turnLock?: ChatTurnLock<TContext>\n /** Pre-turn readiness gate that can short-circuit with a product `Response`\n * before the producer runs (the user row is already persisted). Runs after\n * `turnLock.acquire`, before `beforeTurn`. Omit → always proceed.\n * @experimental Single-consumer (gtm, #200); shape may change. */\n contextGate?(args: ChatTurnProduceArgs<TContext>): ChatTurnGateResult | Promise<ChatTurnGateResult>\n /** Observe the assembled producer input and optionally augment it (rewrite\n * the prompt / prior messages) before the producer runs. Omit → no change.\n * @experimental Single-consumer (gtm, #200); shape may change. */\n beforeTurn?(args: ChatTurnProduceArgs<TContext>): ChatTurnInputPatch | void | Promise<ChatTurnInputPatch | void>\n /** Deterministic run telemetry (start / complete / error) with identity and\n * timing. Omit → no telemetry. */\n lifecycle?: ChatTurnLifecycle<TContext>\n /** Keepalive injected while the producer is quiet. Omit → no keepalives. */\n heartbeat?: ChatTurnHeartbeat\n /** Observe each event the producer emits, before the engine frames it and\n * before any heartbeat injection (the raw sidecar-producer events, for\n * telemetry). Never alters the stream; errors are swallowed. Distinct from\n * `onEvent`, which sees the engine-framed stream incl. lifecycle envelopes.\n * @experimental Single-consumer (gtm, #200); shape may change. */\n onRawEvent?(event: ChatRouteEvent, context: TContext): void | Promise<void>\n /** Pre-persist transform of the final text (e.g. `/redact`'s `redactPII`).\n * Live stream is never altered. */\n transformFinalText?(text: string): string | Promise<string>\n /** Post-processing after a turn settles (billing, titles, audit). Fires with\n * `failed:true` + `failureReason` when the turn carried a terminal error\n * event (model 402 / rate-limit / server error) instead of a clean\n * completion, so products skip the deduct and render an error row rather\n * than billing an empty turn and marking it done. A turn that THROWS never\n * reaches this hook (the engine skips it on a producer throw). Errors are\n * swallowed by the engine — they never fail a streamed turn. */\n onTurnComplete?(input: {\n identity: ChatTurnIdentity\n finalText: string\n context: TContext\n failed: boolean\n failureReason?: string\n }): Promise<void>\n /** Per-event side channel (product broadcast). The turn-buffer tap is\n * already wired; this runs in addition. */\n onEvent?(event: { type: string; data?: Record<string, unknown> }, context: TContext): void | Promise<void>\n /** Trace flush handed to `waitUntil` (OTLP export). */\n traceFlush?(context: TContext): Promise<void>\n /** Compose the interaction-answer endpoints (`/interactions`). Omit when the\n * product has no sidecar ask channel. */\n interactions?: InteractionAnswerRouteOptions\n /** Byte budget for inline prompt parts. Default `INLINE_PARTS_MAX_BYTES`. */\n maxInlinePartBytes?: number\n /** Per-flush coalescer for the turn buffer. Default `coalesceDeltas` (this\n * assembly streams the client vocabulary's `{type:'text'|'reasoning',\n * text}` lines, which it merges). A producer streaming raw\n * `message.part.updated` events passes `coalesceChatStreamEvents`. */\n coalesceTurnEvents?: (events: unknown[]) => unknown[]\n replay?: { pollMs?: number; timeoutMs?: number }\n log?: (message: string, meta?: Record<string, unknown>) => void\n}\n\nexport interface ChatTurnRoutes {\n /** POST — run one turn, streaming NDJSON. First line is\n * `{type:'turn', turnId}` (the replay handle); the rest is the engine's\n * event protocol. Pass the platform's `waitUntil` so the turn keeps\n * running (and buffering) after a client disconnect. */\n turn(request: Request, ctx?: { waitUntil?(p: Promise<unknown>): void }): Promise<Response>\n /** GET — replay a buffered turn from `?fromSeq=` (0 = everything), then\n * follow it live until it completes. */\n replay(request: Request, params: { turnId: string }): Promise<Response>\n /** GET `?threadId=` — the reconnect-discovery endpoint: the turn ids still\n * running on a thread, so a client that reloaded mid-turn can re-attach to\n * the live stream via {@link replay} instead of losing it. Returns `[]` when\n * the turn store cannot enumerate running turns (`listRunning` unimplemented). */\n running(request: Request): Promise<Response>\n /** list/answer endpoints from `/interactions`; null when not configured. */\n interactions: InteractionAnswerRoute | null\n}\n\n// ── body validation ────────────────────────────────────────────────────────\n\nfunction errorResponse(err: ChatTurnInputError): Response {\n return Response.json({ code: err.code, error: err.message }, { status: err.status })\n}\n\ninterface ParsedTurnBody {\n payload: ChatTurnRequestPayload\n content: string\n fileParts: ChatTurnFilePartInput[]\n mentions: FileMention[]\n turnId: string | undefined\n}\n\nfunction validateTurnBody(body: Record<string, unknown>, maxInlinePartBytes: number | undefined): ParsedTurnBody {\n const threadId = typeof body.threadId === 'string' ? body.threadId.trim() : ''\n if (!threadId) throw new ChatTurnInputError('Missing threadId')\n const rawContent = body.content ?? body.message ?? ''\n if (typeof rawContent !== 'string') throw new ChatTurnInputError('content must be a string')\n const content = rawContent.trim()\n const fileParts = parseChatTurnParts(body.parts)\n // Path references, not bytes — validated for traversal/charset/count, never\n // counted against the inline-parts byte budget below.\n const mentions = parseFileMentions(body.mentions)\n // A mention is a turn's whole payload often enough to count: \"@chart.png\"\n // with no prose is a real ask, and the pointer block the mentions produce is\n // prompt content the model reads.\n if (!content && fileParts.length === 0 && mentions.length === 0) {\n throw new ChatTurnInputError('Missing content (send text, parts, mentions, or any combination)')\n }\n assertPromptPartsWithinCap(fileParts, maxInlinePartBytes)\n let turnId: string | undefined\n try {\n turnId = normalizeClientTurnId(body.turnId)\n } catch (err) {\n throw new ChatTurnInputError(err instanceof Error ? err.message : 'Invalid turnId')\n }\n return {\n // The VALIDATED, deduped mention list replaces the raw one on the payload,\n // so every downstream seam (`authorize`, `contextGate`, `beforeTurn`,\n // `produce`) reads checked paths and never the request's own.\n payload: { ...body, threadId, content, mentions } as ChatTurnRequestPayload,\n content,\n fileParts,\n mentions,\n turnId,\n }\n}\n\n/** File parts persist onto the user message verbatim — the wire shape is the\n * persisted `ChatFilePart`/`ChatImagePart` vocabulary already. Mentions get\n * the one mapping step their own vocabulary needs. The typed projection is\n * `/chat-store`'s (same boundary as the assistant hop).\n *\n * Mentions persist as parts rather than being folded into the prompt because\n * the prompt is not readable back: a retry rebuilds the turn from the stored\n * row, and a transcript draws its pills from it. Turning them INTO prompt\n * text stays the product's job — only the product knows how to resolve a\n * workspace-relative path to an in-box one (`fileMentionsToParts`'\n * `resolvePath` seam), so the route never dispatches them itself. */\nfunction userPartsWithFiles(\n userParts: Array<Record<string, unknown>>,\n fileParts: ChatTurnFilePartInput[],\n mentions: FileMention[],\n): ChatMessagePart[] {\n return toChatMessageParts([\n ...userParts,\n ...fileParts.map((part) => ({ ...part })),\n ...mentions.map((mention) => ({ ...mentionInputToPart(mention) })),\n ])\n}\n\n// ── producer-stream wrappers (heartbeat + raw tap) ───────────────────────────\n\n/** Fire `onRawEvent` for each producer event, before the engine frames it.\n * Best-effort — a telemetry throw is logged, never propagated. */\nasync function* tapRawEvents(\n source: AsyncIterable<ChatRouteEvent>,\n onRawEvent: (event: ChatRouteEvent) => void | Promise<void>,\n log: (message: string, meta?: Record<string, unknown>) => void,\n): AsyncGenerator<ChatRouteEvent, void, unknown> {\n for await (const event of source) {\n try {\n await onRawEvent(event)\n } catch (err) {\n log('[chat-routes] onRawEvent failed', { error: err instanceof Error ? err.message : String(err) })\n }\n yield event\n }\n}\n\n/** Inject a keepalive whenever `intervalMs` elapses with no source event. The\n * silent window (elapsed + tick) resets on every real event, so a producer\n * that keeps emitting never triggers a heartbeat. Closes the source on early\n * return, matching a `for await` over it. */\nasync function* withStreamHeartbeat(\n source: AsyncIterable<ChatRouteEvent>,\n intervalMs: number,\n makeEvent: (info: { elapsedMs: number; tick: number }) => ChatRouteEvent,\n): AsyncGenerator<ChatRouteEvent, void, unknown> {\n const iterator = source[Symbol.asyncIterator]()\n try {\n let pending = iterator.next()\n let windowStart = Date.now()\n let tick = 0\n for (;;) {\n let timer: ReturnType<typeof setTimeout> | undefined\n let winner: 'event' | 'heartbeat'\n try {\n const heartbeat = new Promise<'heartbeat'>((resolve) => {\n timer = setTimeout(() => resolve('heartbeat'), intervalMs)\n })\n winner = await Promise.race([pending.then(() => 'event' as const), heartbeat])\n } finally {\n // Clear the pending timer on EVERY exit — including a `pending`\n // rejection — so a rejected source never orphans a setTimeout that\n // keeps the runtime alive for up to `intervalMs`.\n if (timer !== undefined) clearTimeout(timer)\n }\n if (winner === 'heartbeat') {\n tick += 1\n yield makeEvent({ elapsedMs: Date.now() - windowStart, tick })\n continue\n }\n const result = await pending\n if (result.done) return\n yield result.value\n pending = iterator.next()\n windowStart = Date.now()\n tick = 0\n }\n } finally {\n await iterator.return?.()\n }\n}\n\n// ── the factory ────────────────────────────────────────────────────────────\n\nexport function createChatTurnRoutes<TContext = void>(\n options: CreateChatTurnRoutesOptions<TContext>,\n): ChatTurnRoutes {\n const log = options.log ?? ((message, meta) => console.error(message, meta ?? ''))\n\n async function turn(request: Request, ctx?: { waitUntil?(p: Promise<unknown>): void }): Promise<Response> {\n const [rawBody, badBody] = await parseJsonObjectBody(request)\n if (badBody) return badBody\n\n let parsed: ParsedTurnBody\n try {\n parsed = validateTurnBody(rawBody, options.maxInlinePartBytes)\n } catch (err) {\n if (err instanceof ChatTurnInputError) return errorResponse(err)\n throw err\n }\n const { payload, content, fileParts, mentions, turnId } = parsed\n\n const auth = await options.authorize({ request, intent: 'turn', body: payload })\n if (!auth.ok) return auth.response\n const { tenantId, userId, context } = auth\n\n // Turn identity: reuse the just-persisted user row on a retry (same\n // turnId or identical trailing content) instead of double-inserting.\n const existingMessages = (await options.store.listMessages(payload.threadId)).map((m) => ({\n id: m.id,\n role: m.role,\n content: m.content,\n parts: (m.parts ?? null) as PersistedChatMessageForTurn['parts'],\n }))\n const chatTurn = resolveChatTurn({ existingMessages, userContent: content, turnId })\n\n const identity: ChatTurnIdentity = {\n tenantId,\n sessionId: payload.threadId,\n userId,\n turnIndex: chatTurn.turnIndex,\n }\n const executionId = deriveExecutionId({\n projectId: options.projectId,\n sessionId: payload.threadId,\n turnIndex: chatTurn.turnIndex,\n })\n const turnStreamId = crypto.randomUUID()\n\n const prompt: string | ChatTurnPartInput[] =\n fileParts.length === 0\n ? content\n : content\n ? [{ type: 'text', text: content }, ...fileParts]\n : [...fileParts]\n\n // The producer input every pre-turn seam reads (and `beforeTurn` may\n // rewrite). Mutated in place before the producer's deferred first pull.\n let produceArgs: ChatTurnProduceArgs<TContext> = {\n request,\n body: payload,\n identity,\n context,\n prompt,\n executionId,\n turnStreamId,\n priorMessages: chatTurn.priorMessages,\n }\n\n // Single-flight lock: acquire before any side effect. `release` runs\n // exactly once — in the drain's `finally` on a normal turn, or right here\n // on a short-circuit / throw.\n let lockAcquired = false\n let lockHandle: unknown\n let lockReleased = false\n const releaseLock = async (): Promise<void> => {\n if (!lockAcquired || lockReleased) return\n lockReleased = true\n try {\n await options.turnLock!.release(lockHandle)\n } catch (err) {\n log('[chat-routes] turnLock.release failed', {\n turnId: turnStreamId,\n error: err instanceof Error ? err.message : String(err),\n })\n }\n }\n if (options.turnLock) {\n const acquired = await options.turnLock.acquire(produceArgs)\n if (!acquired.acquired) return acquired.response\n lockAcquired = true\n lockHandle = acquired.handle\n }\n\n // Turn state, hoisted so the pre-stream `catch` can settle the lifecycle\n // (fire `onTurnError`, close the span) even when a seam throws\n // synchronously before the drain — the drain would otherwise be the only\n // path that runs the terminal hook.\n let producer: ChatTurnRouteProducer | undefined\n let runFailed = false\n // Data of the event that marked the run failed — handed to `onTurnError`\n // when no drain throw supplies a richer cause.\n let lastFailureData: Record<string, unknown> | undefined\n let turnStartedAtMs = 0\n let turnStarted = false\n let lifecycleSettled = false\n\n // Exactly one terminal lifecycle hook, after the turn settles (idempotent).\n // Failure is this route's own verdict (`runFailed` from error/failed\n // events, or a drain/sync throw), not the engine's envelope.\n const fireTerminalLifecycle = async (failed: boolean, terminalError: unknown): Promise<void> => {\n if (lifecycleSettled) return\n lifecycleSettled = true\n const lifecycle = options.lifecycle\n if (!lifecycle) return\n const durationMs = Date.now() - turnStartedAtMs\n try {\n if (failed) {\n await lifecycle.onTurnError?.({\n identity, executionId, turnStreamId, context, durationMs,\n error: terminalError ?? lastFailureData ?? new Error('chat turn failed'),\n })\n } else {\n await lifecycle.onTurnComplete?.({\n identity, executionId, turnStreamId, context, durationMs,\n finalText: producer?.finalText() ?? '',\n usage: producer?.usage?.() ?? {},\n })\n }\n } catch (err) {\n log('[chat-routes] lifecycle terminal hook failed', {\n turnId: turnStreamId,\n error: err instanceof Error ? err.message : String(err),\n })\n }\n }\n\n try {\n // The product (via `authorize`) may suppress the user-row insert for a\n // dispatched/synthetic turn. AND-composition: it can only subtract, never\n // resurrect a turn the engine already deduped as a retry.\n const insertUserMessage = chatTurn.shouldInsertUserMessage && (auth.insertUserMessage ?? true)\n if (insertUserMessage) {\n await options.store.appendMessage({\n threadId: payload.threadId,\n role: 'user',\n content,\n parts: userPartsWithFiles(chatTurn.userParts, fileParts, mentions),\n })\n }\n\n // Domain-readiness gate: may short-circuit with the product's own\n // response before the producer runs. The user row above is kept (a real\n // user turn); the gate's response is the assistant side of it.\n if (options.contextGate) {\n const gate = await options.contextGate(produceArgs)\n if (!gate.proceed) {\n await releaseLock()\n return gate.response\n }\n }\n\n // Observe + optionally augment the assembled producer input.\n if (options.beforeTurn) {\n const patch = await options.beforeTurn(produceArgs)\n if (patch) produceArgs = { ...produceArgs, ...patch }\n }\n\n // Durability tap: every engine event buffers (coalesced) so a dropped\n // client replays the tail. Live delivery rides the Response body, not the\n // tap, so `write` is intentionally absent.\n const tap = createBufferedTurnTap({\n store: options.turnStore,\n turnId: turnStreamId,\n scopeId: payload.threadId,\n coalesce: options.coalesceTurnEvents ?? coalesceDeltas,\n })\n const turnMarker = { type: 'turn', turnId: turnStreamId }\n await tap.onEvent(turnMarker)\n\n turnStartedAtMs = Date.now()\n turnStarted = true\n if (options.lifecycle?.onTurnStart) {\n try {\n await options.lifecycle.onTurnStart({\n identity, executionId, turnStreamId, context, startedAt: turnStartedAtMs,\n })\n } catch (err) {\n log('[chat-routes] lifecycle.onTurnStart failed', {\n turnId: turnStreamId,\n error: err instanceof Error ? err.message : String(err),\n })\n }\n }\n\n const result = handleChatTurn({\n identity,\n waitUntil: ctx?.waitUntil,\n log,\n hooks: {\n // The engine wants a synchronous producer; box resolution is async —\n // defer it into the generator's first pull.\n produce: () => ({\n stream: (async function* () {\n producer = await options.produce(produceArgs)\n let source: AsyncIterable<ChatRouteEvent> = producer.stream\n if (options.onRawEvent) {\n source = tapRawEvents(source, (event) => options.onRawEvent!(event, context), log)\n }\n if (options.heartbeat) {\n source = withStreamHeartbeat(source, options.heartbeat.intervalMs, options.heartbeat.event)\n }\n for await (const event of source) yield event\n })(),\n finalText: () => producer?.finalText() ?? '',\n }),\n onEvent: async (event) => {\n if (event.type === 'session.run.failed' || event.type === 'error') {\n runFailed = true\n lastFailureData = event.data\n }\n await tap.onEvent(event)\n if (options.onEvent) await options.onEvent(event, context)\n },\n ...(options.transformFinalText ? { transformFinalText: options.transformFinalText } : {}),\n persistAssistantMessage: async ({ finalText }) => {\n // The typed boundary: stream-normalizer records → stored vocabulary\n // (validating projection owned by /chat-store — no cast here). The\n // scalar `finalText` arrives already transformed by the engine; the\n // producer's text PARTS are raw, so the same transform must run over\n // each text segment before persistence or a redaction (legal PII)\n // leaks at rest through message.parts.\n const rawParts = producer?.assistantParts ? producer.assistantParts() : undefined\n const projected =\n rawParts && options.transformFinalText\n ? await Promise.all(\n rawParts.map(async (part) =>\n String((part as { type?: unknown }).type ?? '') === 'text'\n ? { ...part, text: await options.transformFinalText!(String((part as { text?: unknown }).text ?? '')) }\n : part,\n ),\n )\n : rawParts\n const parts = projected ? toChatMessageParts(projected) : undefined\n if (!finalText.trim() && (!parts || parts.length === 0)) return\n const usage = producer?.usage?.() ?? {}\n await options.store.appendMessage({\n threadId: payload.threadId,\n role: 'assistant',\n content: finalText,\n ...(parts && parts.length > 0 ? { parts } : {}),\n ...(producer?.model ? { model: producer.model } : {}),\n ...(usage.inputTokens !== undefined ? { inputTokens: usage.inputTokens } : {}),\n ...(usage.outputTokens !== undefined ? { outputTokens: usage.outputTokens } : {}),\n ...(usage.reasoningTokens !== undefined ? { reasoningTokens: usage.reasoningTokens } : {}),\n ...(usage.cacheReadTokens !== undefined ? { cacheReadTokens: usage.cacheReadTokens } : {}),\n ...(usage.cacheWriteTokens !== undefined ? { cacheWriteTokens: usage.cacheWriteTokens } : {}),\n ...(usage.costUsd !== undefined ? { costUsd: usage.costUsd } : {}),\n })\n },\n ...(options.onTurnComplete\n ? {\n // Wired into the engine's completion hook, which fires only when\n // the stream ended without throwing. A terminal error EVENT\n // (not a throw) still lands here — so surface `runFailed` so the\n // product skips billing an errored turn instead of marking it\n // complete with empty text.\n onTurnComplete: ({ identity: turnIdentity, finalText }: { identity: ChatTurnIdentity; finalText: string }) =>\n options.onTurnComplete!({\n identity: turnIdentity,\n finalText,\n context,\n failed: runFailed,\n ...(runFailed ? { failureReason: failureReasonOf(lastFailureData) } : {}),\n }),\n }\n : {}),\n ...(options.traceFlush ? { traceFlush: () => options.traceFlush!(context) } : {}),\n },\n })\n\n // Tee: one branch to the live client, one drained under waitUntil so the\n // turn (and its buffering via onEvent) runs to completion after a client\n // drop — the engine body executes as it is pulled.\n const [clientBody, drainBody] = result.body.tee()\n const drained = (async () => {\n const reader = drainBody.getReader()\n let drainError: unknown\n try {\n for (;;) {\n const { done } = await reader.read()\n if (done) break\n }\n } catch (err) {\n drainError = err\n log('[chat-routes] turn drain failed', {\n turnId: turnStreamId,\n error: err instanceof Error ? err.message : String(err),\n })\n }\n const failed = runFailed || drainError !== undefined\n try {\n await tap.done(failed ? 'error' : 'complete')\n } catch (err) {\n log('[chat-routes] turn buffer finalize failed', {\n turnId: turnStreamId,\n error: err instanceof Error ? err.message : String(err),\n })\n }\n await fireTerminalLifecycle(failed, drainError)\n await releaseLock()\n })()\n if (ctx?.waitUntil) ctx.waitUntil(drained)\n else void drained.catch(() => {})\n\n // Announce the replay handle before the engine's first event.\n const encoder = new TextEncoder()\n const marker = new ReadableStream<Uint8Array>({\n start(controller) {\n controller.enqueue(encoder.encode(`${JSON.stringify(turnMarker)}\\n`))\n controller.close()\n },\n })\n const body = concatStreams([marker, clientBody])\n\n return new Response(body, {\n headers: {\n 'Content-Type': result.contentType,\n 'Cache-Control': 'no-cache',\n },\n })\n } catch (err) {\n // A throw before the turn began streaming (user-insert, gate, beforeTurn,\n // lifecycle-start, tap setup, engine construction, tee). If the turn had\n // already started, settle the lifecycle with `onTurnError` (close the\n // span) — the drain never ran to do it. Then release the lock, propagate.\n if (turnStarted) await fireTerminalLifecycle(true, err)\n await releaseLock()\n throw err\n }\n }\n\n async function replay(request: Request, params: { turnId: string }): Promise<Response> {\n const turnId = params.turnId?.trim()\n if (!turnId) return Response.json({ error: 'Missing turnId' }, { status: 400 })\n const auth = await options.authorize({ request, intent: 'replay', turnId })\n if (!auth.ok) return auth.response\n\n const fromSeqRaw = new URL(request.url).searchParams.get('fromSeq')\n const fromSeq = fromSeqRaw ? Math.max(0, Math.trunc(Number(fromSeqRaw)) || 0) : 0\n\n const encoder = new TextEncoder()\n const events = replayTurnEvents({\n store: options.turnStore,\n turnId,\n fromSeq,\n ...(options.replay?.pollMs !== undefined ? { pollMs: options.replay.pollMs } : {}),\n ...(options.replay?.timeoutMs !== undefined ? { timeoutMs: options.replay.timeoutMs } : {}),\n })\n const body = new ReadableStream<Uint8Array>({\n async pull(controller) {\n const { done, value } = await events.next()\n if (done) {\n controller.close()\n return\n }\n controller.enqueue(encoder.encode(`${value.event}\\n`))\n },\n cancel() {\n void events.return(undefined)\n },\n })\n return new Response(body, {\n headers: {\n 'Content-Type': 'application/x-ndjson',\n 'Cache-Control': 'no-cache',\n },\n })\n }\n\n async function running(request: Request): Promise<Response> {\n const threadId = new URL(request.url).searchParams.get('threadId')?.trim()\n if (!threadId) return Response.json({ error: 'Missing threadId' }, { status: 400 })\n const auth = await options.authorize({ request, intent: 'running', threadId })\n if (!auth.ok) return auth.response\n // `listRunning` is optional on the store; a store that cannot enumerate\n // running turns simply reports none — the client falls back to the persisted\n // transcript, never to a hang.\n const ids = (await options.turnStore.listRunning?.(threadId)) ?? []\n return Response.json({ running: ids })\n }\n\n return {\n turn,\n replay,\n running,\n interactions: options.interactions ? createInteractionAnswerRoute(options.interactions) : null,\n }\n}\n\n/** Sequential concat of byte streams (marker line, then the engine body). */\nfunction concatStreams(streams: ReadableStream<Uint8Array>[]): ReadableStream<Uint8Array> {\n let index = 0\n let reader: ReadableStreamDefaultReader<Uint8Array> | null = null\n return new ReadableStream<Uint8Array>({\n async pull(controller) {\n for (;;) {\n if (!reader) {\n const next = streams[index++]\n if (!next) {\n controller.close()\n return\n }\n reader = next.getReader()\n }\n const { done, value } = await reader.read()\n if (done) {\n reader = null\n continue\n }\n controller.enqueue(value)\n return\n }\n },\n async cancel(reason) {\n await reader?.cancel(reason)\n for (const stream of streams.slice(index)) await stream.cancel(reason)\n },\n })\n}\n","/**\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","/**\n * Sandbox lane: bridge a raw sandbox event stream (`streamSandboxPrompt`) into\n * the `ChatTurnProducer` shape agent-runtime's `handleChatTurn` consumes AND\n * the client vocabulary `/web-react`'s `dispatchChatStreamLine` already parses\n * (`text` / `reasoning` / `tool_call` / `tool_result` / `usage` /\n * `interaction`). Legal and tax each hand-rolled this mapping differently;\n * this is that middle, composed from `/stream`'s normalizers — no new loop\n * logic, no SDK import (the event source is an injected `AsyncIterable`).\n *\n * Alongside the live mapping it accumulates the PERSISTED projection — the\n * `message.parts` rows `/chat-store` stores — via `normalizePersistedPart` /\n * `mergePersistedPart` / `finalizeAssistantParts`, plus the usage receipt from\n * `step-finish` parts. `createChatTurnRoutes` reads both after drain.\n */\n\nimport {\n cancelStatusFor,\n interactionPartKey,\n interactionToPersistedPart,\n isRenderableInteractionKind,\n parseInteractionCancel,\n parseInteractionRequest,\n} from '../interactions/contract'\nimport {\n parsePlanSubmittedEvent,\n planToPersistedPart,\n} from '../plans/index'\nimport {\n asRecord,\n asString,\n finalizeAssistantParts,\n getPartKey,\n mergePersistedPart,\n normalizePersistedPart,\n normalizeToolEvent,\n type JsonRecord,\n type StreamEvent,\n} from '../stream/index'\nimport type { ChatTurnRouteProducer, ChatTurnUsage } from './turn-routes'\n\nexport interface SandboxChatProducerOptions {\n /** The raw sandbox event stream (e.g. `streamSandboxPrompt(...)`). */\n events: AsyncIterable<unknown>\n /** Recorded on the persisted assistant message. */\n model?: string\n /** Which ask kinds the product renders a card for. Anything else is\n * auto-declined (see `declineInteraction`) so the run never hangs in the\n * broker waiting on a card no client will show. Default: question/plan. */\n isRenderableInteraction?: (kind: string) => boolean\n /** Resolve a non-renderable ask (wire `respondToSessionInteraction` with the\n * session's sidecar connection). Without it, non-renderable asks are only\n * logged — the run stays blocked until the broker times out. */\n declineInteraction?: (id: string) => Promise<void>\n log?: (message: string, meta?: Record<string, unknown>) => void\n}\n\ninterface TextTracker {\n /** Full accumulated text per part key, to derive suffix deltas from\n * snapshot-only harness events. */\n seen: Map<string, string>\n}\n\n/** Delta to emit for one text/reasoning part update: prefer the harness's\n * explicit delta; otherwise diff the snapshot against what was already\n * emitted for that part (snapshot-only harnesses re-send the whole text). */\nfunction textDelta(tracker: TextTracker, key: string, part: JsonRecord, rawDelta: unknown): string {\n const explicit = typeof rawDelta === 'string' ? rawDelta : undefined\n const previous = tracker.seen.get(key) ?? ''\n if (explicit !== undefined) {\n tracker.seen.set(key, previous + explicit)\n return explicit\n }\n const snapshot = asString(part.text) ?? asString(part.content) ?? ''\n if (!snapshot) return ''\n if (snapshot.startsWith(previous)) {\n tracker.seen.set(key, snapshot)\n return snapshot.slice(previous.length)\n }\n // The snapshot replaced the text outright — emit it whole; the persisted\n // projection stays correct because finalText is authoritative at finalize.\n tracker.seen.set(key, snapshot)\n return snapshot\n}\n\nfunction usageFromStepFinish(part: JsonRecord, usage: ChatTurnUsage): void {\n const tokens = asRecord(part.tokens)\n if (tokens) {\n const cache = asRecord(tokens.cache)\n const add = (current: number | undefined, value: unknown): number | undefined => {\n const n = Number(value)\n if (!Number.isFinite(n)) return current\n return (current ?? 0) + n\n }\n usage.inputTokens = add(usage.inputTokens, tokens.input)\n usage.outputTokens = add(usage.outputTokens, tokens.output)\n usage.reasoningTokens = add(usage.reasoningTokens, tokens.reasoning)\n if (cache) {\n usage.cacheReadTokens = add(usage.cacheReadTokens, cache.read)\n usage.cacheWriteTokens = add(usage.cacheWriteTokens, cache.write)\n }\n }\n const cost = Number(part.cost)\n if (Number.isFinite(cost)) usage.costUsd = (usage.costUsd ?? 0) + cost\n}\n\nexport function createSandboxChatProducer(options: SandboxChatProducerOptions): ChatTurnRouteProducer {\n const log = options.log ?? ((message, meta) => console.error(message, meta ?? ''))\n const renderable = options.isRenderableInteraction ?? isRenderableInteractionKind\n\n let fullText = ''\n const partOrder: string[] = []\n const partMap = new Map<string, JsonRecord>()\n const tracker: TextTracker = { seen: new Map() }\n const usage: ChatTurnUsage = {}\n /** Tool ids already announced as `tool_call` / settled as `tool_result`. */\n const announcedTools = new Set<string>()\n const settledTools = new Set<string>()\n /** Id-less step boundaries: one occurrence per key, never merged. */\n let stepCounter = 0\n\n function recordPersistedPart(part: JsonRecord, delta: string | undefined, keyOverride?: string): void {\n const persisted = normalizePersistedPart(part)\n if (!persisted) return\n const key = keyOverride ?? getPartKey(persisted)\n if (!partMap.has(key)) partOrder.push(key)\n partMap.set(key, mergePersistedPart(partMap.get(key), persisted, delta))\n }\n\n async function* stream(): AsyncGenerator<StreamEvent, void, unknown> {\n for await (const raw of options.events) {\n const record = asRecord(raw)\n if (!record || typeof record.type !== 'string') continue\n // Fold bare tool_call/tool_result shapes into the canonical part event;\n // everything else keeps its original record (verbatim forwarding must\n // not strip fields outside `data`).\n const normalized = normalizeToolEvent({ type: record.type, data: asRecord(record.data) })\n const event = normalized.type === 'message.part.updated' ? normalized : (record as unknown as StreamEvent)\n\n if (event.type === 'message.part.updated') {\n const part = asRecord(event.data?.part)\n if (!part) continue\n const rawDelta = event.data?.delta\n const partType = String(part.type ?? '')\n\n if (partType === 'text' || partType === 'reasoning') {\n const key = getPartKey(part)\n const delta = textDelta(tracker, key, part, rawDelta)\n recordPersistedPart(part, delta || undefined)\n if (delta) {\n if (partType === 'text') fullText += delta\n yield { type: partType, text: delta } as StreamEvent & { text: string }\n }\n continue\n }\n\n if (partType === 'tool') {\n recordPersistedPart(part, undefined)\n const persisted = partMap.get(getPartKey(part))\n const state = asRecord(persisted?.state)\n const toolId = String(persisted?.id ?? '')\n const toolName = String(persisted?.tool ?? 'tool')\n if (toolId && !announcedTools.has(toolId)) {\n announcedTools.add(toolId)\n yield {\n type: 'tool_call',\n call: { toolCallId: toolId, toolName, args: asRecord(state?.input) ?? {} },\n } as StreamEvent\n }\n const status = String(state?.status ?? '')\n if (toolId && (status === 'completed' || status === 'error') && !settledTools.has(toolId)) {\n settledTools.add(toolId)\n yield {\n type: 'tool_result',\n toolCallId: toolId,\n toolName,\n outcome: {\n ok: status === 'completed',\n ...(state?.output !== undefined ? { result: state.output } : {}),\n ...(asString(state?.error) ? { message: asString(state?.error) } : {}),\n },\n } as StreamEvent\n }\n continue\n }\n\n if (partType === 'step-finish') {\n usageFromStepFinish(part, usage)\n // Persist the per-step receipt too (unique key per occurrence: the\n // parts have no id and two receipts must never merge into one).\n recordPersistedPart(part, undefined, `step-finish:#${stepCounter++}`)\n const promptTokens = usage.inputTokens ?? 0\n const completionTokens = usage.outputTokens ?? 0\n if (promptTokens || completionTokens) {\n yield { type: 'usage', usage: { promptTokens, completionTokens } } as StreamEvent\n }\n continue\n }\n\n if (partType === 'step-start') {\n recordPersistedPart(part, undefined, `step-start:#${stepCounter}`)\n continue\n }\n\n // Remaining storable kinds (file/image/subtask) have no live\n // vocabulary line; they persist so the transcript keeps them.\n recordPersistedPart(part, undefined)\n continue\n }\n\n if (event.type === 'interaction') {\n const parsed = parseInteractionRequest(asRecord(record.data))\n if (!parsed.succeeded) {\n log('[chat-routes] dropping malformed interaction event', { error: parsed.error })\n continue\n }\n if (renderable(parsed.value.kind)) {\n recordPersistedPart(\n interactionToPersistedPart(parsed.value, 'pending'),\n undefined,\n interactionPartKey(parsed.value.id),\n )\n yield event\n continue\n }\n // Non-renderable ask: the run is blocked in the broker until someone\n // answers. Decline it so the turn proceeds instead of hanging.\n if (options.declineInteraction) {\n try {\n await options.declineInteraction(parsed.value.id)\n } catch (err) {\n log('[chat-routes] failed to auto-decline interaction', {\n id: parsed.value.id,\n error: err instanceof Error ? err.message : String(err),\n })\n }\n } else {\n log('[chat-routes] non-renderable interaction with no declineInteraction wired', {\n id: parsed.value.id,\n kind: parsed.value.kind,\n })\n }\n continue\n }\n\n if (event.type === 'interaction.cancel') {\n const parsed = parseInteractionCancel(asRecord(record.data))\n if (!parsed.succeeded) {\n log('[chat-routes] dropping malformed interaction.cancel event', { error: parsed.error })\n continue\n }\n const key = interactionPartKey(parsed.value.id)\n const existing = partMap.get(key)\n if (existing?.type === 'interaction' && existing.status === 'pending') {\n recordPersistedPart({\n ...existing,\n status: cancelStatusFor(parsed.value.reason),\n ...(parsed.value.reason ? { cancelReason: parsed.value.reason } : {}),\n }, undefined, key)\n }\n yield event\n continue\n }\n\n if (event.type === 'plan.submitted') {\n const parsed = parsePlanSubmittedEvent(record)\n if (!parsed.succeeded) {\n log('[chat-routes] dropping malformed plan.submitted event', { error: parsed.error })\n continue\n }\n recordPersistedPart(planToPersistedPart(parsed.value), undefined)\n yield event\n continue\n }\n\n if (event.type === 'result') {\n const finalText = asString(event.data?.finalText)\n if (finalText) fullText = finalText\n const resultUsage = asRecord(event.data?.usage)\n if (resultUsage) {\n const input = Number(resultUsage.inputTokens)\n const output = Number(resultUsage.outputTokens)\n if (Number.isFinite(input)) usage.inputTokens = input\n if (Number.isFinite(output)) usage.outputTokens = output\n }\n continue\n }\n\n // Everything else (error, lifecycle) forwards\n // verbatim — the client parser ignores unknown types.\n yield event\n }\n\n }\n\n return {\n stream: stream(),\n finalText: () => fullText,\n assistantParts: () => finalizeAssistantParts(partOrder, partMap, fullText),\n usage: () => usage,\n ...(options.model ? { model: options.model } : {}),\n }\n}\n","import { getPartKey, mergePersistedPart, type StreamEvent } from '../stream/index'\nimport type { ChatTurnRouteProducer } from './turn-routes'\n\nexport interface ChatRouteDurableProjection {\n observe(event: unknown): void | Promise<void>\n materialize(): Array<Record<string, unknown>> | Promise<Array<Record<string, unknown>>>\n}\n\nexport type ChatRouteDurableProjectionLogger =\n (message: string, meta?: Record<string, unknown>) => void\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error)\n}\n\n/** Adds durable lifecycle projection to any producer lane without moving its\n * transport into agent-app. The projection is observed inline and its\n * materialized parts replace same-key pending snapshots after the stream\n * drains. Projection persistence is best-effort for the live lane: a store\n * outage must not terminate an otherwise healthy sandbox stream. Failures are\n * reported through the optional logger so products can retain diagnostics. */\nexport function withDurableChatProjection(\n producer: ChatTurnRouteProducer,\n projection: ChatRouteDurableProjection,\n log: ChatRouteDurableProjectionLogger = (message, meta) => console.error(message, meta ?? ''),\n): ChatTurnRouteProducer {\n let projected: Array<Record<string, unknown>> = []\n async function* stream(): AsyncGenerator<StreamEvent, void, unknown> {\n for await (const event of producer.stream) {\n try {\n await projection.observe(event)\n } catch (error) {\n log('[chat-routes] durable projection observe failed', {\n eventType: event.type,\n error: errorMessage(error),\n })\n }\n yield event\n }\n try {\n projected = await projection.materialize()\n } catch (error) {\n log('[chat-routes] durable projection materialize failed', {\n error: errorMessage(error),\n })\n }\n }\n return {\n ...producer,\n stream: stream(),\n assistantParts: () => {\n const parts = producer.assistantParts?.() ?? []\n const order: string[] = []\n const byKey = new Map<string, Record<string, unknown>>()\n for (const part of [...parts, ...projected]) {\n const key = getPartKey(part)\n if (!byKey.has(key)) order.push(key)\n byKey.set(key, mergePersistedPart(byKey.get(key), part))\n }\n return order.map((key) => byKey.get(key)!)\n },\n }\n}\n","/**\n * `createUploadRoute` — the multimodal middle. Accepts multipart file uploads\n * and returns `PromptInputPart`-shaped descriptors the client echoes back on\n * send (`ChatTurnRequestPayload.parts`):\n *\n * ≤ inlineMaxBytes (700 KiB default) → inline `data:` URI part — rides the\n * turn body directly, no sandbox round trip.\n * > inlineMaxBytes → written into the sandbox workspace (base64 through the\n * structural `write` seam — `box.fs` satisfies it) and referenced by\n * `path`. Mandatory two-step: the gateway caps request bodies at ~1 MiB,\n * so a large file can never ride the prompt POST.\n *\n * The sink is structural (no sandbox-SDK import); products pass `box.fs`.\n *\n * @remarks Sole consumer today is the `--chat` scaffold (`create-agent-app\n * --chat` → `template-chat/src/chat.ts`), the reference multimodal path. The\n * fleet apps (gtm/tax/legal/insurance) each keep their OWN upload route into a\n * durable vault (KV, or AES-GCM-encrypted R2) — a different persistence model\n * from this route's inline-`data:`-or-ephemeral-sandbox-workspace split, so\n * they don't (and shouldn't) route through it. This stays the scaffold's proven\n * upload pattern, not a fleet primitive; keep that distinction in mind before\n * widening its surface.\n */\n\nimport type { ChatTurnFilePartInput } from './wire'\n\n/** 700 KiB: base64 inflates ~4/3, so an inline part stays comfortably under\n * the ~1 MiB gateway body cap alongside the JSON envelope. */\nexport const UPLOAD_INLINE_MAX_BYTES = 700 * 1024\n\n/** 8 MiB default ceiling per file — one base64 `write` call handles it. Raise\n * it only with a sink that can take the bigger single write. */\nexport const UPLOAD_MAX_FILE_BYTES = 8 * 1024 * 1024\n\n/** Structural match of the sandbox SDK's `box.fs` write surface (v0.10.5+:\n * `encoding: 'base64'` is the worker-safe binary path). */\nexport interface SandboxUploadSink {\n write(path: string, content: string, options?: { encoding?: 'utf8' | 'base64' }): Promise<unknown>\n}\n\nexport type UploadAuthorization =\n | {\n ok: true\n /** Where large files land. Absent/null: only inline uploads are\n * accepted and an over-inline-cap file is rejected with 413. */\n sink?: SandboxUploadSink | null\n /** Per-request override of the workspace directory large files go to. */\n uploadDir?: string\n }\n | { ok: false; response: Response }\n\nexport interface CreateUploadRouteOptions {\n /** Authenticate the caller and resolve the sandbox file sink (usually\n * `ensureWorkspaceSandbox(...)` → `box.fs`). */\n authorize(args: { request: Request }): Promise<UploadAuthorization>\n /** Inline-vs-sandbox threshold. Default {@link UPLOAD_INLINE_MAX_BYTES}. */\n inlineMaxBytes?: number\n /** Hard per-file cap. Default {@link UPLOAD_MAX_FILE_BYTES}. */\n maxFileBytes?: number\n /** Workspace directory for path-ref files. Default `'uploads'`. */\n uploadDir?: string\n}\n\n/** One uploaded file, ready for the composer chip and the turn body. */\nexport interface UploadedChatFile {\n id: string\n name: string\n size: number\n mediaType: string\n /** True when the part carries the bytes inline (`data:` URI). */\n inline: boolean\n /** Echo this back verbatim in `ChatTurnRequestPayload.parts`. */\n part: ChatTurnFilePartInput\n}\n\n/** Path-safe file name: basename only, conservative charset, length-capped. */\nexport function sanitizeUploadFilename(name: string): string {\n const base = name.split(/[\\\\/]/).pop() ?? 'file'\n const safe = base.replace(/[^A-Za-z0-9._-]+/g, '_').replace(/^\\.+/, '_')\n return (safe || 'file').slice(0, 120)\n}\n\nconst BASE64_CHUNK = 0x8000\n\nexport function bytesToBase64(bytes: Uint8Array): string {\n let binary = ''\n for (let offset = 0; offset < bytes.length; offset += BASE64_CHUNK) {\n binary += String.fromCharCode(...bytes.subarray(offset, offset + BASE64_CHUNK))\n }\n return btoa(binary)\n}\n\nfunction uploadError(status: number, code: string, error: string): Response {\n return Response.json({ code, error }, { status })\n}\n\nexport function createUploadRoute(options: CreateUploadRouteOptions): (request: Request) => Promise<Response> {\n const inlineMaxBytes = options.inlineMaxBytes ?? UPLOAD_INLINE_MAX_BYTES\n const maxFileBytes = options.maxFileBytes ?? UPLOAD_MAX_FILE_BYTES\n\n return async function upload(request: Request): Promise<Response> {\n const auth = await options.authorize({ request })\n if (!auth.ok) return auth.response\n const sink = auth.sink ?? null\n const uploadDir = (auth.uploadDir ?? options.uploadDir ?? 'uploads').replace(/\\/+$/, '')\n\n let form: FormData\n try {\n form = await request.formData()\n } catch {\n return uploadError(400, 'INVALID_UPLOAD', 'Expected a multipart/form-data body with file fields')\n }\n const files: File[] = []\n form.forEach((value) => {\n if (value instanceof File) files.push(value)\n })\n if (files.length === 0) {\n return uploadError(400, 'INVALID_UPLOAD', 'No files in the upload body')\n }\n\n const uploaded: UploadedChatFile[] = []\n for (const file of files) {\n const name = sanitizeUploadFilename(file.name)\n const mediaType = file.type || 'application/octet-stream'\n const partType: ChatTurnFilePartInput['type'] = mediaType.startsWith('image/') ? 'image' : 'file'\n\n if (file.size > maxFileBytes) {\n return uploadError(\n 413,\n 'FILE_TOO_LARGE',\n `${name} is ${file.size}B, over the ${maxFileBytes}B per-file cap`,\n )\n }\n\n const id = crypto.randomUUID()\n if (file.size <= inlineMaxBytes) {\n const base64 = bytesToBase64(new Uint8Array(await file.arrayBuffer()))\n uploaded.push({\n id,\n name,\n size: file.size,\n mediaType,\n inline: true,\n part: {\n type: partType,\n filename: name,\n mediaType,\n url: `data:${mediaType};base64,${base64}`,\n },\n })\n continue\n }\n\n if (!sink) {\n return uploadError(\n 413,\n 'SANDBOX_REQUIRED',\n `${name} is ${file.size}B, over the ${inlineMaxBytes}B inline cap, and no sandbox is available to hold it`,\n )\n }\n const path = `${uploadDir}/${id}-${name}`\n const base64 = bytesToBase64(new Uint8Array(await file.arrayBuffer()))\n await sink.write(path, base64, { encoding: 'base64' })\n uploaded.push({\n id,\n name,\n size: file.size,\n mediaType,\n inline: false,\n part: { type: partType, filename: name, mediaType, path },\n })\n }\n\n return Response.json({ files: uploaded })\n }\n}\n"],"mappings":"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","names":["base64"]}
@@ -1,6 +1,6 @@
1
1
  export { B as BULK_DELETE_MAX_THREADS, C as ChatStoreInputError, t as threadTitleFromMessage } from '../core-7qIM7svy.js';
2
- import { c as ChatMessagePart } from '../parts-DjX0RRTS.js';
3
- export { C as ChatFilePart, a as ChatImagePart, b as ChatInteractionPart, d as ChatNoticePart, e as ChatPartTime, f as ChatPlanPart, g as ChatReasoningPart, h as ChatStepFinishPart, i as ChatStepStartPart, j as ChatSubtaskPart, k as ChatTextPart, l as ChatToolPart, m as ChatToolState, n as ChatToolStatus, o as ChatUsageTokens, S as StorableHarnessPartKind, p as isChatInteractionPart, q as isChatPlanPart, r as isChatStepFinishPart, s as isChatTextPart, t as isChatToolPart, u as toChatMessageParts } from '../parts-DjX0RRTS.js';
2
+ import { e as ChatMessagePart } from '../parts-1_3y2JmR.js';
3
+ export { C as ChatFilePart, a as ChatImagePart, b as ChatInteractionPart, c as ChatMentionKind, d as ChatMentionPart, 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';
4
4
  import * as drizzle_orm from 'drizzle-orm';
5
5
  import * as drizzle_orm_sqlite_core from 'drizzle-orm/sqlite-core';
6
6
  import { SQLiteColumnBuilderBase, AnySQLiteTable, AnySQLiteColumn, BaseSQLiteDatabase } from 'drizzle-orm/sqlite-core';
@@ -5,12 +5,15 @@ import {
5
5
  } from "../chunk-5EQCITY3.js";
6
6
  import {
7
7
  isChatInteractionPart,
8
+ isChatMentionPart,
8
9
  isChatPlanPart,
9
10
  isChatStepFinishPart,
10
11
  isChatTextPart,
11
12
  isChatToolPart,
13
+ mentionInputToPart,
14
+ mentionPartsFromMessageParts,
12
15
  toChatMessageParts
13
- } from "../chunk-I2ATYB7R.js";
16
+ } from "../chunk-6E2XJSCT.js";
14
17
  import "../chunk-XAWFPMAR.js";
15
18
  import "../chunk-SIXYZ2FB.js";
16
19
 
@@ -192,10 +195,13 @@ export {
192
195
  createChatStore,
193
196
  createChatTables,
194
197
  isChatInteractionPart,
198
+ isChatMentionPart,
195
199
  isChatPlanPart,
196
200
  isChatStepFinishPart,
197
201
  isChatTextPart,
198
202
  isChatToolPart,
203
+ mentionInputToPart,
204
+ mentionPartsFromMessageParts,
199
205
  threadTitleFromMessage,
200
206
  toChatMessageParts
201
207
  };