import {
  ALLOWED_ATTACHMENT_SNIFFED_MIMES,
  ATTACHMENT_ACCEPT,
  ATTACHMENT_MAX_COUNT,
  MAX_ATTACHMENT_TOTAL_BYTES,
  MAX_BINARY_ATTACHMENT_BYTES,
  MAX_TEXT_ATTACHMENT_BYTES,
  attachmentSizeErrorMessage,
  attachmentTotalSizeErrorMessage,
  checkAttachmentType,
  createSandboxFileIndexRoute,
  sanitizeAttachmentFileName,
  sniffBinary
} from "../chunk-5GA3MKOC.js";
import {
  coalesceDeltas,
  createBufferedTurnTap,
  normalizeClientTurnId,
  replayTurnEvents,
  resolveChatTurn,
  stampReplaySeq
} from "../chunk-2EJFIQUV.js";
import {
  DEFAULT_STALE_TURN_LOCK_GRACE_MS,
  DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS,
  reconcileStaleTurnLock
} from "../chunk-CCVWR33L.js";
import {
  createInteractionAnswerRoute
} from "../chunk-6KMAM7BL.js";
import {
  parseJsonObjectBody
} from "../chunk-PTUMBMVH.js";
import {
  ChatTurnInputError,
  DISPATCH_MAX_MEDIA_PARTS,
  DISPATCH_MAX_PARTS,
  DISPATCH_REQUEST_MAX_BYTES,
  DISPATCH_STRUCTURAL_RESERVE_BYTES,
  INLINE_PARTS_MAX_BYTES,
  MENTION_MAX_COUNT,
  assertPromptPartsWithinCap,
  attachmentInputToPart,
  attachmentKindForMime,
  base64WireLen,
  buildMentionPromptBlock,
  chatTurnRequestInit,
  fileMentionsToParts,
  formatBytes,
  mediaTypeForMentionPath,
  mentionInputToPart,
  mentionKindForPath,
  parseChatTurnParts,
  parseFileMentions,
  promptPartsByteSize,
  toChatMessageParts,
  validateSandboxMentionPath
} from "../chunk-FEUW4G2L.js";
import "../chunk-V3HO43PG.js";
import {
  asRecord,
  asString,
  draftAssistantParts,
  finalizeAssistantParts,
  finalizePendingInteractionParts,
  getPartKey,
  mergePersistedPart,
  normalizePersistedPart,
  normalizeToolEvent,
  terminalizeDanglingAssistantToolUpdates
} from "../chunk-5EPIPT4V.js";
import {
  cancelStatusFor,
  interactionPartKey,
  interactionToPersistedPart,
  isRenderableInteractionKind,
  noticePart,
  noticePartKey,
  parseInteractionCancel,
  parseInteractionRequest
} from "../chunk-3ZK5IJSW.js";
import {
  parsePlanSubmittedEvent,
  planToPersistedPart
} from "../chunk-YJMCRXQQ.js";
import {
  buildModelChain,
  isUpstreamUnavailable,
  runWithModelFailover
} from "../chunk-DEXBRUZR.js";
import {
  flattenHistory,
  readSandboxBinaryBytes,
  statSandboxFileSize
} from "../chunk-JXIQSGOV.js";
import "../chunk-LWSJK546.js";
import "../chunk-CQZSAR77.js";
import "../chunk-ICOHEZK6.js";
import "../chunk-3EJ6SFJI.js";
import "../chunk-YEFFHORB.js";
import "../chunk-S5SRJJQG.js";
import "../chunk-JML7WKWU.js";

// src/chat-routes/turn-routes.ts
import { deriveExecutionId, handleChatTurn } from "@tangle-network/agent-runtime";

// src/chat-routes/draft-persistence.ts
var CONTENT_EVENT_TYPES = /* @__PURE__ */ new Set([
  "text",
  "reasoning",
  "tool_call",
  "tool_result",
  "usage",
  "notice",
  "error",
  "file",
  "interaction",
  "interaction.cancel",
  "plan.submitted",
  "message.part.updated"
]);
var DEFAULT_INTERVAL_MS = 2e3;
var DEFAULT_BACKOFF_BYTES = 262144;
var DEFAULT_MAX_DRAFT_TOOL_OUTPUT_BYTES = 32768;
function isDraftContentEvent(event) {
  return typeof event?.type === "string" && CONTENT_EVENT_TYPES.has(event.type);
}
function capDraftToolOutput(part, maxBytes) {
  if (maxBytes <= 0) return part;
  if (String(part.type ?? "") !== "tool") return part;
  const state = part.state;
  if (!state || typeof state !== "object") return part;
  const record = state;
  const output = record.output;
  if (output === void 0 || output === null) return part;
  const serialized = typeof output === "string" ? output : safeStringify(output);
  if (serialized.length <= maxBytes) return part;
  const metadata = record.metadata && typeof record.metadata === "object" ? record.metadata : {};
  return {
    ...part,
    state: {
      ...record,
      output: `${serialized.slice(0, maxBytes)}\u2026[draft-truncated ${serialized.length - maxBytes} chars]`,
      metadata: { ...metadata, draftTruncated: true }
    }
  };
}
function safeStringify(value) {
  try {
    return JSON.stringify(value) ?? "";
  } catch {
    return String(value);
  }
}
function createAssistantDraftWriter(options) {
  const log = options.log ?? (() => {
  });
  const baseIntervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
  const backoffBytes = options.backoffBytes ?? DEFAULT_BACKOFF_BYTES;
  const maxToolOutput = options.maxDraftToolOutputBytes ?? DEFAULT_MAX_DRAFT_TOOL_OUTPUT_BYTES;
  let dirty = false;
  let closed = false;
  let inFlight;
  let lastWriteAt = 0;
  let lastBlobBytes = 0;
  let rowId;
  let writes = 0;
  function currentIntervalMs() {
    if (lastBlobBytes >= backoffBytes * 10) return baseIntervalMs * 5;
    if (lastBlobBytes >= backoffBytes) return Math.round(baseIntervalMs * 2.5);
    return baseIntervalMs;
  }
  async function projectValues(snapshot) {
    const transform = options.transformText;
    const content = transform ? await transform(snapshot.content) : snapshot.content;
    let parts;
    if (snapshot.parts) {
      const capped = snapshot.parts.map((part) => capDraftToolOutput(part, maxToolOutput));
      const redacted = transform ? await Promise.all(
        capped.map(
          async (part) => String(part.type ?? "") === "text" ? { ...part, text: await transform(String(part.text ?? "")) } : part
        )
      ) : capped;
      parts = toChatMessageParts(redacted);
    }
    const usage = snapshot.usage ?? {};
    return {
      content,
      ...parts && parts.length > 0 ? { parts } : {},
      ...snapshot.model ? { model: snapshot.model } : {},
      ...usage.inputTokens !== void 0 ? { inputTokens: usage.inputTokens } : {},
      ...usage.outputTokens !== void 0 ? { outputTokens: usage.outputTokens } : {},
      ...usage.reasoningTokens !== void 0 ? { reasoningTokens: usage.reasoningTokens } : {},
      ...usage.cacheReadTokens !== void 0 ? { cacheReadTokens: usage.cacheReadTokens } : {},
      ...usage.cacheWriteTokens !== void 0 ? { cacheWriteTokens: usage.cacheWriteTokens } : {},
      ...usage.costUsd !== void 0 ? { costUsd: usage.costUsd } : {}
    };
  }
  async function adoptRow() {
    if (rowId) return rowId;
    const existing = (await options.store.listMessages(options.threadId)).find(
      (message) => message.id === options.messageId
    );
    if (existing) rowId = existing.id;
    return rowId;
  }
  async function writeOnce(values) {
    if (await adoptRow()) {
      await options.store.updateMessage(rowId, values);
      writes += 1;
      return;
    }
    const inserted = await options.store.appendMessage({
      id: options.messageId,
      threadId: options.threadId,
      role: "assistant",
      ...values
    });
    rowId = rowIdOf(inserted) ?? options.messageId;
    writes += 1;
  }
  function trigger() {
    if (closed) return;
    if (!dirty) return;
    if (inFlight) return;
    const now = Date.now();
    if (lastWriteAt !== 0 && now - lastWriteAt < currentIntervalMs()) return;
    const snapshot = options.snapshot();
    if (!snapshot) return;
    if (!snapshot.content && (!snapshot.parts || snapshot.parts.length === 0)) return;
    dirty = false;
    lastWriteAt = now;
    inFlight = (async () => {
      try {
        const values = await projectValues(snapshot);
        lastBlobBytes = values.parts ? safeStringify(values.parts).length : 0;
        await writeOnce(values);
      } catch (err) {
        log("[chat-routes] incremental assistant persistence failed", {
          messageId: options.messageId,
          error: err instanceof Error ? err.message : String(err)
        });
      } finally {
        inFlight = void 0;
        if (dirty && !closed) trigger();
      }
    })();
  }
  return {
    notify(event) {
      if (closed) return;
      if (isDraftContentEvent(event)) dirty = true;
      trigger();
    },
    async close() {
      closed = true;
      if (inFlight) await inFlight;
    },
    async finalize(values) {
      closed = true;
      if (inFlight) await inFlight;
      await writeOnce(values);
    },
    rowId: () => rowId,
    async discard() {
      closed = true;
      if (inFlight) await inFlight;
      if (!options.store.deleteMessage) return;
      try {
        if (!await adoptRow()) return;
        await options.store.deleteMessage(rowId);
        rowId = void 0;
      } catch (err) {
        log("[chat-routes] draft assistant row discard failed", {
          messageId: options.messageId,
          error: err instanceof Error ? err.message : String(err)
        });
      }
    },
    writeCount: () => writes
  };
}
function storeSupportsDraftPersistence(store) {
  return typeof store.updateMessage === "function";
}
function rowIdOf(inserted) {
  const id = inserted?.id;
  return typeof id === "string" && id ? id : null;
}
function assistantRowIdForTurn(turnKey) {
  return `assistant:${turnKey}`;
}

