import {
  createSandboxFileIndexRoute
} from "../chunk-LCNY3DCM.js";
import {
  asRecord,
  asString,
  coalesceDeltas,
  createBufferedTurnTap,
  finalizeAssistantParts,
  getPartKey,
  mergePersistedPart,
  normalizeClientTurnId,
  normalizePersistedPart,
  normalizeToolEvent,
  replayTurnEvents,
  resolveChatTurn
} from "../chunk-4AUQIAYU.js";
import {
  createInteractionAnswerRoute
} from "../chunk-6KVH5SP5.js";
import {
  parseJsonObjectBody
} from "../chunk-HFC4BTWJ.js";
import {
  ChatTurnInputError,
  INLINE_PARTS_MAX_BYTES,
  MENTION_MAX_COUNT,
  assertPromptPartsWithinCap,
  buildMentionPromptBlock,
  chatTurnRequestInit,
  fileMentionsToParts,
  mediaTypeForMentionPath,
  mentionInputToPart,
  mentionKindForPath,
  parseChatTurnParts,
  parseFileMentions,
  promptPartsByteSize,
  toChatMessageParts,
  validateSandboxMentionPath
} from "../chunk-6E2XJSCT.js";
import {
  cancelStatusFor,
  interactionPartKey,
  interactionToPersistedPart,
  isRenderableInteractionKind,
  parseInteractionCancel,
  parseInteractionRequest
} from "../chunk-XAWFPMAR.js";
import {
  parsePlanSubmittedEvent,
  planToPersistedPart
} from "../chunk-SIXYZ2FB.js";

// src/chat-routes/turn-routes.ts
import { deriveExecutionId, handleChatTurn } from "@tangle-network/agent-runtime";
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 ?? ""));
  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
    }));
    const chatTurn = resolveChatTurn({ existingMessages, userContent: content, turnId });
    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 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 {
          await lifecycle.onTurnComplete?.({
            identity,
            executionId,
            turnStreamId,
            context,
            durationMs,
            finalText: producer?.finalText() ?? "",
            usage: producer?.usage?.() ?? {}
          });
        }
      } 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);
      if (insertUserMessage) {
        await options.store.appendMessage({
          threadId: payload.threadId,
          role: "user",
          content,
          parts: userPartsWithFiles(chatTurn.userParts, fileParts, mentions)
        });
      }
      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);
      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);
            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)) return;
            const usage = producer?.usage?.() ?? {};
            await options.store.appendMessage({
              threadId: payload.threadId,
              role: "assistant",
              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 } : {}
            });
          },
          ...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 }) => options.onTurnComplete({
              identity: turnIdentity,
              finalText,
              context,
              failed: runFailed,
              ...runFailed ? { failureReason: failureReasonOf(lastFailureData) } : {}
            })
          } : {},
          ...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;
        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(`${value.event}
`));
      },
      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/stale-turn-lock.ts