// src/chat-routes/turn-routes.ts
function failureReasonOf(data) {
  if (!data) return void 0;
  const message = data.message ?? data.error ?? data.reason;
  if (typeof message === "string" && message.length > 0) return message;
  return void 0;
}
function errorResponse(err) {
  return Response.json({ code: err.code, error: err.message }, { status: err.status });
}
function validateTurnBody(body, maxInlinePartBytes) {
  const threadId = typeof body.threadId === "string" ? body.threadId.trim() : "";
  if (!threadId) throw new ChatTurnInputError("Missing threadId");
  const rawContent = body.content ?? body.message ?? "";
  if (typeof rawContent !== "string") throw new ChatTurnInputError("content must be a string");
  const content = rawContent.trim();
  const fileParts = parseChatTurnParts(body.parts);
  const mentions = parseFileMentions(body.mentions);
  if (!content && fileParts.length === 0 && mentions.length === 0) {
    throw new ChatTurnInputError("Missing content (send text, parts, mentions, or any combination)");
  }
  assertPromptPartsWithinCap(fileParts, maxInlinePartBytes);
  let turnId;
  try {
    turnId = normalizeClientTurnId(body.turnId);
  } catch (err) {
    throw new ChatTurnInputError(err instanceof Error ? err.message : "Invalid turnId");
  }
  return {
    // The VALIDATED, deduped mention list replaces the raw one on the payload,
    // so every downstream seam (`authorize`, `contextGate`, `beforeTurn`,
    // `produce`) reads checked paths and never the request's own.
    payload: { ...body, threadId, content, mentions },
    content,
    fileParts,
    mentions,
    turnId
  };
}
function userPartsWithFiles(userParts, fileParts, mentions) {
  return toChatMessageParts([
    ...userParts,
    ...fileParts.map((part) => ({ ...part })),
    ...mentions.map((mention) => ({ ...mentionInputToPart(mention) }))
  ]);
}
async function* tapRawEvents(source, onRawEvent, log) {
  for await (const event of source) {
    try {
      await onRawEvent(event);
    } catch (err) {
      log("[chat-routes] onRawEvent failed", { error: err instanceof Error ? err.message : String(err) });
    }
    yield event;
  }
}
async function* withStreamHeartbeat(source, intervalMs, makeEvent) {
  const iterator = source[Symbol.asyncIterator]();
  try {
    let pending = iterator.next();
    let windowStart = Date.now();
    let tick = 0;
    for (; ; ) {
      let timer;
      let winner;
      try {
        const heartbeat = new Promise((resolve) => {
          timer = setTimeout(() => resolve("heartbeat"), intervalMs);
        });
        winner = await Promise.race([pending.then(() => "event"), heartbeat]);
      } finally {
        if (timer !== void 0) clearTimeout(timer);
      }
      if (winner === "heartbeat") {
        tick += 1;
        yield makeEvent({ elapsedMs: Date.now() - windowStart, tick });
        continue;
      }
      const result = await pending;
      if (result.done) return;
      yield result.value;
      pending = iterator.next();
      windowStart = Date.now();
      tick = 0;
    }
  } finally {
    await iterator.return?.();
  }
}
function createChatTurnRoutes(options) {
  const log = options.log ?? ((message, meta) => console.error(message, meta ?? ""));
  const draftStore = options.store;
  if (options.incrementalPersistence && !storeSupportsDraftPersistence(draftStore)) {
    throw new Error(
      "incrementalPersistence requires a store with updateMessage() \u2014 `/chat-store`'s createChatStore has it; a product store must implement it or omit the option"
    );
  }
  const draftTuning = options.incrementalPersistence === false || !storeSupportsDraftPersistence(draftStore) ? null : options.incrementalPersistence ?? {};
  async function turn(request, ctx) {
    const [rawBody, badBody] = await parseJsonObjectBody(request);
    if (badBody) return badBody;
    let parsed;
    try {
      parsed = validateTurnBody(rawBody, options.maxInlinePartBytes);
    } catch (err) {
      if (err instanceof ChatTurnInputError) return errorResponse(err);
      throw err;
    }
    const { payload, content, fileParts, mentions, turnId } = parsed;
    const auth = await options.authorize({ request, intent: "turn", body: payload });
    if (!auth.ok) return auth.response;
    const { tenantId, userId, context } = auth;
    const existingMessages = (await options.store.listMessages(payload.threadId)).map((m) => ({
      id: m.id,
      role: m.role,
      content: m.content,
      parts: m.parts ?? null
    }));
    let hasRunningTurn = false;
    if (draftTuning && existingMessages.at(-1)?.role === "assistant") {
      try {
        hasRunningTurn = (await options.turnStore.listRunning?.(payload.threadId) ?? []).length > 0;
      } catch (err) {
        log("[chat-routes] listRunning probe failed; treating the thread as idle", {
          threadId: payload.threadId,
          error: err instanceof Error ? err.message : String(err)
        });
      }
    }
    const chatTurn = resolveChatTurn({ existingMessages, userContent: content, turnId, hasRunningTurn });
    const identity = {
      tenantId,
      sessionId: payload.threadId,
      userId,
      turnIndex: chatTurn.turnIndex
    };
    const executionId = deriveExecutionId({
      projectId: options.projectId,
      sessionId: payload.threadId,
      turnIndex: chatTurn.turnIndex
    });
    const turnStreamId = crypto.randomUUID();
    const prompt = fileParts.length === 0 ? content : content ? [{ type: "text", text: content }, ...fileParts] : [...fileParts];
    let produceArgs = {
      request,
      body: payload,
      identity,
      context,
      prompt,
      executionId,
      turnStreamId,
      priorMessages: chatTurn.priorMessages
    };
    let lockAcquired = false;
    let lockHandle;
    let lockReleased = false;
    const releaseLock = async () => {
      if (!lockAcquired || lockReleased) return;
      lockReleased = true;
      try {
        await options.turnLock.release(lockHandle);
      } catch (err) {
        log("[chat-routes] turnLock.release failed", {
          turnId: turnStreamId,
          error: err instanceof Error ? err.message : String(err)
        });
      }
    };
    if (options.turnLock) {
      const acquired = await options.turnLock.acquire(produceArgs);
      if (!acquired.acquired) return acquired.response;
      lockAcquired = true;
      lockHandle = acquired.handle;
    }
    let producer;
    let draft;
    let assistantMessageId;
    const assistantRowId = () => assistantMessageId ?? draft?.rowId() ?? null;
    let runFailed = false;
    let lastFailureData;
    let turnStartedAtMs = 0;
    let turnStarted = false;
    let lifecycleSettled = false;
    const fireTerminalLifecycle = async (failed, terminalError) => {
      if (lifecycleSettled) return;
      lifecycleSettled = true;
      const lifecycle = options.lifecycle;
      if (!lifecycle) return;
      const durationMs = Date.now() - turnStartedAtMs;
      try {
        if (failed) {
          await lifecycle.onTurnError?.({
            identity,
            executionId,
            turnStreamId,
            context,
            durationMs,
            error: terminalError ?? lastFailureData ?? new Error("chat turn failed")
          });
        } else {
          const failoverInfo = producer?.modelFailover?.();
          await lifecycle.onTurnComplete?.({
            identity,
            executionId,
            turnStreamId,
            context,
            durationMs,
            finalText: producer?.finalText() ?? "",
            usage: producer?.usage?.() ?? {},
            assistantMessageId: assistantRowId(),
            ...producer?.model ? { model: producer.model } : {},
            ...failoverInfo ? { modelFailover: failoverInfo } : {}
          });
        }
      } catch (err) {
        log("[chat-routes] lifecycle terminal hook failed", {
          turnId: turnStreamId,
          error: err instanceof Error ? err.message : String(err)
        });
      }
    };
    try {
      const insertUserMessage = chatTurn.shouldInsertUserMessage && (auth.insertUserMessage ?? true);
      let userMessageId = chatTurn.reusedUserMessageId ?? null;
      if (insertUserMessage) {
        userMessageId = rowIdOf(
          await options.store.appendMessage({
            threadId: payload.threadId,
            role: "user",
            content,
            parts: userPartsWithFiles(chatTurn.userParts, fileParts, mentions)
          })
        );
      }
      produceArgs = { ...produceArgs, userMessageId };
      if (options.contextGate) {
        const gate = await options.contextGate(produceArgs);
        if (!gate.proceed) {
          await releaseLock();
          return gate.response;
        }
      }
      if (options.beforeTurn) {
        const patch = await options.beforeTurn(produceArgs);
        if (patch) produceArgs = { ...produceArgs, ...patch };
      }
      const tap = createBufferedTurnTap({
        store: options.turnStore,
        turnId: turnStreamId,
        scopeId: payload.threadId,
        coalesce: options.coalesceTurnEvents ?? coalesceDeltas
      });
      const turnMarker = { type: "turn", turnId: turnStreamId };
      await tap.onEvent(turnMarker);
      if (draftTuning) {
        draft = createAssistantDraftWriter({
          ...draftTuning,
          store: draftStore,
          threadId: payload.threadId,
          messageId: options.draftMessageId ? options.draftMessageId({ identity, executionId, threadId: payload.threadId }) : assistantRowIdForTurn(executionId),
          snapshot: () => {
            if (!producer) return null;
            return {
              content: producer.finalText(),
              ...producer.draftParts ? { parts: producer.draftParts() } : {},
              ...producer.usage ? { usage: producer.usage() } : {},
              ...producer.model ? { model: producer.model } : {}
            };
          },
          ...options.transformFinalText ? { transformText: options.transformFinalText } : {},
          log
        });
      }
      turnStartedAtMs = Date.now();
      turnStarted = true;
      if (options.lifecycle?.onTurnStart) {
        try {
          await options.lifecycle.onTurnStart({
            identity,
            executionId,
            turnStreamId,
            context,
            startedAt: turnStartedAtMs
          });
        } catch (err) {
          log("[chat-routes] lifecycle.onTurnStart failed", {
            turnId: turnStreamId,
            error: err instanceof Error ? err.message : String(err)
          });
        }
      }
      const result = handleChatTurn({
        identity,
        waitUntil: ctx?.waitUntil,
        log,
        hooks: {
          // The engine wants a synchronous producer; box resolution is async —
          // defer it into the generator's first pull.
          produce: () => ({
            stream: (async function* () {
              producer = await options.produce(produceArgs);
              let source = producer.stream;
              if (options.onRawEvent) {
                source = tapRawEvents(source, (event) => options.onRawEvent(event, context), log);
              }
              if (options.heartbeat) {
                source = withStreamHeartbeat(source, options.heartbeat.intervalMs, options.heartbeat.event);
              }
              for await (const event of source) yield event;
            })(),
            finalText: () => producer?.finalText() ?? ""
          }),
          onEvent: async (event) => {
            if (event.type === "session.run.failed" || event.type === "error") {
              runFailed = true;
              lastFailureData = event.data;
            }
            await tap.onEvent(event);
            draft?.notify(event);
            if (options.onEvent) await options.onEvent(event, context);
          },
          ...options.transformFinalText ? { transformFinalText: options.transformFinalText } : {},
          persistAssistantMessage: async ({ finalText }) => {
            const rawParts = producer?.assistantParts ? producer.assistantParts() : void 0;
            const projected = rawParts && options.transformFinalText ? await Promise.all(
              rawParts.map(
                async (part) => String(part.type ?? "") === "text" ? { ...part, text: await options.transformFinalText(String(part.text ?? "")) } : part
              )
            ) : rawParts;
            const parts = projected ? toChatMessageParts(projected) : void 0;
            if (!finalText.trim() && (!parts || parts.length === 0)) {
              await draft?.discard();
              assistantMessageId = draft?.rowId() ?? null;
              return;
            }
            const usage = producer?.usage?.() ?? {};
            const values = {
              content: finalText,
              ...parts && parts.length > 0 ? { parts } : {},
              ...producer?.model ? { model: producer.model } : {},
              ...usage.inputTokens !== void 0 ? { inputTokens: usage.inputTokens } : {},
              ...usage.outputTokens !== void 0 ? { outputTokens: usage.outputTokens } : {},
              ...usage.reasoningTokens !== void 0 ? { reasoningTokens: usage.reasoningTokens } : {},
              ...usage.cacheReadTokens !== void 0 ? { cacheReadTokens: usage.cacheReadTokens } : {},
              ...usage.cacheWriteTokens !== void 0 ? { cacheWriteTokens: usage.cacheWriteTokens } : {},
              ...usage.costUsd !== void 0 ? { costUsd: usage.costUsd } : {}
            };
            if (draft) {
              await draft.finalize(values);
              assistantMessageId = draft.rowId() ?? null;
              return;
            }
            assistantMessageId = rowIdOf(
              await options.store.appendMessage({
                threadId: payload.threadId,
                role: "assistant",
                ...values
              })
            );
          },
          ...options.onTurnComplete ? {
            // Wired into the engine's completion hook, which fires only when
            // the stream ended without throwing. A terminal error EVENT
            // (not a throw) still lands here — so surface `runFailed` so the
            // product skips billing an errored turn instead of marking it
            // complete with empty text.
            onTurnComplete: ({ identity: turnIdentity, finalText }) => {
              const failoverInfo = producer?.modelFailover?.();
              return options.onTurnComplete({
                identity: turnIdentity,
                finalText,
                context,
                failed: runFailed,
                assistantMessageId: assistantRowId(),
                ...runFailed ? { failureReason: failureReasonOf(lastFailureData) } : {},
                ...producer?.model ? { model: producer.model } : {},
                ...failoverInfo ? { modelFailover: failoverInfo } : {}
              });
            }
          } : {},
          ...options.traceFlush ? { traceFlush: () => options.traceFlush(context) } : {}
        }
      });
      const [clientBody, drainBody] = result.body.tee();
      const drained = (async () => {
        const reader = drainBody.getReader();
        let drainError;
        try {
          for (; ; ) {
            const { done } = await reader.read();
            if (done) break;
          }
        } catch (err) {
          drainError = err;
          log("[chat-routes] turn drain failed", {
            turnId: turnStreamId,
            error: err instanceof Error ? err.message : String(err)
          });
        }
        const failed = runFailed || drainError !== void 0;
        await draft?.close();
        try {
          await tap.done(failed ? "error" : "complete");
        } catch (err) {
          log("[chat-routes] turn buffer finalize failed", {
            turnId: turnStreamId,
            error: err instanceof Error ? err.message : String(err)
          });
        }
        await fireTerminalLifecycle(failed, drainError);
        await releaseLock();
      })();
      if (ctx?.waitUntil) ctx.waitUntil(drained);
      else void drained.catch(() => {
      });
      const encoder = new TextEncoder();
      const marker = new ReadableStream({
        start(controller) {
          controller.enqueue(encoder.encode(`${JSON.stringify(turnMarker)}
`));
          controller.close();
        }
      });
      const body = concatStreams([marker, clientBody]);
      return new Response(body, {
        headers: {
          "Content-Type": result.contentType,
          "Cache-Control": "no-cache"
        }
      });
    } catch (err) {
      if (turnStarted) await fireTerminalLifecycle(true, err);
      await releaseLock();
      throw err;
    }
  }
  async function replay(request, params) {
    const turnId = params.turnId?.trim();
    if (!turnId) return Response.json({ error: "Missing turnId" }, { status: 400 });
    const auth = await options.authorize({ request, intent: "replay", turnId });
    if (!auth.ok) return auth.response;
    const fromSeqRaw = new URL(request.url).searchParams.get("fromSeq");
    const fromSeq = fromSeqRaw ? Math.max(0, Math.trunc(Number(fromSeqRaw)) || 0) : 0;
    const encoder = new TextEncoder();
    const events = replayTurnEvents({
      store: options.turnStore,
      turnId,
      fromSeq,
      ...options.replay?.pollMs !== void 0 ? { pollMs: options.replay.pollMs } : {},
      ...options.replay?.timeoutMs !== void 0 ? { timeoutMs: options.replay.timeoutMs } : {}
    });
    const body = new ReadableStream({
      async pull(controller) {
        const { done, value } = await events.next();
        if (done) {
          controller.close();
          return;
        }
        controller.enqueue(encoder.encode(`${stampReplaySeq(value)}
`));
      },
      cancel() {
        void events.return(void 0);
      }
    });
    return new Response(body, {
      headers: {
        "Content-Type": "application/x-ndjson",
        "Cache-Control": "no-cache"
      }
    });
  }
  async function running(request) {
    const threadId = new URL(request.url).searchParams.get("threadId")?.trim();
    if (!threadId) return Response.json({ error: "Missing threadId" }, { status: 400 });
    const auth = await options.authorize({ request, intent: "running", threadId });
    if (!auth.ok) return auth.response;
    const ids = await options.turnStore.listRunning?.(threadId) ?? [];
    return Response.json({ running: ids });
  }
  return {
    turn,
    replay,
    running,
    interactions: options.interactions ? createInteractionAnswerRoute(options.interactions) : null
  };
}
function concatStreams(streams) {
  let index = 0;
  let reader = null;
  return new ReadableStream({
    async pull(controller) {
      for (; ; ) {
        if (!reader) {
          const next = streams[index++];
          if (!next) {
            controller.close();
            return;
          }
          reader = next.getReader();
        }
        const { done, value } = await reader.read();
        if (done) {
          reader = null;
          continue;
        }
        controller.enqueue(value);
        return;
      }
    },
    async cancel(reason) {
      await reader?.cancel(reason);
      for (const stream of streams.slice(index)) await stream.cancel(reason);
    }
  });
}

// src/chat-routes/model-failover-stream.ts
var TERMINAL_FAILURE_TYPES = /* @__PURE__ */ new Set(["error", "session.run.failed"]);
var NON_COMMITTING_PART_TYPES = /* @__PURE__ */ new Set(["step-start", "step-finish"]);
function isCommittingSandboxEvent(event) {
  const record = asRecord(event);
  if (!record) return false;
  const type = asString(record.type) ?? "";
  if (!type) return false;
  if (type === "message.part.updated") {
    const part = asRecord(asRecord(record.data)?.part);
    const partType = asString(part?.type) ?? "";
    if (NON_COMMITTING_PART_TYPES.has(partType)) return false;
    if (partType === "text" || partType === "reasoning") {
      const text = asString(part?.text) ?? asString(part?.content) ?? "";
      return text.length > 0;
    }
    return true;
  }
  if (type === "start" || type === "execution.started" || type === "status" || type === "model-processing" || type === "session.created" || type === "session.updated" || type === "session.idle" || type === "step-start" || type === "step-finish" || type === "turn" || type === "warning") {
    return false;
  }
  return true;
}
function classifyTerminalFailure(event) {
  const record = asRecord(event);
  if (!record) return null;
  const type = asString(record.type) ?? "";
  if (!TERMINAL_FAILURE_TYPES.has(type)) return null;
  const data = asRecord(record.data);
  const outage = isUpstreamUnavailable(data) || isUpstreamUnavailable(record);
  const reason = asString(data?.message) ?? asString(data?.error) ?? asString(data?.reason) ?? asString(record.message) ?? `sandbox stream reported ${type}`;
  const code = asString(data?.errorCode) ?? asString(data?.code);
  return { outage, reason, ...code ? { code } : {} };
}
function isEmptyTerminalReceipt(event) {
  const record = asRecord(event);
  if (!record) return false;
  const type = asString(record.type) ?? "";
  if (type !== "result" && type !== "done") return false;
  const data = asRecord(record.data);
  const text = asString(data?.finalText) ?? asString(data?.text) ?? "";
  return text.trim().length === 0;
}
var MAX_EMPTY_TURN_RETRIES = 3;
function resolveEmptyTurnRetries(value) {
  if (typeof value !== "number" || !Number.isFinite(value)) return 0;
  const truncated = Math.trunc(value);
  if (truncated <= 0) return 0;
  return Math.min(truncated, MAX_EMPTY_TURN_RETRIES);
}
async function closeIterator(iterator, log) {
  try {
    await iterator.return?.();
  } catch (err) {
    log?.("[chat-routes] abandoning a failed model stream threw", {
      error: err instanceof Error ? err.message : String(err)
    });
  }
}
function streamWithModelFailover(options) {
  const committing = options.isCommitting ?? isCommittingSandboxEvent;
  const emptyTurnRetries = resolveEmptyTurnRetries(options.emptyTurnRetries);
  let serving;
  let trail = [];
  let fellBack = false;
  let attemptIndex = 0;
  const drainOnce = async (model) => {
    attemptIndex += 1;
    const source = await options.open({ model, attempt: attemptIndex });
    const iterator = source[Symbol.asyncIterator]();
    const buffered = [];
    for (; ; ) {
      const next = await iterator.next();
      if (next.done) {
        return { outcome: { committed: true, buffered, iterator: null }, empty: true };
      }
      const event = next.value;
      const failure = classifyTerminalFailure(event);
      if (failure?.outage) {
        await closeIterator(iterator, options.log);
        return {
          outcome: {
            committed: false,
            error: failure.reason,
            ...failure.code ? { errorCode: failure.code } : {}
          },
          empty: false
        };
      }
      buffered.push(event);
      if (failure) return { outcome: { committed: true, buffered, iterator }, empty: false };
      if (committing(event)) {
        return { outcome: { committed: true, buffered, iterator }, empty: isEmptyTerminalReceipt(event) };
      }
    }
  };
  const probe = async (model) => {
    for (let retry = 0; ; retry += 1) {
      const pass = await drainOnce(model);
      if (!pass.empty || retry >= emptyTurnRetries) return pass.outcome;
      const iterator = pass.outcome.committed ? pass.outcome.iterator : null;
      if (iterator) await closeIterator(iterator, options.log);
      const info = { model, retry: retry + 1, remaining: emptyTurnRetries - retry - 1 };
      options.log?.("[chat-routes] turn completed with no assistant text; re-running the same model", {
        ...info
      });
      options.onEmptyTurnRetry?.(info);
    }
  };
  const events = (async function* () {
    let handle;
    try {
      const outcome = await runWithModelFailover({
        models: options.models,
        run: probe,
        // The probe has already classified the raw payload with
        // `isUpstreamUnavailable`; this reads its verdict rather than
        // re-classifying a wrapper object, so the two can never disagree.
        isUnavailableResult: (result) => result.committed === false,
        onFallback: (attempt, nextModel) => {
          const info = {
            from: attempt.model,
            to: nextModel,
            reason: attempt.reason ?? "upstream unavailable"
          };
          options.log?.("[chat-routes] model upstream unavailable; falling over", { ...info });
          options.onFallback?.(info);
        }
      });
      serving = outcome.model;
      trail = outcome.attempts;
      fellBack = outcome.usedFallback;
      handle = outcome.value;
    } catch (err) {
      const attempts = err?.attempts;
      if (Array.isArray(attempts)) trail = attempts;
      throw err;
    }
    for (const event of handle.buffered) yield event;
    const live = handle.iterator;
    if (!live) return;
    try {
      for (; ; ) {
        const next = await live.next();
        if (next.done) return;
        yield next.value;
      }
    } finally {
      await closeIterator(live, options.log);
    }
  })();
  return {
    events,
    servingModel: () => serving,
    attempts: () => trail,
    usedFallback: () => fellBack
  };
}