var DEFAULT_STALE_TURN_LOCK_GRACE_MS = 5 * 60 * 1e3;
var DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS = 60 * 1e3;
function messageOf(err) {
  return err instanceof Error ? err.message : String(err);
}
async function reconcileStaleTurnLock(options) {
  let sandbox;
  try {
    sandbox = await options.probeSandbox();
  } catch (err) {
    return forceReleaseUnreachable(options, {
      unreachableReason: "SANDBOX_PROBE_FAILED",
      unreachableDetail: messageOf(err)
    });
  }
  if (sandbox.status !== "running") {
    return forceReleaseUnreachable(options, {
      unreachableReason: sandbox.status === "absent" ? "SANDBOX_ABSENT" : "SANDBOX_NOT_RUNNING",
      ...sandbox.status === "not-running" && sandbox.state !== void 0 ? { sandboxState: sandbox.state } : {}
    });
  }
  const observedAt = (options.now ?? Date.now)();
  let session;
  try {
    session = await options.probeSession();
  } catch (err) {
    return forceReleaseUnreachable(options, {
      unreachableReason: "SESSION_PROBE_FAILED",
      unreachableDetail: messageOf(err)
    });
  }
  if (!session.reachable) {
    return forceReleaseUnreachable(options, {
      unreachableReason: "SESSION_UNREACHABLE",
      ...session.reason !== void 0 ? { sessionProbeError: session.reason } : {}
    });
  }
  const diagnostics = {
    sandboxReachable: true,
    sessionTerminal: session.terminal,
    ...session.diagnostics ?? {}
  };
  if (!session.terminal) return { released: false, diagnostics };
  const terminalGraceMs = options.terminalGraceMs ?? DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS;
  const lockAgeMs = observedAt - options.lockStartedAt;
  if (lockAgeMs < terminalGraceMs) {
    const log = options.log ?? ((message, meta) => console.warn(message, meta));
    const withheld = {
      ...diagnostics,
      lockAgeMs,
      terminalReleaseGraceMs: terminalGraceMs,
      terminalReleaseWithheld: "LOCK_WITHIN_TERMINAL_GRACE_PERIOD"
    };
    log("[chat-routes] stale turn lock held: terminal verdict but lock is younger than the registration window", {
      ...options.context ?? {},
      ...withheld
    });
    return { released: false, diagnostics: withheld };
  }
  const released = await options.release({ observedAt });
  return {
    released,
    diagnostics: { ...diagnostics, lockAgeMs, terminalReleaseGraceMs: terminalGraceMs, released, observedAt }
  };
}
async function forceReleaseUnreachable(options, reason) {
  const log = options.log ?? ((message, meta) => console.warn(message, meta));
  const graceMs = options.graceMs ?? DEFAULT_STALE_TURN_LOCK_GRACE_MS;
  const at = (options.now ?? Date.now)();
  const lockAgeMs = at - options.lockStartedAt;
  const diagnostics = {
    ...reason,
    sandboxReachable: false,
    lockAgeMs,
    forceReleaseGraceMs: graceMs
  };
  if (lockAgeMs < graceMs) {
    log("[chat-routes] stale turn lock held: sandbox unreachable but lock is inside the grace period", {
      ...options.context ?? {},
      ...diagnostics
    });
    return { released: false, diagnostics: { ...diagnostics, forceReleaseWithheld: "LOCK_WITHIN_GRACE_PERIOD" } };
  }
  const released = await options.release({ observedAt: at });
  log("[chat-routes] force-released stale turn lock: sandbox unreachable, no turn can be executing", {
    ...options.context ?? {},
    ...diagnostics,
    released
  });
  return { released, diagnostics: { ...diagnostics, forceReleased: released } };
}

// 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 createSandboxChatProducer(options) {
  const log = options.log ?? ((message, meta) => console.error(message, meta ?? ""));
  const renderable = options.isRenderableInteraction ?? isRenderableInteractionKind;
  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;
  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));
  }
  async function* stream() {
    for await (const raw of options.events) {
      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 : record;
      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;
        }
        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 event;
          continue;
        }
        if (options.declineInteraction) {
          try {
            await options.declineInteraction(parsed.value.id);
          } catch (err) {
            log("[chat-routes] failed to auto-decline interaction", {
              id: parsed.value.id,
              error: err instanceof Error ? err.message : String(err)
            });
          }
        } else {
          log("[chat-routes] non-renderable interaction with no declineInteraction wired", {
            id: parsed.value.id,
            kind: parsed.value.kind
          });
        }
        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 event;
        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 event;
        continue;
      }
      if (event.type === "result") {
        const finalText = asString(event.data?.finalText);
        if (finalText) fullText = finalText;
        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;
        }
        continue;
      }
      yield event;
    }
  }
  return {
    stream: stream(),
    finalText: () => fullText,
    assistantParts: () => finalizeAssistantParts(partOrder, partMap, fullText),
    usage: () => usage,
    ...options.model ? { model: options.model } : {}
  };
}

// 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 });
  };
}
export {
  ChatTurnInputError,
  DEFAULT_STALE_TURN_LOCK_GRACE_MS,
  DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS,
  INLINE_PARTS_MAX_BYTES,
  MENTION_MAX_COUNT,
  UPLOAD_INLINE_MAX_BYTES,
  UPLOAD_MAX_FILE_BYTES,
  assertPromptPartsWithinCap,
  buildMentionPromptBlock,
  bytesToBase64,
  chatTurnRequestInit,
  createChatTurnRoutes,
  createSandboxChatProducer,
  createSandboxFileIndexRoute,
  createUploadRoute,
  fileMentionsToParts,
  mediaTypeForMentionPath,
  mentionKindForPath,
  parseChatTurnParts,
  parseFileMentions,
  promptPartsByteSize,
  reconcileStaleTurnLock,
  sanitizeUploadFilename,
  validateSandboxMentionPath,
  withDurableChatProjection
};
//# sourceMappingURL=index.js.map