// src/chat-routes/sandbox-producer.ts
function textDelta(tracker, key, part, rawDelta) {
  const explicit = typeof rawDelta === "string" ? rawDelta : void 0;
  const previous = tracker.seen.get(key) ?? "";
  if (explicit !== void 0) {
    tracker.seen.set(key, previous + explicit);
    return explicit;
  }
  const snapshot = asString(part.text) ?? asString(part.content) ?? "";
  if (!snapshot) return "";
  if (snapshot.startsWith(previous)) {
    tracker.seen.set(key, snapshot);
    return snapshot.slice(previous.length);
  }
  tracker.seen.set(key, snapshot);
  return snapshot;
}
function usageFromStepFinish(part, usage) {
  const tokens = asRecord(part.tokens);
  if (tokens) {
    const cache = asRecord(tokens.cache);
    const add = (current, value) => {
      const n = Number(value);
      if (!Number.isFinite(n)) return current;
      return (current ?? 0) + n;
    };
    usage.inputTokens = add(usage.inputTokens, tokens.input);
    usage.outputTokens = add(usage.outputTokens, tokens.output);
    usage.reasoningTokens = add(usage.reasoningTokens, tokens.reasoning);
    if (cache) {
      usage.cacheReadTokens = add(usage.cacheReadTokens, cache.read);
      usage.cacheWriteTokens = add(usage.cacheWriteTokens, cache.write);
    }
  }
  const cost = Number(part.cost);
  if (Number.isFinite(cost)) usage.costUsd = (usage.costUsd ?? 0) + cost;
}
function toFiniteNumber(value) {
  if (typeof value === "number") return Number.isFinite(value) ? value : null;
  if (typeof value === "string" && value.trim() !== "") {
    const parsed = Number(value);
    return Number.isFinite(parsed) ? parsed : null;
  }
  return null;
}
function extractReportedTurnUsage(data) {
  const tokenUsage = asRecord(data?.tokenUsage);
  if (!tokenUsage) return null;
  const inputTokens = toFiniteNumber(tokenUsage.inputTokens);
  const outputTokens = toFiniteNumber(tokenUsage.outputTokens);
  if (inputTokens === null || outputTokens === null) return null;
  const reported = { inputTokens, outputTokens };
  const reasoningTokens = toFiniteNumber(tokenUsage.reasoningTokens);
  if (reasoningTokens !== null) reported.reasoningTokens = reasoningTokens;
  const cacheReadTokens = toFiniteNumber(tokenUsage.cacheReadInputTokens);
  if (cacheReadTokens !== null) reported.cacheReadTokens = cacheReadTokens;
  const cacheWriteTokens = toFiniteNumber(tokenUsage.cacheCreationInputTokens);
  if (cacheWriteTokens !== null) reported.cacheWriteTokens = cacheWriteTokens;
  const costUsd = toFiniteNumber(data?.totalCostUsd) ?? toFiniteNumber(tokenUsage.cost);
  if (costUsd !== null) reported.costUsd = costUsd;
  return reported;
}
function applyTerminalUsage(reported, usage) {
  usage.inputTokens = reported.inputTokens;
  usage.outputTokens = reported.outputTokens;
  if (reported.reasoningTokens !== void 0) usage.reasoningTokens = reported.reasoningTokens;
  if (reported.cacheReadTokens !== void 0) usage.cacheReadTokens = reported.cacheReadTokens;
  if (reported.cacheWriteTokens !== void 0) usage.cacheWriteTokens = reported.cacheWriteTokens;
  if (reported.costUsd !== void 0) usage.costUsd = reported.costUsd;
}
function sandboxStreamErrorMessage(data) {
  const record = asRecord(data);
  const message = asString(record?.message) ?? asString(record?.error);
  if (message) return message;
  try {
    return JSON.stringify(data) || "Sandbox stream returned an error event without details.";
  } catch {
    return "Sandbox stream returned an error event without details.";
  }
}
function toProducerWireEvent(event) {
  return event;
}
function sandboxStreamFailureDiagnostic(error) {
  const streamMessage = error?.streamMessage;
  const message = typeof streamMessage === "string" ? streamMessage : error instanceof Error ? error.message : "Sandbox stream failed";
  const diagnostics = asRecord(error?.diagnostics);
  let diagnosticText;
  if (diagnostics) {
    try {
      diagnosticText = JSON.stringify(diagnostics);
    } catch {
      diagnosticText = "[unserializable diagnostics]";
    }
  }
  const userMessage = [
    "The sandbox model stream stopped before a clean completion.",
    `Error: ${message}`,
    "Please retry. If it repeats, send these support details to the team."
  ].join("\n\n");
  const failureNote = diagnosticText ? `sandbox-stream: ${message}; ${diagnosticText}` : `sandbox-stream: ${message}`;
  return { userMessage, failureNote };
}
function createSandboxChatProducer(options) {
  const log = options.log ?? ((message, meta) => console.error(message, meta ?? ""));
  const renderable = options.isRenderableInteraction ?? isRenderableInteractionKind;
  if (options.openEvents && !options.model) {
    throw new Error(
      "createSandboxChatProducer: `openEvents` requires `model` \u2014 failover must know which model it is running"
    );
  }
  if (!options.openEvents && !options.events) {
    throw new Error("createSandboxChatProducer: pass `openEvents` (failover-capable) or `events`");
  }
  if (!options.openEvents && resolveEmptyTurnRetries(options.emptyTurnRetries) > 0) {
    throw new Error(
      "createSandboxChatProducer: `emptyTurnRetries` requires `openEvents` \u2014 a fixed `events` stream cannot be re-opened"
    );
  }
  const chain = options.model ? buildModelChain(options.model, options.modelFailover === false ? [] : options.fallbackModels ?? []) : [];
  const pendingModelNotices = [];
  let modelNoticeCount = 0;
  let failover;
  let source;
  if (options.openEvents) {
    failover = streamWithModelFailover({
      models: chain,
      open: options.openEvents,
      log,
      ...options.emptyTurnRetries !== void 0 ? { emptyTurnRetries: options.emptyTurnRetries } : {},
      ...options.onEmptyTurnRetry ? { onEmptyTurnRetry: options.onEmptyTurnRetry } : {},
      onFallback: (info) => {
        modelNoticeCount += 1;
        pendingModelNotices.push({
          id: `model-fallback-${modelNoticeCount}`,
          // Named models on both sides: a quality regression after a downgrade
          // must be attributable to the model that actually answered.
          text: `${info.from} was unavailable (${info.reason}) \u2014 answered with ${info.to} instead.`
        });
        options.onModelFallback?.(info);
      }
    });
    source = failover.events;
  } else {
    source = options.events;
  }
  const servingModel = () => failover?.servingModel() ?? options.model;
  let fullText = "";
  const partOrder = [];
  const partMap = /* @__PURE__ */ new Map();
  const tracker = { seen: /* @__PURE__ */ new Map() };
  const usage = {};
  const announcedTools = /* @__PURE__ */ new Set();
  const settledTools = /* @__PURE__ */ new Set();
  let stepCounter = 0;
  let danglingToolsTerminalized = false;
  let interactionOutcome = "answered";
  let warningCount = 0;
  const promotedFileParts = /* @__PURE__ */ new Map();
  function recordPersistedPart(part, delta, keyOverride) {
    const persisted = normalizePersistedPart(part);
    if (!persisted) return;
    const key = keyOverride ?? getPartKey(persisted);
    if (!partMap.has(key)) partOrder.push(key);
    partMap.set(key, mergePersistedPart(partMap.get(key), persisted, delta));
  }
  function* emitTerminalizedTools() {
    if (danglingToolsTerminalized) return;
    danglingToolsTerminalized = true;
    const updates = terminalizeDanglingAssistantToolUpdates(partOrder, partMap, fullText);
    for (const part of updates) {
      const toolId = asString(part.id);
      if (!toolId || settledTools.has(toolId)) continue;
      settledTools.add(toolId);
      const state = asRecord(part.state);
      yield {
        type: "tool_result",
        toolCallId: toolId,
        toolName: asString(part.tool) ?? "tool",
        outcome: {
          ok: false,
          ...asString(state?.error) ? { message: asString(state?.error) } : {}
        }
      };
    }
  }
  function* emitFinalUsage() {
    const promptTokens = usage.inputTokens ?? 0;
    const completionTokens = usage.outputTokens ?? 0;
    if (promptTokens || completionTokens) {
      yield { type: "usage", usage: { promptTokens, completionTokens } };
    }
  }
  function* drainModelNotices() {
    while (pendingModelNotices.length > 0) {
      const queued = pendingModelNotices.shift();
      const notice = noticePart("warning", queued.id, queued.text);
      recordPersistedPart(notice, void 0, noticePartKey(notice.id));
      yield { type: "notice", id: notice.id, noticeKind: "warning", text: queued.text };
    }
  }
  async function* stream() {
    try {
      for await (const raw of source) {
        yield* drainModelNotices();
        const record = asRecord(raw);
        if (!record || typeof record.type !== "string") continue;
        const normalized = normalizeToolEvent({ type: record.type, data: asRecord(record.data) });
        const event = normalized.type === "message.part.updated" ? normalized : { type: record.type, data: asRecord(record.data) };
        if (event.type === "message.part.updated") {
          const part = asRecord(event.data?.part);
          if (!part) continue;
          const rawDelta = event.data?.delta;
          const partType = String(part.type ?? "");
          if (partType === "text" || partType === "reasoning") {
            const key = getPartKey(part);
            const delta = textDelta(tracker, key, part, rawDelta);
            recordPersistedPart(part, delta || void 0);
            if (delta) {
              if (partType === "text") fullText += delta;
              yield { type: partType, text: delta };
            }
            continue;
          }
          if (partType === "tool") {
            recordPersistedPart(part, void 0);
            const persisted = partMap.get(getPartKey(part));
            const state = asRecord(persisted?.state);
            const toolId = String(persisted?.id ?? "");
            const toolName = String(persisted?.tool ?? "tool");
            if (toolId && !announcedTools.has(toolId)) {
              announcedTools.add(toolId);
              yield {
                type: "tool_call",
                call: { toolCallId: toolId, toolName, args: asRecord(state?.input) ?? {} }
              };
            }
            const status = String(state?.status ?? "");
            if (toolId && (status === "completed" || status === "error") && !settledTools.has(toolId)) {
              settledTools.add(toolId);
              yield {
                type: "tool_result",
                toolCallId: toolId,
                toolName,
                outcome: {
                  ok: status === "completed",
                  ...state?.output !== void 0 ? { result: state.output } : {},
                  ...asString(state?.error) ? { message: asString(state?.error) } : {}
                }
              };
            }
            continue;
          }
          if (partType === "step-finish") {
            usageFromStepFinish(part, usage);
            recordPersistedPart(part, void 0, `step-finish:#${stepCounter++}`);
            const promptTokens = usage.inputTokens ?? 0;
            const completionTokens = usage.outputTokens ?? 0;
            if (promptTokens || completionTokens) {
              yield { type: "usage", usage: { promptTokens, completionTokens } };
            }
            continue;
          }
          if (partType === "step-start") {
            recordPersistedPart(part, void 0, `step-start:#${stepCounter}`);
            continue;
          }
          if (partType === "file" && options.promoteFilePart) {
            const promote = options.promoteFilePart;
            const rawId = asString(part.id);
            const rawUrl = asString(part.url);
            const memoKey = rawId ? `id:${rawId}` : rawUrl ? `url:${rawUrl}` : void 0;
            const attempt = () => Promise.resolve().then(() => promote(part)).catch((err) => {
              const reason = err instanceof Error ? err.message : String(err);
              log("[chat-routes] file part promotion threw", { key: memoKey ?? "(keyless)", error: reason });
              return { succeeded: false, reason };
            });
            let pending;
            if (memoKey) {
              pending = promotedFileParts.get(memoKey) ?? attempt();
              promotedFileParts.set(memoKey, pending);
            } else {
              pending = attempt();
            }
            const outcome = await pending;
            if (outcome.succeeded) {
              recordPersistedPart(outcome.part, void 0, outcome.key);
            } else if (outcome.part) {
              recordPersistedPart(outcome.part, void 0, outcome.key);
            } else {
              recordPersistedPart(part, void 0);
            }
            continue;
          }
          recordPersistedPart(part, void 0);
          continue;
        }
        if (event.type === "interaction") {
          const parsed = parseInteractionRequest(asRecord(record.data));
          if (!parsed.succeeded) {
            log("[chat-routes] dropping malformed interaction event", { error: parsed.error });
            continue;
          }
          if (renderable(parsed.value.kind)) {
            recordPersistedPart(
              interactionToPersistedPart(parsed.value, "pending"),
              void 0,
              interactionPartKey(parsed.value.id)
            );
            yield toProducerWireEvent(record);
            continue;
          }
          let declineFailed = false;
          if (options.declineInteraction) {
            try {
              await options.declineInteraction(parsed.value.id);
            } catch (err) {
              declineFailed = true;
              log("[chat-routes] failed to auto-decline interaction", {
                id: parsed.value.id,
                error: err instanceof Error ? err.message : String(err)
              });
            }
          } else {
            declineFailed = true;
            log("[chat-routes] non-renderable interaction with no declineInteraction wired", {
              id: parsed.value.id,
              kind: parsed.value.kind
            });
          }
          const text = declineFailed ? `The agent requested ${parsed.value.kind} approval; declining it failed \u2014 it will expire on its own.` : `The agent requested ${parsed.value.kind} approval \u2014 auto-declined by policy.`;
          const notice = noticePart("auto-declined", `auto-declined-${parsed.value.id}`, text);
          recordPersistedPart(notice, void 0, noticePartKey(notice.id));
          yield { type: "notice", id: notice.id, noticeKind: "auto-declined", text };
          continue;
        }
        if (event.type === "interaction.cancel") {
          const parsed = parseInteractionCancel(asRecord(record.data));
          if (!parsed.succeeded) {
            log("[chat-routes] dropping malformed interaction.cancel event", { error: parsed.error });
            continue;
          }
          const key = interactionPartKey(parsed.value.id);
          const existing = partMap.get(key);
          if (existing?.type === "interaction" && existing.status === "pending") {
            recordPersistedPart({
              ...existing,
              status: cancelStatusFor(parsed.value.reason),
              ...parsed.value.reason ? { cancelReason: parsed.value.reason } : {}
            }, void 0, key);
          }
          yield toProducerWireEvent(record);
          continue;
        }
        if (event.type === "plan.submitted") {
          const parsed = parsePlanSubmittedEvent(record);
          if (!parsed.succeeded) {
            log("[chat-routes] dropping malformed plan.submitted event", { error: parsed.error });
            continue;
          }
          recordPersistedPart(planToPersistedPart(parsed.value), void 0);
          yield toProducerWireEvent(record);
          continue;
        }
        if (event.type === "warning") {
          const message = asString(event.data?.message);
          if (message) {
            warningCount += 1;
            const code = asString(event.data?.code);
            const text = code ? `${code}: ${message}` : message;
            const notice = noticePart("warning", `warning-${warningCount}`, text);
            recordPersistedPart(notice, void 0, noticePartKey(notice.id));
            yield { type: "notice", id: notice.id, noticeKind: "warning", text };
          }
          yield toProducerWireEvent(record);
          continue;
        }
        if (event.type === "result") {
          const finalText = asString(event.data?.finalText);
          if (finalText) fullText = finalText;
          const reported = extractReportedTurnUsage(asRecord(event.data));
          if (reported) {
            applyTerminalUsage(reported, usage);
            yield* emitFinalUsage();
          } else {
            const resultUsage = asRecord(event.data?.usage);
            if (resultUsage) {
              const input = Number(resultUsage.inputTokens);
              const output = Number(resultUsage.outputTokens);
              if (Number.isFinite(input)) usage.inputTokens = input;
              if (Number.isFinite(output)) usage.outputTokens = output;
            }
          }
          yield* emitTerminalizedTools();
          continue;
        }
        if (event.type === "done") {
          const reported = extractReportedTurnUsage(asRecord(event.data));
          if (reported) {
            applyTerminalUsage(reported, usage);
            yield* emitFinalUsage();
          }
          yield* emitTerminalizedTools();
          yield toProducerWireEvent(record);
          continue;
        }
        if (event.type === "error") {
          const message = sandboxStreamErrorMessage(event.data);
          const errorContent = fullText.trim() ? `The sandbox model stream stopped before a clean completion.

Error: ${message}` : `The sandbox agent returned an error before producing a visible answer.

Error: ${message}`;
          const errorDelta = fullText ? `

---
${errorContent}` : errorContent;
          fullText += errorDelta;
          interactionOutcome = "expired";
          yield { type: "text", text: errorDelta };
          yield* emitTerminalizedTools();
          yield toProducerWireEvent(record);
          continue;
        }
        yield toProducerWireEvent(record);
      }
      yield* drainModelNotices();
    } catch (streamErr) {
      const diagnostic = sandboxStreamFailureDiagnostic(streamErr);
      log("[chat-routes] sandbox stream failed", {
        failureNote: diagnostic.failureNote,
        error: streamErr instanceof Error ? streamErr.message : String(streamErr)
      });
      const errorDelta = fullText ? `

---
${diagnostic.userMessage}` : diagnostic.userMessage;
      fullText += errorDelta;
      interactionOutcome = "expired";
      yield { type: "text", text: errorDelta };
      yield* emitTerminalizedTools();
      yield {
        type: "error",
        data: {
          message: diagnostic.userMessage,
          code: "sandbox.stream_failed",
          details: { failureNote: diagnostic.failureNote }
        }
      };
    }
  }
  return {
    stream: stream(),
    finalText: () => fullText,
    assistantParts: () => finalizePendingInteractionParts(
      finalizeAssistantParts(partOrder, partMap, fullText),
      interactionOutcome
    ),
    // Mid-stream snapshot for incremental persistence: the same accumulators,
    // WITHOUT the two completion-time settlements (`terminalizeDanglingTool*`
    // and `finalizePendingInteractionParts`). A running tool and an unanswered
    // ask are the correct live state; settling them early would persist
    // phantom failures the final write then reverses.
    draftParts: () => draftAssistantParts(partOrder, partMap, fullText),
    usage: () => usage,
    // A GETTER, not a captured value: `turn-routes` reads `producer.model` at
    // draft-snapshot and persist time, both of which happen after the stream
    // resolved which model serves. A plain property would freeze the PREFERRED
    // model into the row and make a downgrade unattributable — the exact
    // failure mode this work exists to prevent.
    get model() {
      return servingModel();
    },
    modelFailover: () => ({
      model: servingModel(),
      attempts: failover?.attempts() ?? [],
      usedFallback: failover?.usedFallback() ?? false
    })
  };
}

// src/chat-routes/detached-turn.ts
var TERMINAL_ERROR_TYPES = /* @__PURE__ */ new Set(["error", "session.run.failed"]);
function errorMessageOf(ev) {
  const rec = ev;
  const raw = rec?.data?.message ?? rec?.data?.reason ?? rec?.message;
  return typeof raw === "string" && raw ? raw : "run failed";
}
function hasUsage(usage) {
  return typeof usage.inputTokens === "number" && usage.inputTokens > 0;
}
function cachedResultFrom(final) {
  return {
    state: "completed",
    text: final?.text ?? "",
    parts: final?.parts ?? [],
    usage: final?.usage ?? {},
    cached: true
  };
}
async function runDetachedTurn(opts) {
  const { store, turnId, scopeId } = opts;
  let producer;
  const servingModel = () => producer?.model ?? opts.model;
  let draft;
  if (opts.persist) {
    const { store: persistStore, threadId, messageId, transformText, ...tuning } = opts.persist;
    if (!storeSupportsDraftPersistence(persistStore)) {
      throw new Error(
        "runDetachedTurn persist requires a store with updateMessage() \u2014 `/chat-store`'s createChatStore has it"
      );
    }
    draft = createAssistantDraftWriter({
      ...tuning,
      store: persistStore,
      threadId,
      messageId: messageId ?? assistantRowIdForTurn(turnId),
      snapshot: () => producer ? {
        content: producer.finalText?.() ?? "",
        ...producer.draftParts ? { parts: producer.draftParts() } : {},
        ...producer.usage ? { usage: producer.usage() } : {},
        ...servingModel() ? { model: servingModel() } : {}
      } : null,
      ...transformText ? { transformText } : {},
      ...opts.log ? { log: opts.log } : {}
    });
  }
  const settleRow = async (base) => {
    const info = producer?.modelFailover?.();
    const result = {
      ...base,
      ...servingModel() ? { model: servingModel() } : {},
      ...info ? { usedModelFallback: info.usedFallback, modelAttempts: info.attempts } : {}
    };
    if (!draft) return result;
    const transform = opts.persist?.transformText;
    const content = transform ? await transform(result.text) : result.text;
    const rawParts = transform ? await Promise.all(
      result.parts.map(
        async (part) => String(part.type ?? "") === "text" ? { ...part, text: await transform(String(part.text ?? "")) } : part
      )
    ) : result.parts;
    const parts2 = toChatMessageParts(rawParts);
    if (!content.trim() && parts2.length === 0) {
      await draft.discard();
      return { ...result, messageId: null };
    }
    const values = {
      content,
      ...parts2.length > 0 ? { parts: parts2 } : {},
      ...result.model ? { model: result.model } : {},
      ...result.usage.inputTokens !== void 0 ? { inputTokens: result.usage.inputTokens } : {},
      ...result.usage.outputTokens !== void 0 ? { outputTokens: result.usage.outputTokens } : {},
      ...result.usage.reasoningTokens !== void 0 ? { reasoningTokens: result.usage.reasoningTokens } : {},
      ...result.usage.cacheReadTokens !== void 0 ? { cacheReadTokens: result.usage.cacheReadTokens } : {},
      ...result.usage.cacheWriteTokens !== void 0 ? { cacheWriteTokens: result.usage.cacheWriteTokens } : {},
      ...result.usage.costUsd !== void 0 ? { costUsd: result.usage.costUsd } : {}
    };
    await draft.finalize(values);
    return { ...result, messageId: draft.rowId() ?? null };
  };
  const completed = async () => {
    if (!opts.completedResult) return null;
    try {
      return await opts.completedResult() ?? null;
    } catch (err) {
      opts.log?.("[chat-routes] runDetachedTurn completedResult lookup failed", { turnId, err: String(err) });
      return null;
    }
  };
  let prior = null;
  try {
    prior = await store.getStatus(turnId);
  } catch (err) {
    opts.log?.("[chat-routes] runDetachedTurn getStatus failed; treating as no prior", { turnId, err: String(err) });
  }
  if (prior === "complete") return await settleRow(cachedResultFrom(await completed()));
  if (prior === "running") {
    const final = await completed();
    if (final) {
      await store.setStatus(turnId, "complete", scopeId).catch((err) => {
        opts.log?.("[chat-routes] runDetachedTurn failed to settle a completed running turn", { turnId, err: String(err) });
      });
      return await settleRow(cachedResultFrom(final));
    }
    if (opts.resetBuffer) {
      await opts.resetBuffer(turnId).catch((err) => {
        opts.log?.("[chat-routes] runDetachedTurn resetBuffer failed; re-stream may duplicate rows", { turnId, err: String(err) });
      });
    } else {
      opts.log?.("[chat-routes] runDetachedTurn re-streaming over a running buffer without resetBuffer; rows may duplicate", { turnId });
    }
  }
  const tap = createBufferedTurnTap({
    store,
    turnId,
    scopeId,
    coalesce: opts.coalesce ?? coalesceDeltas
  });
  await tap.onEvent({ type: "turn", turnId });
  producer = createSandboxChatProducer({
    ...opts.openEvents ? { openEvents: opts.openEvents } : { events: opts.events },
    model: opts.model,
    ...opts.fallbackModels ? { fallbackModels: opts.fallbackModels } : {},
    ...opts.modelFailover === false ? { modelFailover: false } : {},
    ...opts.onModelFallback ? { onModelFallback: opts.onModelFallback } : {},
    isRenderableInteraction: opts.isRenderableInteraction,
    declineInteraction: opts.declineInteraction,
    promoteFilePart: opts.promoteFilePart,
    log: opts.log
  });
  let runError;
  try {
    for await (const ev of producer.stream) {
      const type = ev.type;
      if (typeof type === "string" && TERMINAL_ERROR_TYPES.has(type)) runError = errorMessageOf(ev);
      await tap.onEvent(ev);
      draft?.notify(ev);
    }
    await tap.done(runError ? "error" : "complete");
  } catch (err) {
    await tap.done("error").catch(() => {
    });
    await draft?.close().catch(() => {
    });
    throw err;
  }
  const text = producer.finalText?.() ?? "";
  const parts = producer.assistantParts?.() ?? [];
  let usage = producer.usage?.() ?? {};
  if (!runError && !hasUsage(usage)) {
    const final = await completed();
    if (final?.usage) usage = { ...usage, ...final.usage };
    if (!text && final?.text) {
      return await settleRow({ state: "completed", text: final.text, parts: final.parts ?? parts, usage, cached: false });
    }
  }
  if (runError) return await settleRow({ state: "failed", text, parts, usage, error: runError, cached: false });
  return await settleRow({ state: "completed", text, parts, usage, cached: false });
}

// src/chat-routes/durable-projection.ts
function errorMessage(error) {
  return error instanceof Error ? error.message : String(error);
}
function withDurableChatProjection(producer, projection, log = (message, meta) => console.error(message, meta ?? "")) {
  let projected = [];
  async function* stream() {
    for await (const event of producer.stream) {
      try {
        await projection.observe(event);
      } catch (error) {
        log("[chat-routes] durable projection observe failed", {
          eventType: event.type,
          error: errorMessage(error)
        });
      }
      yield event;
    }
    try {
      projected = await projection.materialize();
    } catch (error) {
      log("[chat-routes] durable projection materialize failed", {
        error: errorMessage(error)
      });
    }
  }
  return {
    ...producer,
    stream: stream(),
    assistantParts: () => {
      const parts = producer.assistantParts?.() ?? [];
      const order = [];
      const byKey = /* @__PURE__ */ new Map();
      for (const part of [...parts, ...projected]) {
        const key = getPartKey(part);
        if (!byKey.has(key)) order.push(key);
        byKey.set(key, mergePersistedPart(byKey.get(key), part));
      }
      return order.map((key) => byKey.get(key));
    }
  };
}

// src/chat-routes/upload.ts
var UPLOAD_INLINE_MAX_BYTES = 700 * 1024;
var UPLOAD_MAX_FILE_BYTES = 8 * 1024 * 1024;
function sanitizeUploadFilename(name) {
  const base = name.split(/[\\/]/).pop() ?? "file";
  const safe = base.replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^\.+/, "_");
  return (safe || "file").slice(0, 120);
}
var BASE64_CHUNK = 32768;
function bytesToBase64(bytes) {
  let binary = "";
  for (let offset = 0; offset < bytes.length; offset += BASE64_CHUNK) {
    binary += String.fromCharCode(...bytes.subarray(offset, offset + BASE64_CHUNK));
  }
  return btoa(binary);
}
function uploadError(status, code, error) {
  return Response.json({ code, error }, { status });
}
function createUploadRoute(options) {
  const inlineMaxBytes = options.inlineMaxBytes ?? UPLOAD_INLINE_MAX_BYTES;
  const maxFileBytes = options.maxFileBytes ?? UPLOAD_MAX_FILE_BYTES;
  return async function upload(request) {
    const auth = await options.authorize({ request });
    if (!auth.ok) return auth.response;
    const sink = auth.sink ?? null;
    const uploadDir = (auth.uploadDir ?? options.uploadDir ?? "uploads").replace(/\/+$/, "");
    let form;
    try {
      form = await request.formData();
    } catch {
      return uploadError(400, "INVALID_UPLOAD", "Expected a multipart/form-data body with file fields");
    }
    const files = [];
    form.forEach((value) => {
      if (value instanceof File) files.push(value);
    });
    if (files.length === 0) {
      return uploadError(400, "INVALID_UPLOAD", "No files in the upload body");
    }
    const uploaded = [];
    for (const file of files) {
      const name = sanitizeUploadFilename(file.name);
      const mediaType = file.type || "application/octet-stream";
      const partType = mediaType.startsWith("image/") ? "image" : "file";
      if (file.size > maxFileBytes) {
        return uploadError(
          413,
          "FILE_TOO_LARGE",
          `${name} is ${file.size}B, over the ${maxFileBytes}B per-file cap`
        );
      }
      const id = crypto.randomUUID();
      if (file.size <= inlineMaxBytes) {
        const base642 = bytesToBase64(new Uint8Array(await file.arrayBuffer()));
        uploaded.push({
          id,
          name,
          size: file.size,
          mediaType,
          inline: true,
          part: {
            type: partType,
            filename: name,
            mediaType,
            url: `data:${mediaType};base64,${base642}`
          }
        });
        continue;
      }
      if (!sink) {
        return uploadError(
          413,
          "SANDBOX_REQUIRED",
          `${name} is ${file.size}B, over the ${inlineMaxBytes}B inline cap, and no sandbox is available to hold it`
        );
      }
      const path = `${uploadDir}/${id}-${name}`;
      const base64 = bytesToBase64(new Uint8Array(await file.arrayBuffer()));
      await sink.write(path, base64, { encoding: "base64" });
      uploaded.push({
        id,
        name,
        size: file.size,
        mediaType,
        inline: false,
        part: { type: partType, filename: name, mediaType, path }
      });
    }
    return Response.json({ files: uploaded });
  };
}

// src/chat-routes/resolve-attachments.ts
var MAX_ATTACHMENT_NAME_LENGTH = 256;
function isAttachmentKind(value) {
  return value === "image" || value === "file";
}
var CONTROL_CHARS = /[\x00-\x1F\x7F]/;
function defaultValidateAttachmentPath(path) {
  if (path.includes("\0")) return { succeeded: false, error: "attachment path must not contain null bytes" };
  if (path.includes("\\")) return { succeeded: false, error: "attachment path must not contain backslashes" };
  if (CONTROL_CHARS.test(path)) return { succeeded: false, error: "attachment path must not contain control characters" };
  if (path.startsWith("/")) return { succeeded: false, error: "attachment path must be store-relative, not absolute" };
  const segments = path.split("/");
  if (segments.some((segment) => segment === "..")) {
    return { succeeded: false, error: 'attachment path must not contain ".." segments' };
  }
  if (segments.some((segment) => segment.startsWith("."))) {
    return { succeeded: false, error: "attachment path must not contain a hidden (dotfile) segment" };
  }
  return { succeeded: true };
}
function parseAttachmentInput(value, index, validatePath) {
  if (!value || typeof value !== "object" || Array.isArray(value)) {
    return { succeeded: false, error: `attachments[${index}] must be an object` };
  }
  const record = value;
  const path = record.path;
  if (typeof path !== "string" || !path) {
    return { succeeded: false, error: `attachments[${index}].path must be a non-empty string` };
  }
  const name = record.name;
  if (typeof name !== "string" || !name.trim()) {
    return { succeeded: false, error: `attachments[${index}].name must be a non-empty string` };
  }
  if (name.length > MAX_ATTACHMENT_NAME_LENGTH) {
    return { succeeded: false, error: `attachments[${index}].name must not exceed ${MAX_ATTACHMENT_NAME_LENGTH} characters` };
  }
  if (CONTROL_CHARS.test(name)) {
    return { succeeded: false, error: `attachments[${index}].name must not contain control characters` };
  }
  const size = record.size;
  if (typeof size !== "number" || !Number.isFinite(size)) {
    return { succeeded: false, error: `attachments[${index}].size must be a finite number` };
  }
  if (size < 0) {
    return { succeeded: false, error: `attachments[${index}].size must not be negative` };
  }
  const mediaType = record.mediaType;
  if (typeof mediaType !== "string") {
    return { succeeded: false, error: `attachments[${index}].mediaType must be a string` };
  }
  const kind = record.kind;
  if (!isAttachmentKind(kind)) {
    return { succeeded: false, error: `attachments[${index}].kind must be "image" or "file"` };
  }
  const pathCheck = validatePath(path);
  if (!pathCheck.succeeded) return { succeeded: false, error: pathCheck.error };
  return { succeeded: true, value: { path, name, size, mediaType, kind } };
}
async function resolveChatAttachments(value, options) {
  const maxCount = options.maxCount ?? ATTACHMENT_MAX_COUNT;
  const maxTotalBytes = options.maxTotalBytes ?? MAX_ATTACHMENT_TOTAL_BYTES;
  const validatePath = options.validatePath ?? defaultValidateAttachmentPath;
  if (value === void 0 || value === null) return { succeeded: true, value: [] };
  if (!Array.isArray(value)) return { succeeded: false, error: "attachments must be an array" };
  if (value.length > maxCount) {
    return { succeeded: false, error: `attachments must not exceed ${maxCount} entries` };
  }
  const inputs = [];
  const seenPaths = /* @__PURE__ */ new Set();
  for (let index = 0; index < value.length; index += 1) {
    const parsed = parseAttachmentInput(value[index], index, validatePath);
    if (!parsed.succeeded) return parsed;
    if (seenPaths.has(parsed.value.path)) {
      return { succeeded: false, error: `attachments must not repeat a path: ${parsed.value.path}` };
    }
    seenPaths.add(parsed.value.path);
    inputs.push(parsed.value);
  }
  const advisoryTotal = inputs.reduce((sum, input) => sum + input.size, 0);
  if (advisoryTotal > maxTotalBytes) {
    return { succeeded: false, error: attachmentTotalSizeErrorMessage(advisoryTotal, maxTotalBytes) };
  }
  let totalStoredBytes = 0;
  for (const input of inputs) {
    const read = await options.readAttachment(options.scopeId, input.path);
    if (!read.ok) return { succeeded: false, error: read.reason };
    totalStoredBytes += read.size;
    if (totalStoredBytes > maxTotalBytes) {
      return { succeeded: false, error: attachmentTotalSizeErrorMessage(totalStoredBytes, maxTotalBytes) };
    }
    input.size = read.size;
  }
  return { succeeded: true, value: inputs.map(attachmentInputToPart) };
}

// src/chat-routes/promote-file-part.ts
var PROMOTE_MAX_FILE_BYTES = 10 * 1024 * 1024;
var EXT_TO_MIME = {
  md: "text/markdown",
  markdown: "text/markdown",
  txt: "text/plain",
  log: "text/plain",
  csv: "text/csv",
  tsv: "text/tab-separated-values",
  json: "application/json",
  yaml: "text/yaml",
  yml: "text/yaml",
  xml: "application/xml",
  html: "text/html",
  htm: "text/html",
  pdf: "application/pdf",
  png: "image/png",
  jpg: "image/jpeg",
  jpeg: "image/jpeg",
  gif: "image/gif",
  webp: "image/webp",
  avif: "image/avif",
  heic: "image/heic",
  heif: "image/heif",
  svg: "image/svg+xml",
  mp4: "video/mp4",
  webm: "video/webm",
  mov: "video/quicktime",
  mp3: "audio/mpeg",
  wav: "audio/wav",
  ogg: "audio/ogg",
  m4a: "audio/mp4",
  aac: "audio/aac"
};
function sniffMimeFromName(filename) {
  const ext = filename.split(".").pop()?.toLowerCase();
  if (!ext) return "text/plain";
  return EXT_TO_MIME[ext] ?? "text/plain";
}
function base64ToBytes(base64) {
  const binary = atob(base64);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
  return bytes;
}
function parseDataUrl(url) {
  const match = /^data:[^,]*,([\s\S]*)$/.exec(url);
  if (!match) return null;
  return { base64: /;base64,/i.test(url), data: match[1] ?? "" };
}
function dataUrlMime(url) {
  if (!url) return void 0;
  const match = /^data:([^;,]+)[;,]/.exec(url);
  return match ? match[1] : void 0;
}
function basenameFromUrl(url) {
  if (!url || url.startsWith("data:")) return void 0;
  const withoutQuery = url.split(/[?#]/)[0] ?? url;
  const segments = withoutQuery.split("/").filter(Boolean);
  return segments[segments.length - 1] || void 0;
}
function resolveFileUrlPath(url) {
  const withoutScheme = url.startsWith("file://") ? url.slice("file://".length) : url;
  try {
    return { succeeded: true, path: decodeURIComponent(withoutScheme) };
  } catch (err) {
    return { succeeded: false, reason: `malformed file path: ${err instanceof Error ? err.message : String(err)}` };
  }
}
function oversizeReason(filename, actual, limit) {
  return `${filename} is ${formatBytes(actual)}; attachments are limited to ${formatBytes(limit)}`;
}
function resolveDataUrlBytes(url, filename, maxBytes) {
  const parsed = parseDataUrl(url);
  if (!parsed) return { succeeded: false, reason: "malformed data URI" };
  let bytes;
  try {
    bytes = parsed.base64 ? base64ToBytes(parsed.data) : new TextEncoder().encode(decodeURIComponent(parsed.data));
  } catch (err) {
    return { succeeded: false, reason: `failed to decode data URI: ${err instanceof Error ? err.message : String(err)}` };
  }
  if (bytes.byteLength > maxBytes) {
    return { succeeded: false, reason: oversizeReason(filename, bytes.byteLength, maxBytes) };
  }
  return { succeeded: true, bytes };
}
async function resolveSandboxFileBytes(input) {
  const stat = await statSandboxFileSize(input.box, input.path, { sessionId: input.sessionId });
  if (!stat.succeeded) {
    return { succeeded: false, reason: `could not stat agent file: ${stat.error}` };
  }
  if (stat.value > input.maxBytes) {
    return { succeeded: false, reason: oversizeReason(input.filename, stat.value, input.maxBytes) };
  }
  const read = await readSandboxBinaryBytes(input.box, input.path, stat.value, { sessionId: input.sessionId });
  if (!read.succeeded) {
    return { succeeded: false, reason: `could not read agent file: ${read.error}` };
  }
  return { succeeded: true, bytes: read.value.bytes };
}
async function resolveBytes(input) {
  const url = input.raw.url;
  if (!url) return { succeeded: false, reason: "the file part carries no url" };
  if (url.startsWith("data:")) return resolveDataUrlBytes(url, input.filename, input.maxBytes);
  const isSandboxPath = url.startsWith("file://") || url.startsWith("/");
  if (!isSandboxPath) return { succeeded: false, reason: `unsupported file URL scheme: ${url}` };
  if (!input.box) return { succeeded: false, reason: "no sandbox to read agent file" };
  const resolvedPath = resolveFileUrlPath(url);
  if (!resolvedPath.succeeded) return resolvedPath;
  return resolveSandboxFileBytes({
    path: resolvedPath.path,
    box: input.box,
    sessionId: input.sessionId,
    filename: input.filename,
    maxBytes: input.maxBytes
  });
}
async function hash8(seed) {
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(seed));
  return Array.from(new Uint8Array(digest).slice(0, 4)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
}
function defaultBuildAttachmentPath(args) {
  const extensionMatch = /\.[A-Za-z0-9]+$/.exec(args.filename);
  const extension = extensionMatch ? extensionMatch[0] : "";
  const base = extension ? args.filename.slice(0, -extension.length) : args.filename;
  return `uploads/agent/${args.date}/${base}-${args.hash8}${extension}`;
}
async function promoteAgentFilePart(options) {
  const maxBytes = options.maxBytes ?? PROMOTE_MAX_FILE_BYTES;
  const sniffMime = options.sniffMime ?? sniffMimeFromName;
  const buildAttachmentPath = options.buildAttachmentPath ?? defaultBuildAttachmentPath;
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
  const filename = sanitizeAttachmentFileName(
    options.raw.filename ?? basenameFromUrl(options.raw.url) ?? "agent-file"
  );
  const resolved = await resolveBytes({
    raw: options.raw,
    box: options.box,
    sessionId: options.sessionId,
    filename,
    maxBytes
  });
  if (!resolved.succeeded) return { succeeded: false, filename, reason: resolved.reason };
  const mediaType = options.raw.mediaType ?? options.raw.mime ?? dataUrlMime(options.raw.url) ?? sniffMime(filename);
  const kind = attachmentKindForMime(mediaType);
  const digest = await hash8(options.raw.id ?? options.raw.url ?? filename);
  const date = now().toISOString().split("T")[0] ?? "";
  const path = buildAttachmentPath({ filename, hash8: digest, date, mediaType, kind });
  let written;
  try {
    written = await options.writeAttachment(options.scopeId, path, resolved.bytes, {
      mediaType,
      name: filename,
      originalName: options.raw.filename ?? filename,
      size: resolved.bytes.byteLength
    });
  } catch (err) {
    return { succeeded: false, filename, reason: err instanceof Error ? err.message : String(err) };
  }
  if (!written.ok) return { succeeded: false, filename, reason: written.reason };
  return {
    succeeded: true,
    part: {
      type: kind,
      path,
      name: filename,
      size: resolved.bytes.byteLength,
      mediaType
    }
  };
}

// src/chat-routes/attachment-upload.ts
function attachmentUploadError(status, code, message, path) {
  return Response.json(
    { error: path === void 0 ? { code, message } : { code, message, path } },
    { status }
  );
}
function createAttachmentUploadRoute(options) {
  const maxCount = options.limits?.maxCount ?? ATTACHMENT_MAX_COUNT;
  const maxBinaryBytes = options.limits?.maxBinaryBytes ?? MAX_BINARY_ATTACHMENT_BYTES;
  const maxTextBytes = options.limits?.maxTextBytes ?? MAX_TEXT_ATTACHMENT_BYTES;
  const maxTotalBytes = options.limits?.maxTotalBytes ?? MAX_ATTACHMENT_TOTAL_BYTES;
  const allowedKinds = options.allowedKinds ?? ["image", "file"];
  const allowedSniffedMimes = options.allowedSniffedMimes ?? ALLOWED_ATTACHMENT_SNIFFED_MIMES;
  const pathFor = options.pathFor ?? ((name) => name);
  const validatePath = options.validatePath ?? defaultValidateAttachmentPath;
  const sniffMime = options.sniffMime ?? sniffMimeFromName;
  return async function attachmentUpload(request) {
    const auth = await options.authorize({ request });
    if (!auth.ok) return auth.response;
    const write = auth.writeAttachment ?? options.writeAttachment;
    let form;
    try {
      form = await request.formData();
    } catch {
      return attachmentUploadError(400, "invalid_upload", "Expected a multipart/form-data body with file fields");
    }
    const files = [];
    form.forEach((value) => {
      if (value instanceof File) files.push(value);
    });
    if (files.length === 0) {
      return attachmentUploadError(400, "invalid_upload", "No files in the upload body");
    }
    if (files.length > maxCount) {
      return attachmentUploadError(
        400,
        "attachment_count_exceeded",
        `Too many files \u2014 the ${maxCount}-file limit was exceeded`
      );
    }
    const advisoryTotal = files.reduce((sum, file) => sum + file.size, 0);
    if (advisoryTotal > maxTotalBytes) {
      return attachmentUploadError(
        413,
        "attachments_total_too_large",
        attachmentTotalSizeErrorMessage(advisoryTotal, maxTotalBytes)
      );
    }
    const prepared = [];
    const seenPaths = /* @__PURE__ */ new Set();
    let totalBytes = 0;
    for (const file of files) {
      const bytes = new Uint8Array(await file.arrayBuffer());
      const sniff = sniffBinary(bytes);
      const name = sanitizeAttachmentFileName(file.name);
      const typeCheck = checkAttachmentType(name, sniff, allowedSniffedMimes);
      if (!typeCheck.succeeded) {
        return attachmentUploadError(
          typeCheck.code === "attachment_type_mismatch" ? 400 : 415,
          typeCheck.code,
          typeCheck.message
        );
      }
      const kind = attachmentKindForMime(sniff.mime ?? "");
      if (!allowedKinds.includes(kind)) {
        return attachmentUploadError(
          415,
          "attachment_kind_not_allowed",
          `${name} is a "${kind}" attachment, which this upload route does not accept`
        );
      }
      const limit = sniff.binary ? maxBinaryBytes : maxTextBytes;
      if (bytes.length > limit) {
        return attachmentUploadError(
          413,
          "attachment_too_large",
          attachmentSizeErrorMessage(name, bytes.length, limit)
        );
      }
      const path = pathFor(name);
      const pathCheck = validatePath(path);
      if (!pathCheck.succeeded) {
        return attachmentUploadError(400, "invalid_attachment_path", pathCheck.error, path);
      }
      if (seenPaths.has(path)) {
        return attachmentUploadError(
          400,
          "attachment_duplicate_path",
          `attachments must not repeat a path within one upload: ${path}`,
          path
        );
      }
      seenPaths.add(path);
      totalBytes += bytes.length;
      if (totalBytes > maxTotalBytes) {
        return attachmentUploadError(
          413,
          "attachments_total_too_large",
          attachmentTotalSizeErrorMessage(totalBytes, maxTotalBytes)
        );
      }
      const mediaType = sniff.mime ?? sniffMime(name);
      prepared.push({ path, name, bytes, originalName: file.name, size: bytes.length, mediaType, kind });
    }
    const uploaded = [];
    for (const input of prepared) {
      const written = await write(auth.scopeId, input.path, input.bytes, {
        mediaType: input.mediaType,
        name: input.name,
        originalName: input.originalName,
        size: input.size
      });
      if (!written.ok) {
        return attachmentUploadError(413, "attachment_write_failed", written.reason, input.path);
      }
      uploaded.push({
        path: input.path,
        name: input.name,
        size: input.size,
        mediaType: input.mediaType,
        kind: input.kind
      });
    }
    return Response.json({ files: uploaded });
  };
}

// src/chat-routes/dispatch-parts.ts
function byteLen(value) {
  return new TextEncoder().encode(value).length;
}
async function readSandboxMention(box, absolutePath, options) {
  const stat = await statSandboxFileSize(box, absolutePath);
  if (!stat.succeeded) {
    return { succeeded: false, error: `mentioned sandbox file missing or unreadable: ${absolutePath} \u2014 ${stat.error}` };
  }
  if (!options.readBytes) return { succeeded: true, value: { size: stat.value } };
  const read = await readSandboxBinaryBytes(box, absolutePath, stat.value);
  if (!read.succeeded) {
    return { succeeded: false, error: `mentioned sandbox file read failed: ${absolutePath} \u2014 ${read.error}` };
  }
  return { succeeded: true, value: { size: stat.value, base64: bytesToBase64(read.value.bytes) } };
}
function violatesUrlPathXor(part) {
  if (part.type === "text") return false;
  const hasUrl = typeof part.url === "string" && part.url.startsWith("data:");
  const hasPath = typeof part.path === "string" && part.path.startsWith("/");
  return hasUrl === hasPath;
}
function readResultToBase64(read) {
  if (typeof read.base64 === "string") return read.base64;
  if (read.bytes) return bytesToBase64(read.bytes);
  return void 0;
}
async function buildDispatchParts(input) {
  const readMention = input.readSandboxMention ?? readSandboxMention;
  const resolveMentionPath = input.resolveMentionPath ?? input.resolveAttachmentPath;
  const forcePath = input.forcePath ?? false;
  const mentions = input.mentions ?? [];
  const requestMaxBytes = input.requestMaxBytes ?? DISPATCH_REQUEST_MAX_BYTES;
  const structuralReserveBytes = input.structuralReserveBytes ?? DISPATCH_STRUCTURAL_RESERVE_BYTES;
  const maxParts = input.maxParts ?? DISPATCH_MAX_PARTS;
  const parts = [{ type: "text", text: input.text }];
  const emittedAbsPaths = /* @__PURE__ */ new Set();
  const flattenedForSizing = flattenHistory(input.text, input.history);
  const inlineBudget = requestMaxBytes - base64WireLen(byteLen(flattenedForSizing)) - byteLen(JSON.stringify(input.systemPrompt)) - input.profileWireBytes - structuralReserveBytes;
  let runningInline = 0;
  for (const attachment of input.attachments) {
    if (!attachment.path) {
      return { succeeded: false, error: `attachment path must be non-empty: ${attachment.name}` };
    }
    let read;
    try {
      read = await input.readAttachment(input.scopeId, attachment.path);
    } catch (err) {
      return {
        succeeded: false,
        error: `attachment store read failed: ${attachment.path} \u2014 ${err instanceof Error ? err.message : String(err)}`
      };
    }
    if (!read.ok) return { succeeded: false, error: read.reason };
    const base64 = readResultToBase64(read);
    if (base64 === void 0) {
      return { succeeded: false, error: `attachment store read produced no content: ${attachment.path}` };
    }
    const mediaType = attachment.mediaType ?? read.mediaType;
    if (attachment.type === "image" && !mediaType) {
      return { succeeded: false, error: `attachment is missing a mediaType required for an image data URI: ${attachment.path}` };
    }
    const absPath = input.resolveAttachmentPath(attachment.path);
    emittedAbsPaths.add(absPath);
    if (attachment.type === "image") {
      const inlinePart2 = {
        type: "image",
        filename: attachment.name,
        mediaType,
        url: `data:${mediaType};base64,${base64}`
      };
      const cost2 = byteLen(JSON.stringify(inlinePart2));
      if (!forcePath && runningInline + cost2 <= inlineBudget) {
        parts.push(inlinePart2);
        runningInline += cost2;
      } else {
        parts.push({ type: "image", filename: attachment.name, mediaType, path: absPath });
      }
      continue;
    }
    const fileMediaType = mediaType ?? "application/octet-stream";
    const inlinePart = {
      type: "file",
      filename: attachment.name,
      mediaType: fileMediaType,
      url: `data:${fileMediaType};base64,${base64}`
    };
    const cost = byteLen(JSON.stringify(inlinePart));
    if (!forcePath && runningInline + cost <= inlineBudget) {
      parts.push(inlinePart);
      runningInline += cost;
    } else {
      parts.push({ type: "file", path: absPath });
    }
  }
  if (mentions.length > 0 && !input.box) {
    return { succeeded: false, error: "internal error: sandbox mentions require a box to read from" };
  }
  for (const mention of mentions) {
    if (!mention.path) {
      return { succeeded: false, error: `mention path must be non-empty: ${mention.name}` };
    }
    const absPath = resolveMentionPath(mention.path);
    if (emittedAbsPaths.has(absPath)) continue;
    emittedAbsPaths.add(absPath);
    const isImage = mention.mentionKind === "image";
    const mediaType = isImage ? mediaTypeForMentionPath(mention.path) : void 0;
    let stat;
    try {
      stat = await readMention(input.box, absPath, { readBytes: false });
    } catch (err) {
      return { succeeded: false, error: `mention read failed: ${absPath} \u2014 ${err instanceof Error ? err.message : String(err)}` };
    }
    if (!stat.succeeded) return { succeeded: false, error: stat.error };
    const projectedInlineCost = base64WireLen(stat.value.size) + byteLen(JSON.stringify({ type: "image", filename: mention.name, mediaType: mediaType ?? "", url: "" }));
    if (isImage && mediaType && !forcePath && runningInline + projectedInlineCost <= inlineBudget) {
      let read;
      try {
        read = await readMention(input.box, absPath, { readBytes: true });
      } catch (err) {
        return { succeeded: false, error: `mention read failed: ${absPath} \u2014 ${err instanceof Error ? err.message : String(err)}` };
      }
      if (!read.succeeded) return { succeeded: false, error: read.error };
      if (!read.value.base64) return { succeeded: false, error: `mentioned image produced no bytes: ${absPath}` };
      const inlinePart = {
        type: "image",
        filename: mention.name,
        mediaType,
        url: `data:${mediaType};base64,${read.value.base64}`
      };
      const cost = byteLen(JSON.stringify(inlinePart));
      if (runningInline + cost <= inlineBudget) {
        parts.push(inlinePart);
        runningInline += cost;
        continue;
      }
    }
    parts.push(
      isImage && mediaType ? { type: "image", filename: mention.name, mediaType, path: absPath } : { type: "file", path: absPath }
    );
  }
  for (const part of parts) {
    if (violatesUrlPathXor(part)) {
      return { succeeded: false, error: "internal error: emitted media part violates the url/path exclusivity invariant" };
    }
  }
  const textPartSize = base64WireLen(byteLen(flattenedForSizing));
  const mediaPartsSize = parts.slice(1).reduce((total, part) => total + byteLen(JSON.stringify(part)), 0);
  const systemPromptSize = byteLen(JSON.stringify(input.systemPrompt));
  if (textPartSize + mediaPartsSize + systemPromptSize + input.profileWireBytes + structuralReserveBytes > requestMaxBytes) {
    return { succeeded: false, error: "dispatch parts exceed the sandbox proxy request cap even after path demotion" };
  }
  if (parts.length > maxParts) {
    return { succeeded: false, error: `dispatch parts exceed the sidecar per-request cap of ${maxParts}` };
  }
  return { succeeded: true, value: parts };
}
export {
  ALLOWED_ATTACHMENT_SNIFFED_MIMES,
  ATTACHMENT_ACCEPT,
  ATTACHMENT_MAX_COUNT,
  ChatTurnInputError,
  DEFAULT_STALE_TURN_LOCK_GRACE_MS,
  DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS,
  DISPATCH_MAX_MEDIA_PARTS,
  DISPATCH_MAX_PARTS,
  DISPATCH_REQUEST_MAX_BYTES,
  DISPATCH_STRUCTURAL_RESERVE_BYTES,
  INLINE_PARTS_MAX_BYTES,
  MAX_ATTACHMENT_TOTAL_BYTES,
  MAX_BINARY_ATTACHMENT_BYTES,
  MAX_EMPTY_TURN_RETRIES,
  MAX_TEXT_ATTACHMENT_BYTES,
  MENTION_MAX_COUNT,
  PROMOTE_MAX_FILE_BYTES,
  UPLOAD_INLINE_MAX_BYTES,
  UPLOAD_MAX_FILE_BYTES,
  assertPromptPartsWithinCap,
  assistantRowIdForTurn,
  attachmentSizeErrorMessage,
  attachmentTotalSizeErrorMessage,
  base64WireLen,
  buildDispatchParts,
  buildMentionPromptBlock,
  bytesToBase64,
  chatTurnRequestInit,
  checkAttachmentType,
  classifyTerminalFailure,
  createAssistantDraftWriter,
  createAttachmentUploadRoute,
  createChatTurnRoutes,
  createSandboxChatProducer,
  createSandboxFileIndexRoute,
  createUploadRoute,
  defaultValidateAttachmentPath,
  fileMentionsToParts,
  formatBytes,
  isCommittingSandboxEvent,
  isDraftContentEvent,
  mediaTypeForMentionPath,
  mentionKindForPath,
  parseChatTurnParts,
  parseFileMentions,
  promoteAgentFilePart,
  promptPartsByteSize,
  reconcileStaleTurnLock,
  resolveChatAttachments,
  resolveEmptyTurnRetries,
  rowIdOf,
  runDetachedTurn,
  sanitizeAttachmentFileName,
  sanitizeUploadFilename,
  sniffBinary,
  sniffMimeFromName,
  storeSupportsDraftPersistence,
  streamWithModelFailover,
  validateSandboxMentionPath,
  withDurableChatProjection
};
//# sourceMappingURL=index.js.map