@tangle-network/agent-app 0.43.40 → 0.43.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/assistant/index.d.ts +3 -2
  2. package/dist/assistant/index.js +4 -3
  3. package/dist/assistant/index.js.map +1 -1
  4. package/dist/chat-routes/index.d.ts +19 -5
  5. package/dist/chat-routes/index.js +100 -7
  6. package/dist/chat-routes/index.js.map +1 -1
  7. package/dist/chat-store/index.d.ts +4 -3
  8. package/dist/chat-store/index.js +5 -1
  9. package/dist/chat-store/index.js.map +1 -1
  10. package/dist/chunk-2EO7CPL3.js +191 -0
  11. package/dist/chunk-2EO7CPL3.js.map +1 -0
  12. package/dist/{chunk-Y4QHNQ75.js → chunk-6KVH5SP5.js} +110 -6
  13. package/dist/chunk-6KVH5SP5.js.map +1 -0
  14. package/dist/{chunk-XYWCGFII.js → chunk-B5JD3DXD.js} +48 -2
  15. package/dist/chunk-B5JD3DXD.js.map +1 -0
  16. package/dist/{chunk-I2R2XT4M.js → chunk-I2ATYB7R.js} +18 -2
  17. package/dist/chunk-I2ATYB7R.js.map +1 -0
  18. package/dist/chunk-SIXYZ2FB.js +101 -0
  19. package/dist/chunk-SIXYZ2FB.js.map +1 -0
  20. package/dist/{chunk-4TXDD6P2.js → chunk-XAWFPMAR.js} +38 -3
  21. package/dist/chunk-XAWFPMAR.js.map +1 -0
  22. package/dist/{chunk-D4HB72W2.js → chunk-ZLHK25C3.js} +1890 -1213
  23. package/dist/chunk-ZLHK25C3.js.map +1 -0
  24. package/dist/chunk-ZU5GNSOJ.js +920 -0
  25. package/dist/chunk-ZU5GNSOJ.js.map +1 -0
  26. package/dist/{contract-DYbTzEDf.d.ts → contract-KfqJh_au.d.ts} +23 -2
  27. package/dist/design-canvas-react/index.js +4 -4
  28. package/dist/durable-chat/index.d.ts +419 -0
  29. package/dist/durable-chat/index.js +59 -0
  30. package/dist/durable-chat/index.js.map +1 -0
  31. package/dist/file-index-Bw_IQE_G.d.ts +194 -0
  32. package/dist/index.d.ts +5 -3
  33. package/dist/index.js +123 -45
  34. package/dist/interactions/index.d.ts +58 -3
  35. package/dist/interactions/index.js +7 -3
  36. package/dist/{parts-BcbitSNp.d.ts → parts-DjX0RRTS.d.ts} +9 -3
  37. package/dist/plans/index.d.ts +69 -0
  38. package/dist/plans/index.js +21 -0
  39. package/dist/plans/index.js.map +1 -0
  40. package/dist/stream/index.js +3 -1
  41. package/dist/teams/index.js +9 -9
  42. package/dist/teams/invitations-api.js +3 -3
  43. package/dist/web-react/index.d.ts +345 -94
  44. package/dist/web-react/index.js +50 -5
  45. package/package.json +11 -1
  46. package/dist/chunk-4TXDD6P2.js.map +0 -1
  47. package/dist/chunk-5SV5PSU7.js +0 -80
  48. package/dist/chunk-5SV5PSU7.js.map +0 -1
  49. package/dist/chunk-D4HB72W2.js.map +0 -1
  50. package/dist/chunk-I2R2XT4M.js.map +0 -1
  51. package/dist/chunk-XYWCGFII.js.map +0 -1
  52. package/dist/chunk-Y4QHNQ75.js.map +0 -1
  53. package/dist/wire-BaUF66AS.d.ts +0 -61
@@ -5,11 +5,14 @@ import {
5
5
  } from "../chunk-5EQCITY3.js";
6
6
  import {
7
7
  isChatInteractionPart,
8
+ isChatPlanPart,
8
9
  isChatStepFinishPart,
9
10
  isChatTextPart,
10
11
  isChatToolPart,
11
12
  toChatMessageParts
12
- } from "../chunk-I2R2XT4M.js";
13
+ } from "../chunk-I2ATYB7R.js";
14
+ import "../chunk-XAWFPMAR.js";
15
+ import "../chunk-SIXYZ2FB.js";
13
16
 
14
17
  // src/chat-store/schema.ts
15
18
  import { sql } from "drizzle-orm";
@@ -189,6 +192,7 @@ export {
189
192
  createChatStore,
190
193
  createChatTables,
191
194
  isChatInteractionPart,
195
+ isChatPlanPart,
192
196
  isChatStepFinishPart,
193
197
  isChatTextPart,
194
198
  isChatToolPart,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/chat-store/schema.ts","../../src/chat-store/store.ts"],"sourcesContent":["/**\n * Drizzle schema factory for the chat thread/message tables — the same\n * injection pattern as `createTeamTables`: the product owns the workspace\n * table; the factory wires the thread FK into it so the whole graph lives in\n * one drizzle schema with real cascade semantics. Column names, types,\n * defaults, enums, and indexes mirror legal's and gtm's hand-rolled `thread`/\n * `message` tables so a product with those tables adopts the factory without\n * rewriting rows; `tablePrefix` covers products that namespace (tax's\n * `chat_messages` style).\n *\n * The core is the superset the three products agree on. Divergences dropped,\n * and why:\n * - `thread.status` ('active'|'archived', legal+gtm) — archive semantics\n * diverge (tax uses `archivedAt`); product-domain lifecycle → extra column.\n * - `thread.scopeKind`/`scopeKey`/`harness` (gtm) — artifact anchoring and\n * harness pinning are product-domain → extra columns.\n * - tax's `tax_sessions` session columns (`taxYear`, `projectRef`,\n * `agentSessionId`, `agentRuntime`, `agentHarness`, `profile`, `error`,\n * `userId`) — sandbox-session state, not chat state → extra columns.\n * - `message.toolInput`/`toolOutput` (legal+gtm) — duplicate of the tool\n * part's `state.input`/`state.output` inside `parts` (the shape `/stream`'s\n * `normalizePersistedPart` owns); keeping both invites drift.\n * - `message.vaultFiles` (legal+gtm) — vault is product-domain → extra column.\n * - tax's re-declared `turn_events`/`turn_status` DDL — deliberately NOT here;\n * `/stream`'s turn-buffer owns that DDL (`TURN_BUFFER_D1_SCHEMA_SQL`).\n *\n * Kept beyond the intersection: tax's per-message `model`/`inputTokens`/\n * `outputTokens`, extended to the full usage receipt the harness actually\n * reports in `step-finish` parts (`tokens {input, output, reasoning,\n * cache{read, write}}` + `cost`) — see `./parts`.\n *\n * `threadExtraColumns`/`messageExtraColumns` merge product columns into the\n * table definitions (the `/missions` opaque-extras pattern: the store writes\n * `extras` values verbatim in the SAME insert statement and never reads,\n * validates, or defaults them).\n *\n * SERVER-side module (D1/libsql/better-sqlite3 behind a worker or server\n * route) — but free of `node:` builtins on purpose: D1 workers have none.\n */\n\nimport { sql } from 'drizzle-orm'\nimport { index, integer, real, sqliteTable, text } from 'drizzle-orm/sqlite-core'\nimport type { AnySQLiteColumn, AnySQLiteTable, SQLiteColumnBuilderBase } from 'drizzle-orm/sqlite-core'\nimport type { ChatMessagePart } from './parts'\n\n/** A product table referenced by FK — only the `id` column is touched. */\nexport type ChatParentTable = AnySQLiteTable & { id: AnySQLiteColumn }\n\nexport interface CreateChatTablesOptions<\n TThreadExtras extends Record<string, SQLiteColumnBuilderBase> = {},\n TMessageExtras extends Record<string, SQLiteColumnBuilderBase> = {},\n> {\n /** The product's workspace table — threads reference `workspaceTable.id`\n * with cascade. Omitted: `workspace_id` stays a plain indexed text column\n * (products whose tenant table lives in another database). */\n workspaceTable?: ChatParentTable\n /** Prefixes table AND index names (`'chat_'` → `chat_thread`,\n * `idx_chat_thread_workspace`) for products that namespace chat tables in a\n * shared database. Default: unprefixed `thread`/`message` (legal/gtm row\n * compatibility). */\n tablePrefix?: string\n /** Product columns merged into the thread table (the `/missions` extras\n * pattern) — e.g. a `status` lifecycle enum or gtm's scope columns. */\n threadExtraColumns?: TThreadExtras\n /** Product columns merged into the message table — e.g. legal's\n * `vault_files`. */\n messageExtraColumns?: TMessageExtras\n}\n\nconst hexId = () => text('id').primaryKey().default(sql`(lower(hex(randomblob(16))))`)\n\nconst createdAt = () => integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`)\n\nconst updatedAt = () => integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`)\n\nexport function createChatTables<\n TThreadExtras extends Record<string, SQLiteColumnBuilderBase> = {},\n TMessageExtras extends Record<string, SQLiteColumnBuilderBase> = {},\n>(options: CreateChatTablesOptions<TThreadExtras, TMessageExtras> = {}) {\n const { workspaceTable, tablePrefix = '' } = options\n const threadExtras = options.threadExtraColumns ?? ({} as TThreadExtras)\n const messageExtras = options.messageExtraColumns ?? ({} as TMessageExtras)\n\n const threads = sqliteTable(`${tablePrefix}thread`, {\n id: hexId(),\n workspaceId: workspaceTable\n ? text('workspace_id').notNull().references(() => workspaceTable.id, { onDelete: 'cascade' })\n : text('workspace_id').notNull(),\n title: text('title').notNull(),\n category: text('category'),\n isPinned: integer('is_pinned', { mode: 'boolean' }).notNull().default(false),\n createdAt: createdAt(),\n updatedAt: updatedAt(),\n ...threadExtras,\n }, (table) => [\n index(`idx_${tablePrefix}thread_workspace`).on(table.workspaceId),\n // Supports the store's list ordering (updatedAt desc within a workspace).\n index(`idx_${tablePrefix}thread_workspace_updated`).on(table.workspaceId, table.updatedAt),\n ])\n\n const messages = sqliteTable(`${tablePrefix}message`, {\n id: hexId(),\n threadId: text('thread_id').notNull().references(() => threads.id, { onDelete: 'cascade' }),\n role: text('role', { enum: ['user', 'assistant', 'system', 'tool'] }).notNull(),\n content: text('content').notNull(),\n parts: text('parts', { mode: 'json' }).$type<ChatMessagePart[]>().default([]),\n toolName: text('tool_name'),\n model: text('model'),\n // Usage receipt, flattened from the harness's `step-finish` shape\n // (`tokens {input, output, reasoning, cache{read, write}}` + `cost`).\n inputTokens: integer('input_tokens'),\n outputTokens: integer('output_tokens'),\n reasoningTokens: integer('reasoning_tokens'),\n cacheReadTokens: integer('cache_read_tokens'),\n cacheWriteTokens: integer('cache_write_tokens'),\n costUsd: real('cost_usd'),\n createdAt: createdAt(),\n ...messageExtras,\n }, (table) => [\n index(`idx_${tablePrefix}message_thread`).on(table.threadId),\n index(`idx_${tablePrefix}message_thread_created`).on(table.threadId, table.createdAt),\n ])\n\n return { threads, messages }\n}\n\n/**\n * The base (no-extras) table pair, pinned via an instantiation expression:\n * `ReturnType<typeof createChatTables>` on the bare generic substitutes the\n * extras params with their CONSTRAINT (`Record<string,\n * SQLiteColumnBuilderBase>`), stamping an index signature into the column map\n * that widens every concrete column to `unknown`/`notNull: false` — concrete\n * factory results then fail `extends ChatTables`. (`teams`' `createTeamTables`\n * is non-generic, so its plain `ReturnType` never hits this.)\n */\nexport type ChatTables = ReturnType<typeof createChatTables<{}, {}>>\n\nexport type ChatThreadRow = ChatTables['threads']['$inferSelect']\nexport type ChatMessageRow = ChatTables['messages']['$inferSelect']\nexport type NewChatThreadRow = ChatTables['threads']['$inferInsert']\nexport type NewChatMessageRow = ChatTables['messages']['$inferInsert']\n","/**\n * Typed CRUD over the tables from `createChatTables`. Works against any\n * SQLite drizzle driver (D1, libsql, better-sqlite3) — builders are awaited,\n * never `.run()`/`.all()`, so sync and async drivers behave identically.\n *\n * Access control is an injected seam, never an import: single-thread routes\n * check workspace access themselves (they know the thread), while\n * `bulkDeleteThreads` REQUIRES an `assertAccess` callback because one request\n * can span workspaces — it is called once per distinct workspace and any\n * throw rejects the whole request before a single delete runs (fail-closed;\n * legal's bulk-delete semantics).\n *\n * Deletes run messages-first in ONE `db.batch` round trip when the driver has\n * one (D1, libsql), so a partial failure never leaves orphaned rows behind a\n * deleted thread; drivers without `batch` (better-sqlite3) fall back to\n * sequential awaits in the same order.\n */\n\nimport { asc, desc, eq, inArray, sql } from 'drizzle-orm'\nimport type { BaseSQLiteDatabase } from 'drizzle-orm/sqlite-core'\nimport { BULK_DELETE_MAX_THREADS, ChatStoreInputError, threadTitleFromMessage } from './core'\nimport type { ChatMessagePart } from './parts'\nimport type { ChatMessageRow, ChatTables, ChatThreadRow, NewChatMessageRow, NewChatThreadRow } from './schema'\n\n/** Any SQLite drizzle database — `any` erases the driver-specific run-result\n * and schema generics so better-sqlite3, D1, and libsql handles all fit.\n * `batch` is structural: present on D1/libsql drizzle instances. */\nexport type ChatDatabase = BaseSQLiteDatabase<'sync' | 'async', any, any> & {\n batch?: (statements: [unknown, ...unknown[]]) => Promise<unknown[]>\n}\n\n/** Product-injected access check. Throw to deny; the store never interprets\n * users or roles itself. */\nexport type WorkspaceAccessCheck = (workspaceId: string) => void | Promise<void>\n\nexport interface ListThreadsInput {\n workspaceId: string\n /** Clamped to 1..200; default 50 (legal's list route semantics). */\n limit?: number\n /** Clamped to >= 0; default 0. */\n offset?: number\n}\n\nexport interface ListThreadsResult<TThread = ChatThreadRow> {\n threads: TThread[]\n total: number\n limit: number\n offset: number\n}\n\nexport interface CreateThreadInput {\n workspaceId: string\n /** Title source when `title` is absent: first non-empty line, 80-char cap\n * (`threadTitleFromMessage`). */\n firstMessage?: string\n /** Explicit title; still normalized through `threadTitleFromMessage` so a\n * multi-page paste never becomes a sidebar entry. */\n title?: string\n category?: string | null\n isPinned?: boolean\n /** Opaque product-column values written verbatim in the SAME insert (the\n * `/missions` extras pattern). Never read, validated, or defaulted here. */\n extras?: Record<string, unknown>\n}\n\nexport interface AppendMessageInput {\n threadId: string\n role: 'user' | 'assistant' | 'system' | 'tool'\n content: string\n parts?: ChatMessagePart[]\n toolName?: string | null\n model?: string | null\n inputTokens?: number | null\n outputTokens?: number | null\n reasoningTokens?: number | null\n cacheReadTokens?: number | null\n cacheWriteTokens?: number | null\n costUsd?: number | null\n /** Opaque product-column values written verbatim in the SAME insert. */\n extras?: Record<string, unknown>\n}\n\nexport interface ListMessagesOptions {\n limit?: number\n offset?: number\n}\n\nexport interface BulkDeleteThreadsInput {\n ids: string[]\n /** Called once per distinct workspace the ids touch, before ANY delete. */\n assertAccess: WorkspaceAccessCheck\n}\n\nexport interface ChatStore<TThread = ChatThreadRow, TMessage = ChatMessageRow> {\n listThreads(input: ListThreadsInput): Promise<ListThreadsResult<TThread>>\n getThread(threadId: string): Promise<TThread | null>\n createThread(input: CreateThreadInput): Promise<TThread>\n renameThread(threadId: string, title: string): Promise<TThread | null>\n pinThread(threadId: string, isPinned: boolean): Promise<TThread | null>\n /** Messages + thread in one batch. Resolves false when the thread does not\n * exist. `assertAccess` (optional) receives the thread's workspaceId before\n * the delete — single-thread callers usually check access themselves. */\n deleteThread(threadId: string, options?: { assertAccess?: WorkspaceAccessCheck }): Promise<boolean>\n bulkDeleteThreads(input: BulkDeleteThreadsInput): Promise<{ deleted: number }>\n /** Ordered oldest-first: `created_at`, then rowid (insertion order within a\n * same-second burst — a user+assistant pair lands in one epoch second). */\n listMessages(threadId: string, options?: ListMessagesOptions): Promise<TMessage[]>\n /** Inserts the message and bumps the thread's `updatedAt` in one batch so\n * workspace recency sorts stay truthful. */\n appendMessage(input: AppendMessageInput): Promise<TMessage>\n}\n\n/** One driver round trip when `db.batch` exists; sequential awaits in the\n * given order otherwise. Statement order is the caller's integrity contract\n * (children before parents). */\nasync function runStatements(\n db: ChatDatabase,\n statements: [unknown, ...unknown[]],\n): Promise<unknown[]> {\n if (typeof db.batch === 'function') {\n return await db.batch(statements)\n }\n const results: unknown[] = []\n for (const statement of statements) results.push(await statement)\n return results\n}\n\nfunction clampLimit(limit: number | undefined, fallback: number, max: number): number {\n const value = Number.isFinite(limit) ? Math.trunc(limit as number) : fallback\n return Math.min(Math.max(value, 1), max)\n}\n\nfunction clampOffset(offset: number | undefined): number {\n const value = Number.isFinite(offset) ? Math.trunc(offset as number) : 0\n return Math.max(value, 0)\n}\n\nexport function createChatStore<TTables extends ChatTables>(\n db: ChatDatabase,\n tables: TTables,\n): ChatStore<TTables['threads']['$inferSelect'], TTables['messages']['$inferSelect']> {\n type TThread = TTables['threads']['$inferSelect']\n type TMessage = TTables['messages']['$inferSelect']\n const threads = tables.threads as ChatTables['threads']\n const messages = tables.messages as ChatTables['messages']\n\n return {\n async listThreads(input) {\n const limit = clampLimit(input.limit, 50, 200)\n const offset = clampOffset(input.offset)\n const scope = eq(threads.workspaceId, input.workspaceId)\n const [list, [countRow]] = await Promise.all([\n db.select().from(threads).where(scope)\n // `id` tiebreak keeps pagination stable across same-second updates.\n .orderBy(desc(threads.updatedAt), asc(threads.id))\n .limit(limit)\n .offset(offset),\n db.select({ total: sql<number>`count(*)` }).from(threads).where(scope),\n ])\n return { threads: list as TThread[], total: countRow?.total ?? 0, limit, offset }\n },\n\n async getThread(threadId) {\n const [row] = await db.select().from(threads).where(eq(threads.id, threadId)).limit(1)\n return (row as TThread | undefined) ?? null\n },\n\n async createThread(input) {\n const title = threadTitleFromMessage(input.title ?? input.firstMessage ?? '')\n const values = {\n workspaceId: input.workspaceId,\n title,\n ...(input.category !== undefined ? { category: input.category } : {}),\n ...(input.isPinned !== undefined ? { isPinned: input.isPinned } : {}),\n ...(input.extras ?? {}),\n } as NewChatThreadRow\n const [row] = await db.insert(threads).values(values).returning()\n if (!row) throw new Error('thread insert returned no row')\n return row as TThread\n },\n\n async renameThread(threadId, title) {\n const trimmed = title.trim()\n if (!trimmed) throw new ChatStoreInputError('Missing title')\n const [row] = await db.update(threads)\n .set({ title: trimmed, updatedAt: new Date() })\n .where(eq(threads.id, threadId))\n .returning()\n return (row as TThread | undefined) ?? null\n },\n\n async pinThread(threadId, isPinned) {\n const [row] = await db.update(threads)\n .set({ isPinned, updatedAt: new Date() })\n .where(eq(threads.id, threadId))\n .returning()\n return (row as TThread | undefined) ?? null\n },\n\n async deleteThread(threadId, options) {\n const [existing] = await db.select({ id: threads.id, workspaceId: threads.workspaceId })\n .from(threads)\n .where(eq(threads.id, threadId))\n .limit(1)\n if (!existing) return false\n if (options?.assertAccess) await options.assertAccess(existing.workspaceId)\n // Messages first so a partial failure never leaves orphaned rows behind\n // a deleted thread.\n await runStatements(db, [\n db.delete(messages).where(eq(messages.threadId, threadId)),\n db.delete(threads).where(eq(threads.id, threadId)),\n ])\n return true\n },\n\n async bulkDeleteThreads(input) {\n const { ids, assertAccess } = input\n if (typeof assertAccess !== 'function') throw new ChatStoreInputError('Missing assertAccess')\n if (!Array.isArray(ids) || ids.length === 0 || !ids.every((id) => typeof id === 'string' && id.length > 0)) {\n throw new ChatStoreInputError('Missing ids')\n }\n if (ids.length > BULK_DELETE_MAX_THREADS) {\n throw new ChatStoreInputError(`Too many ids (max ${BULK_DELETE_MAX_THREADS})`)\n }\n\n const rows = await db.select({ id: threads.id, workspaceId: threads.workspaceId })\n .from(threads)\n .where(inArray(threads.id, ids))\n if (rows.length === 0) return { deleted: 0 }\n\n // Access is verified once per workspace the ids touch. Fail-closed: one\n // inaccessible workspace rejects the whole request before any delete.\n // Sorted so the check order (and therefore which denial surfaces) is\n // deterministic — row order follows random hex ids and varies per run.\n const workspaceIds = [...new Set(rows.map((row) => row.workspaceId))].sort()\n for (const workspaceId of workspaceIds) {\n await assertAccess(workspaceId)\n }\n\n const foundIds = rows.map((row) => row.id)\n await runStatements(db, [\n db.delete(messages).where(inArray(messages.threadId, foundIds)),\n db.delete(threads).where(inArray(threads.id, foundIds)),\n ])\n return { deleted: foundIds.length }\n },\n\n async listMessages(threadId, options) {\n const query = db.select().from(messages)\n .where(eq(messages.threadId, threadId))\n .orderBy(asc(messages.createdAt), sql`rowid`)\n .$dynamic()\n if (options?.limit !== undefined) query.limit(clampLimit(options.limit, 1, 1000))\n if (options?.offset !== undefined) query.offset(clampOffset(options.offset))\n return await query as TMessage[]\n },\n\n async appendMessage(input) {\n const values = {\n threadId: input.threadId,\n role: input.role,\n content: input.content,\n ...(input.parts !== undefined ? { parts: input.parts } : {}),\n ...(input.toolName !== undefined ? { toolName: input.toolName } : {}),\n ...(input.model !== undefined ? { model: input.model } : {}),\n ...(input.inputTokens !== undefined ? { inputTokens: input.inputTokens } : {}),\n ...(input.outputTokens !== undefined ? { outputTokens: input.outputTokens } : {}),\n ...(input.reasoningTokens !== undefined ? { reasoningTokens: input.reasoningTokens } : {}),\n ...(input.cacheReadTokens !== undefined ? { cacheReadTokens: input.cacheReadTokens } : {}),\n ...(input.cacheWriteTokens !== undefined ? { cacheWriteTokens: input.cacheWriteTokens } : {}),\n ...(input.costUsd !== undefined ? { costUsd: input.costUsd } : {}),\n ...(input.extras ?? {}),\n } as NewChatMessageRow\n const [insertResult] = await runStatements(db, [\n db.insert(messages).values(values).returning(),\n db.update(threads).set({ updatedAt: new Date() }).where(eq(threads.id, input.threadId)),\n ])\n const row = (insertResult as TMessage[] | undefined)?.[0]\n if (!row) throw new Error('message insert returned no row')\n return row\n },\n }\n}\n"],"mappings":"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","names":["sql","sql"]}
1
+ {"version":3,"sources":["../../src/chat-store/schema.ts","../../src/chat-store/store.ts"],"sourcesContent":["/**\n * Drizzle schema factory for the chat thread/message tables — the same\n * injection pattern as `createTeamTables`: the product owns the workspace\n * table; the factory wires the thread FK into it so the whole graph lives in\n * one drizzle schema with real cascade semantics. Column names, types,\n * defaults, enums, and indexes mirror legal's and gtm's hand-rolled `thread`/\n * `message` tables so a product with those tables adopts the factory without\n * rewriting rows; `tablePrefix` covers products that namespace (tax's\n * `chat_messages` style).\n *\n * The core is the superset the three products agree on. Divergences dropped,\n * and why:\n * - `thread.status` ('active'|'archived', legal+gtm) — archive semantics\n * diverge (tax uses `archivedAt`); product-domain lifecycle → extra column.\n * - `thread.scopeKind`/`scopeKey`/`harness` (gtm) — artifact anchoring and\n * harness pinning are product-domain → extra columns.\n * - tax's `tax_sessions` session columns (`taxYear`, `projectRef`,\n * `agentSessionId`, `agentRuntime`, `agentHarness`, `profile`, `error`,\n * `userId`) — sandbox-session state, not chat state → extra columns.\n * - `message.toolInput`/`toolOutput` (legal+gtm) — duplicate of the tool\n * part's `state.input`/`state.output` inside `parts` (the shape `/stream`'s\n * `normalizePersistedPart` owns); keeping both invites drift.\n * - `message.vaultFiles` (legal+gtm) — vault is product-domain → extra column.\n * - tax's re-declared `turn_events`/`turn_status` DDL — deliberately NOT here;\n * `/stream`'s turn-buffer owns that DDL (`TURN_BUFFER_D1_SCHEMA_SQL`).\n *\n * Kept beyond the intersection: tax's per-message `model`/`inputTokens`/\n * `outputTokens`, extended to the full usage receipt the harness actually\n * reports in `step-finish` parts (`tokens {input, output, reasoning,\n * cache{read, write}}` + `cost`) — see `./parts`.\n *\n * `threadExtraColumns`/`messageExtraColumns` merge product columns into the\n * table definitions (the `/missions` opaque-extras pattern: the store writes\n * `extras` values verbatim in the SAME insert statement and never reads,\n * validates, or defaults them).\n *\n * SERVER-side module (D1/libsql/better-sqlite3 behind a worker or server\n * route) — but free of `node:` builtins on purpose: D1 workers have none.\n */\n\nimport { sql } from 'drizzle-orm'\nimport { index, integer, real, sqliteTable, text } from 'drizzle-orm/sqlite-core'\nimport type { AnySQLiteColumn, AnySQLiteTable, SQLiteColumnBuilderBase } from 'drizzle-orm/sqlite-core'\nimport type { ChatMessagePart } from './parts'\n\n/** A product table referenced by FK — only the `id` column is touched. */\nexport type ChatParentTable = AnySQLiteTable & { id: AnySQLiteColumn }\n\nexport interface CreateChatTablesOptions<\n TThreadExtras extends Record<string, SQLiteColumnBuilderBase> = {},\n TMessageExtras extends Record<string, SQLiteColumnBuilderBase> = {},\n> {\n /** The product's workspace table — threads reference `workspaceTable.id`\n * with cascade. Omitted: `workspace_id` stays a plain indexed text column\n * (products whose tenant table lives in another database). */\n workspaceTable?: ChatParentTable\n /** Prefixes table AND index names (`'chat_'` → `chat_thread`,\n * `idx_chat_thread_workspace`) for products that namespace chat tables in a\n * shared database. Default: unprefixed `thread`/`message` (legal/gtm row\n * compatibility). */\n tablePrefix?: string\n /** Product columns merged into the thread table (the `/missions` extras\n * pattern) — e.g. a `status` lifecycle enum or gtm's scope columns. */\n threadExtraColumns?: TThreadExtras\n /** Product columns merged into the message table — e.g. legal's\n * `vault_files`. */\n messageExtraColumns?: TMessageExtras\n}\n\nconst hexId = () => text('id').primaryKey().default(sql`(lower(hex(randomblob(16))))`)\n\nconst createdAt = () => integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`)\n\nconst updatedAt = () => integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`)\n\nexport function createChatTables<\n TThreadExtras extends Record<string, SQLiteColumnBuilderBase> = {},\n TMessageExtras extends Record<string, SQLiteColumnBuilderBase> = {},\n>(options: CreateChatTablesOptions<TThreadExtras, TMessageExtras> = {}) {\n const { workspaceTable, tablePrefix = '' } = options\n const threadExtras = options.threadExtraColumns ?? ({} as TThreadExtras)\n const messageExtras = options.messageExtraColumns ?? ({} as TMessageExtras)\n\n const threads = sqliteTable(`${tablePrefix}thread`, {\n id: hexId(),\n workspaceId: workspaceTable\n ? text('workspace_id').notNull().references(() => workspaceTable.id, { onDelete: 'cascade' })\n : text('workspace_id').notNull(),\n title: text('title').notNull(),\n category: text('category'),\n isPinned: integer('is_pinned', { mode: 'boolean' }).notNull().default(false),\n createdAt: createdAt(),\n updatedAt: updatedAt(),\n ...threadExtras,\n }, (table) => [\n index(`idx_${tablePrefix}thread_workspace`).on(table.workspaceId),\n // Supports the store's list ordering (updatedAt desc within a workspace).\n index(`idx_${tablePrefix}thread_workspace_updated`).on(table.workspaceId, table.updatedAt),\n ])\n\n const messages = sqliteTable(`${tablePrefix}message`, {\n id: hexId(),\n threadId: text('thread_id').notNull().references(() => threads.id, { onDelete: 'cascade' }),\n role: text('role', { enum: ['user', 'assistant', 'system', 'tool'] }).notNull(),\n content: text('content').notNull(),\n parts: text('parts', { mode: 'json' }).$type<ChatMessagePart[]>().default([]),\n toolName: text('tool_name'),\n model: text('model'),\n // Usage receipt, flattened from the harness's `step-finish` shape\n // (`tokens {input, output, reasoning, cache{read, write}}` + `cost`).\n inputTokens: integer('input_tokens'),\n outputTokens: integer('output_tokens'),\n reasoningTokens: integer('reasoning_tokens'),\n cacheReadTokens: integer('cache_read_tokens'),\n cacheWriteTokens: integer('cache_write_tokens'),\n costUsd: real('cost_usd'),\n createdAt: createdAt(),\n ...messageExtras,\n }, (table) => [\n index(`idx_${tablePrefix}message_thread`).on(table.threadId),\n index(`idx_${tablePrefix}message_thread_created`).on(table.threadId, table.createdAt),\n ])\n\n return { threads, messages }\n}\n\n/**\n * The base (no-extras) table pair, pinned via an instantiation expression:\n * `ReturnType<typeof createChatTables>` on the bare generic substitutes the\n * extras params with their CONSTRAINT (`Record<string,\n * SQLiteColumnBuilderBase>`), stamping an index signature into the column map\n * that widens every concrete column to `unknown`/`notNull: false` — concrete\n * factory results then fail `extends ChatTables`. (`teams`' `createTeamTables`\n * is non-generic, so its plain `ReturnType` never hits this.)\n */\nexport type ChatTables = ReturnType<typeof createChatTables<{}, {}>>\n\nexport type ChatThreadRow = ChatTables['threads']['$inferSelect']\nexport type ChatMessageRow = ChatTables['messages']['$inferSelect']\nexport type NewChatThreadRow = ChatTables['threads']['$inferInsert']\nexport type NewChatMessageRow = ChatTables['messages']['$inferInsert']\n","/**\n * Typed CRUD over the tables from `createChatTables`. Works against any\n * SQLite drizzle driver (D1, libsql, better-sqlite3) — builders are awaited,\n * never `.run()`/`.all()`, so sync and async drivers behave identically.\n *\n * Access control is an injected seam, never an import: single-thread routes\n * check workspace access themselves (they know the thread), while\n * `bulkDeleteThreads` REQUIRES an `assertAccess` callback because one request\n * can span workspaces — it is called once per distinct workspace and any\n * throw rejects the whole request before a single delete runs (fail-closed;\n * legal's bulk-delete semantics).\n *\n * Deletes run messages-first in ONE `db.batch` round trip when the driver has\n * one (D1, libsql), so a partial failure never leaves orphaned rows behind a\n * deleted thread; drivers without `batch` (better-sqlite3) fall back to\n * sequential awaits in the same order.\n */\n\nimport { asc, desc, eq, inArray, sql } from 'drizzle-orm'\nimport type { BaseSQLiteDatabase } from 'drizzle-orm/sqlite-core'\nimport { BULK_DELETE_MAX_THREADS, ChatStoreInputError, threadTitleFromMessage } from './core'\nimport type { ChatMessagePart } from './parts'\nimport type { ChatMessageRow, ChatTables, ChatThreadRow, NewChatMessageRow, NewChatThreadRow } from './schema'\n\n/** Any SQLite drizzle database — `any` erases the driver-specific run-result\n * and schema generics so better-sqlite3, D1, and libsql handles all fit.\n * `batch` is structural: present on D1/libsql drizzle instances. */\nexport type ChatDatabase = BaseSQLiteDatabase<'sync' | 'async', any, any> & {\n batch?: (statements: [unknown, ...unknown[]]) => Promise<unknown[]>\n}\n\n/** Product-injected access check. Throw to deny; the store never interprets\n * users or roles itself. */\nexport type WorkspaceAccessCheck = (workspaceId: string) => void | Promise<void>\n\nexport interface ListThreadsInput {\n workspaceId: string\n /** Clamped to 1..200; default 50 (legal's list route semantics). */\n limit?: number\n /** Clamped to >= 0; default 0. */\n offset?: number\n}\n\nexport interface ListThreadsResult<TThread = ChatThreadRow> {\n threads: TThread[]\n total: number\n limit: number\n offset: number\n}\n\nexport interface CreateThreadInput {\n workspaceId: string\n /** Title source when `title` is absent: first non-empty line, 80-char cap\n * (`threadTitleFromMessage`). */\n firstMessage?: string\n /** Explicit title; still normalized through `threadTitleFromMessage` so a\n * multi-page paste never becomes a sidebar entry. */\n title?: string\n category?: string | null\n isPinned?: boolean\n /** Opaque product-column values written verbatim in the SAME insert (the\n * `/missions` extras pattern). Never read, validated, or defaulted here. */\n extras?: Record<string, unknown>\n}\n\nexport interface AppendMessageInput {\n threadId: string\n role: 'user' | 'assistant' | 'system' | 'tool'\n content: string\n parts?: ChatMessagePart[]\n toolName?: string | null\n model?: string | null\n inputTokens?: number | null\n outputTokens?: number | null\n reasoningTokens?: number | null\n cacheReadTokens?: number | null\n cacheWriteTokens?: number | null\n costUsd?: number | null\n /** Opaque product-column values written verbatim in the SAME insert. */\n extras?: Record<string, unknown>\n}\n\nexport interface ListMessagesOptions {\n limit?: number\n offset?: number\n}\n\nexport interface BulkDeleteThreadsInput {\n ids: string[]\n /** Called once per distinct workspace the ids touch, before ANY delete. */\n assertAccess: WorkspaceAccessCheck\n}\n\nexport interface ChatStore<TThread = ChatThreadRow, TMessage = ChatMessageRow> {\n listThreads(input: ListThreadsInput): Promise<ListThreadsResult<TThread>>\n getThread(threadId: string): Promise<TThread | null>\n createThread(input: CreateThreadInput): Promise<TThread>\n renameThread(threadId: string, title: string): Promise<TThread | null>\n pinThread(threadId: string, isPinned: boolean): Promise<TThread | null>\n /** Messages + thread in one batch. Resolves false when the thread does not\n * exist. `assertAccess` (optional) receives the thread's workspaceId before\n * the delete — single-thread callers usually check access themselves. */\n deleteThread(threadId: string, options?: { assertAccess?: WorkspaceAccessCheck }): Promise<boolean>\n bulkDeleteThreads(input: BulkDeleteThreadsInput): Promise<{ deleted: number }>\n /** Ordered oldest-first: `created_at`, then rowid (insertion order within a\n * same-second burst — a user+assistant pair lands in one epoch second). */\n listMessages(threadId: string, options?: ListMessagesOptions): Promise<TMessage[]>\n /** Inserts the message and bumps the thread's `updatedAt` in one batch so\n * workspace recency sorts stay truthful. */\n appendMessage(input: AppendMessageInput): Promise<TMessage>\n}\n\n/** One driver round trip when `db.batch` exists; sequential awaits in the\n * given order otherwise. Statement order is the caller's integrity contract\n * (children before parents). */\nasync function runStatements(\n db: ChatDatabase,\n statements: [unknown, ...unknown[]],\n): Promise<unknown[]> {\n if (typeof db.batch === 'function') {\n return await db.batch(statements)\n }\n const results: unknown[] = []\n for (const statement of statements) results.push(await statement)\n return results\n}\n\nfunction clampLimit(limit: number | undefined, fallback: number, max: number): number {\n const value = Number.isFinite(limit) ? Math.trunc(limit as number) : fallback\n return Math.min(Math.max(value, 1), max)\n}\n\nfunction clampOffset(offset: number | undefined): number {\n const value = Number.isFinite(offset) ? Math.trunc(offset as number) : 0\n return Math.max(value, 0)\n}\n\nexport function createChatStore<TTables extends ChatTables>(\n db: ChatDatabase,\n tables: TTables,\n): ChatStore<TTables['threads']['$inferSelect'], TTables['messages']['$inferSelect']> {\n type TThread = TTables['threads']['$inferSelect']\n type TMessage = TTables['messages']['$inferSelect']\n const threads = tables.threads as ChatTables['threads']\n const messages = tables.messages as ChatTables['messages']\n\n return {\n async listThreads(input) {\n const limit = clampLimit(input.limit, 50, 200)\n const offset = clampOffset(input.offset)\n const scope = eq(threads.workspaceId, input.workspaceId)\n const [list, [countRow]] = await Promise.all([\n db.select().from(threads).where(scope)\n // `id` tiebreak keeps pagination stable across same-second updates.\n .orderBy(desc(threads.updatedAt), asc(threads.id))\n .limit(limit)\n .offset(offset),\n db.select({ total: sql<number>`count(*)` }).from(threads).where(scope),\n ])\n return { threads: list as TThread[], total: countRow?.total ?? 0, limit, offset }\n },\n\n async getThread(threadId) {\n const [row] = await db.select().from(threads).where(eq(threads.id, threadId)).limit(1)\n return (row as TThread | undefined) ?? null\n },\n\n async createThread(input) {\n const title = threadTitleFromMessage(input.title ?? input.firstMessage ?? '')\n const values = {\n workspaceId: input.workspaceId,\n title,\n ...(input.category !== undefined ? { category: input.category } : {}),\n ...(input.isPinned !== undefined ? { isPinned: input.isPinned } : {}),\n ...(input.extras ?? {}),\n } as NewChatThreadRow\n const [row] = await db.insert(threads).values(values).returning()\n if (!row) throw new Error('thread insert returned no row')\n return row as TThread\n },\n\n async renameThread(threadId, title) {\n const trimmed = title.trim()\n if (!trimmed) throw new ChatStoreInputError('Missing title')\n const [row] = await db.update(threads)\n .set({ title: trimmed, updatedAt: new Date() })\n .where(eq(threads.id, threadId))\n .returning()\n return (row as TThread | undefined) ?? null\n },\n\n async pinThread(threadId, isPinned) {\n const [row] = await db.update(threads)\n .set({ isPinned, updatedAt: new Date() })\n .where(eq(threads.id, threadId))\n .returning()\n return (row as TThread | undefined) ?? null\n },\n\n async deleteThread(threadId, options) {\n const [existing] = await db.select({ id: threads.id, workspaceId: threads.workspaceId })\n .from(threads)\n .where(eq(threads.id, threadId))\n .limit(1)\n if (!existing) return false\n if (options?.assertAccess) await options.assertAccess(existing.workspaceId)\n // Messages first so a partial failure never leaves orphaned rows behind\n // a deleted thread.\n await runStatements(db, [\n db.delete(messages).where(eq(messages.threadId, threadId)),\n db.delete(threads).where(eq(threads.id, threadId)),\n ])\n return true\n },\n\n async bulkDeleteThreads(input) {\n const { ids, assertAccess } = input\n if (typeof assertAccess !== 'function') throw new ChatStoreInputError('Missing assertAccess')\n if (!Array.isArray(ids) || ids.length === 0 || !ids.every((id) => typeof id === 'string' && id.length > 0)) {\n throw new ChatStoreInputError('Missing ids')\n }\n if (ids.length > BULK_DELETE_MAX_THREADS) {\n throw new ChatStoreInputError(`Too many ids (max ${BULK_DELETE_MAX_THREADS})`)\n }\n\n const rows = await db.select({ id: threads.id, workspaceId: threads.workspaceId })\n .from(threads)\n .where(inArray(threads.id, ids))\n if (rows.length === 0) return { deleted: 0 }\n\n // Access is verified once per workspace the ids touch. Fail-closed: one\n // inaccessible workspace rejects the whole request before any delete.\n // Sorted so the check order (and therefore which denial surfaces) is\n // deterministic — row order follows random hex ids and varies per run.\n const workspaceIds = [...new Set(rows.map((row) => row.workspaceId))].sort()\n for (const workspaceId of workspaceIds) {\n await assertAccess(workspaceId)\n }\n\n const foundIds = rows.map((row) => row.id)\n await runStatements(db, [\n db.delete(messages).where(inArray(messages.threadId, foundIds)),\n db.delete(threads).where(inArray(threads.id, foundIds)),\n ])\n return { deleted: foundIds.length }\n },\n\n async listMessages(threadId, options) {\n const query = db.select().from(messages)\n .where(eq(messages.threadId, threadId))\n .orderBy(asc(messages.createdAt), sql`rowid`)\n .$dynamic()\n if (options?.limit !== undefined) query.limit(clampLimit(options.limit, 1, 1000))\n if (options?.offset !== undefined) query.offset(clampOffset(options.offset))\n return await query as TMessage[]\n },\n\n async appendMessage(input) {\n const values = {\n threadId: input.threadId,\n role: input.role,\n content: input.content,\n ...(input.parts !== undefined ? { parts: input.parts } : {}),\n ...(input.toolName !== undefined ? { toolName: input.toolName } : {}),\n ...(input.model !== undefined ? { model: input.model } : {}),\n ...(input.inputTokens !== undefined ? { inputTokens: input.inputTokens } : {}),\n ...(input.outputTokens !== undefined ? { outputTokens: input.outputTokens } : {}),\n ...(input.reasoningTokens !== undefined ? { reasoningTokens: input.reasoningTokens } : {}),\n ...(input.cacheReadTokens !== undefined ? { cacheReadTokens: input.cacheReadTokens } : {}),\n ...(input.cacheWriteTokens !== undefined ? { cacheWriteTokens: input.cacheWriteTokens } : {}),\n ...(input.costUsd !== undefined ? { costUsd: input.costUsd } : {}),\n ...(input.extras ?? {}),\n } as NewChatMessageRow\n const [insertResult] = await runStatements(db, [\n db.insert(messages).values(values).returning(),\n db.update(threads).set({ updatedAt: new Date() }).where(eq(threads.id, input.threadId)),\n ])\n const row = (insertResult as TMessage[] | undefined)?.[0]\n if (!row) throw new Error('message insert returned no row')\n return row\n },\n }\n}\n"],"mappings":"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","names":["sql","sql"]}
@@ -0,0 +1,191 @@
1
+ // src/chat-routes/wire.ts
2
+ function chatTurnRequestInit(payload) {
3
+ return {
4
+ method: "POST",
5
+ headers: { "Content-Type": "application/json" },
6
+ body: JSON.stringify(payload)
7
+ };
8
+ }
9
+ var INLINE_PARTS_MAX_BYTES = 95e4;
10
+ var ChatTurnInputError = class extends Error {
11
+ constructor(message, status = 400, code = "INVALID_CHAT_TURN") {
12
+ super(message);
13
+ this.status = status;
14
+ this.code = code;
15
+ this.name = "ChatTurnInputError";
16
+ }
17
+ status;
18
+ code;
19
+ };
20
+ function partByteSize(part) {
21
+ let bytes = 0;
22
+ if (part.type === "text") return part.text.length;
23
+ if (part.url) bytes += part.url.length;
24
+ if (part.content) bytes += part.content.length;
25
+ if (part.path) bytes += part.path.length;
26
+ return bytes;
27
+ }
28
+ function promptPartsByteSize(parts) {
29
+ return parts.reduce((total, part) => total + partByteSize(part), 0);
30
+ }
31
+ function assertPromptPartsWithinCap(parts, maxBytes = INLINE_PARTS_MAX_BYTES) {
32
+ const total = promptPartsByteSize(parts);
33
+ if (total <= maxBytes) return;
34
+ const largest = [...parts].sort((a, b) => partByteSize(b) - partByteSize(a))[0];
35
+ const largestName = largest && largest.type !== "text" ? largest.filename ?? largest.path ?? largest.type : "text";
36
+ throw new ChatTurnInputError(
37
+ `Inline prompt parts total ${total}B, over the ${maxBytes}B budget (largest: ${largestName}, ${largest ? partByteSize(largest) : 0}B). Upload large files through the upload route so they travel as sandbox path references.`,
38
+ 413,
39
+ "PROMPT_PARTS_TOO_LARGE"
40
+ );
41
+ }
42
+ var MENTION_IMAGE_MEDIA_TYPES = /* @__PURE__ */ new Map([
43
+ [".png", "image/png"],
44
+ [".jpg", "image/jpeg"],
45
+ [".jpeg", "image/jpeg"],
46
+ [".gif", "image/gif"],
47
+ [".webp", "image/webp"],
48
+ [".svg", "image/svg+xml"],
49
+ [".bmp", "image/bmp"],
50
+ [".heic", "image/heic"],
51
+ [".heif", "image/heif"],
52
+ [".avif", "image/avif"]
53
+ ]);
54
+ function extensionOf(path) {
55
+ const base = path.split("/").filter(Boolean).pop() ?? path;
56
+ const dot = base.lastIndexOf(".");
57
+ return dot > 0 ? base.slice(dot).toLowerCase() : "";
58
+ }
59
+ function mediaTypeForMentionPath(path) {
60
+ return MENTION_IMAGE_MEDIA_TYPES.get(extensionOf(path));
61
+ }
62
+ function fileMentionsToParts(mentions, opts = {}) {
63
+ const resolvePath = opts.resolvePath ?? ((path) => path);
64
+ return mentions.map((mention) => {
65
+ const mediaType = mediaTypeForMentionPath(mention.path);
66
+ const part = {
67
+ type: mediaType ? "image" : "file",
68
+ filename: mention.name,
69
+ path: resolvePath(mention.path)
70
+ };
71
+ if (mediaType) part.mediaType = mediaType;
72
+ return part;
73
+ });
74
+ }
75
+ function buildMentionPromptBlock(mentions) {
76
+ if (mentions.length === 0) return "";
77
+ const lines = mentions.map((m) => `- ${m.name} (${m.path})`);
78
+ return `
79
+
80
+ Mentioned files \u2014 read them from these paths:
81
+ ${lines.join("\n")}`;
82
+ }
83
+ function parseChatTurnParts(raw) {
84
+ if (raw === void 0 || raw === null) return [];
85
+ if (!Array.isArray(raw)) throw new ChatTurnInputError("parts must be an array");
86
+ return raw.map((entry, index) => {
87
+ const part = entry;
88
+ if (!part || typeof part !== "object") {
89
+ throw new ChatTurnInputError(`parts[${index}] must be an object`);
90
+ }
91
+ if (part.type !== "image" && part.type !== "file") {
92
+ throw new ChatTurnInputError(`parts[${index}].type must be 'image' or 'file'`);
93
+ }
94
+ for (const key of ["filename", "mediaType", "url", "path", "content"]) {
95
+ if (part[key] !== void 0 && typeof part[key] !== "string") {
96
+ throw new ChatTurnInputError(`parts[${index}].${key} must be a string`);
97
+ }
98
+ }
99
+ if (!part.url && !part.path && !part.content) {
100
+ throw new ChatTurnInputError(`parts[${index}] needs a url, path, or content`);
101
+ }
102
+ return {
103
+ type: part.type,
104
+ ...part.filename !== void 0 ? { filename: part.filename } : {},
105
+ ...part.mediaType !== void 0 ? { mediaType: part.mediaType } : {},
106
+ ...part.url !== void 0 ? { url: part.url } : {},
107
+ ...part.path !== void 0 ? { path: part.path } : {},
108
+ ...part.content !== void 0 ? { content: part.content } : {}
109
+ };
110
+ });
111
+ }
112
+
113
+ // src/chat-routes/file-index.ts
114
+ var DEFAULT_IGNORE_SEGMENTS = [
115
+ "node_modules",
116
+ "dist",
117
+ "build",
118
+ "out",
119
+ "coverage",
120
+ "target",
121
+ "__pycache__",
122
+ "venv"
123
+ ];
124
+ function isIgnored(relPath, ignoreSegments) {
125
+ for (const segment of relPath.split("/")) {
126
+ if (!segment) continue;
127
+ if (segment.startsWith(".")) return true;
128
+ if (ignoreSegments.has(segment)) return true;
129
+ }
130
+ return false;
131
+ }
132
+ function relativeTo(root, path) {
133
+ const prefix = root.endsWith("/") ? root : `${root}/`;
134
+ if (path.startsWith(prefix)) return path.slice(prefix.length);
135
+ if (path === root) return "";
136
+ return path;
137
+ }
138
+ function basename(path) {
139
+ const segments = path.split("/").filter(Boolean);
140
+ return segments[segments.length - 1] ?? path;
141
+ }
142
+ function createSandboxFileIndexRoute(options) {
143
+ const maxDepth = options.maxDepth ?? 12;
144
+ const maxEntries = options.maxEntries ?? 5e3;
145
+ const cacheTtlSeconds = options.cacheTtlSeconds ?? 20;
146
+ const staticIgnore = /* @__PURE__ */ new Set([...DEFAULT_IGNORE_SEGMENTS, ...options.ignore ?? []]);
147
+ return async function fileIndex(request) {
148
+ const auth = await options.authorize({ request });
149
+ if (auth.status === "denied") return auth.response;
150
+ if (auth.status === "warming") {
151
+ return Response.json({ status: "warming" });
152
+ }
153
+ const cache = options.cache;
154
+ if (cache && auth.cacheKey) {
155
+ const cached = await cache.get(auth.cacheKey);
156
+ if (cached) return Response.json(cached);
157
+ }
158
+ const ignoreSegments = auth.ignore?.length ? /* @__PURE__ */ new Set([...staticIgnore, ...auth.ignore]) : staticIgnore;
159
+ const scan = await auth.fs.tree(auth.root, { maxDepth });
160
+ const filtered = scan.files.filter((f) => !isIgnored(relativeTo(scan.root, f.path), ignoreSegments));
161
+ const truncated = scan.stats.truncated || filtered.length > maxEntries;
162
+ const files = filtered.slice(0, maxEntries).map((f) => {
163
+ const path = relativeTo(scan.root, f.path);
164
+ const entry = { path, name: basename(path) };
165
+ if (typeof f.size === "number") entry.size = f.size;
166
+ return entry;
167
+ });
168
+ const body = {
169
+ status: "ready",
170
+ files,
171
+ truncated,
172
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
173
+ };
174
+ if (cache && auth.cacheKey) await cache.put(auth.cacheKey, body, { ttlSeconds: cacheTtlSeconds });
175
+ return Response.json(body);
176
+ };
177
+ }
178
+
179
+ export {
180
+ chatTurnRequestInit,
181
+ INLINE_PARTS_MAX_BYTES,
182
+ ChatTurnInputError,
183
+ promptPartsByteSize,
184
+ assertPromptPartsWithinCap,
185
+ mediaTypeForMentionPath,
186
+ fileMentionsToParts,
187
+ buildMentionPromptBlock,
188
+ parseChatTurnParts,
189
+ createSandboxFileIndexRoute
190
+ };
191
+ //# sourceMappingURL=chunk-2EO7CPL3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/chat-routes/wire.ts","../src/chat-routes/file-index.ts"],"sourcesContent":["/**\n * Wire contract between the chat client (composer + `streamChatTurn`) and the\n * assembled server vertical (`createChatTurnRoutes`). Import-free on purpose:\n * `/web-react` re-exports these types into browser bundles, so nothing here may\n * reach a Node builtin or an engine package.\n *\n * The part shape mirrors the sandbox SDK's `PromptInputPart` structurally\n * (text | image | file with filename/mediaType/url/path/content) — derived\n * here, not imported, so the client bundle never touches the SDK.\n */\n\nexport interface ChatTurnTextPartInput {\n type: 'text'\n text: string\n}\n\n/** A non-text prompt part the upload route hands back and the client echoes\n * on send. `url` carries an inline `data:` URI for small files; `path` is a\n * sandbox workspace reference for large ones (the >1 MiB gateway body cap\n * makes the two-step upload mandatory). */\nexport interface ChatTurnFilePartInput {\n type: 'image' | 'file'\n filename?: string\n mediaType?: string\n url?: string\n path?: string\n content?: string\n}\n\nexport type ChatTurnPartInput = ChatTurnTextPartInput | ChatTurnFilePartInput\n\n/** POST body for the turn route. `content` may be empty when `parts` carry the\n * message (an image-only send). Product routing fields (workspaceId etc.) ride\n * alongside and are read by the product's `authorize` seam. */\nexport interface ChatTurnRequestPayload {\n threadId: string\n content?: string\n /** Non-text parts from the upload route, echoed back verbatim. */\n parts?: ChatTurnFilePartInput[]\n model?: string\n effort?: 'auto' | 'low' | 'medium' | 'high'\n harness?: string\n /** Client-generated idempotency key for the logical turn (retry-safe). */\n turnId?: string\n [key: string]: unknown\n}\n\n/** `fetch` init for the turn route — the one place the client wire shape is\n * serialized, so composer glue and products never drift from the server's\n * parser. */\nexport function chatTurnRequestInit(payload: ChatTurnRequestPayload): RequestInit {\n return {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n }\n}\n\n// ── inline-part byte budget ─────────────────────────────────────────────────\n//\n// The sandbox gateway caps request bodies at 1 MiB; a turn body whose inline\n// `data:` parts exceed it dies at the gateway with an opaque 413. Enforce the\n// budget at the route boundary instead, with headroom for the JSON envelope\n// (same fail-loud-at-the-choke-point style as /sandbox's provision-payload and\n// env-size gates).\n\nexport const INLINE_PARTS_MAX_BYTES = 950_000\n\nexport class ChatTurnInputError extends Error {\n constructor(message: string, readonly status = 400, readonly code = 'INVALID_CHAT_TURN') {\n super(message)\n this.name = 'ChatTurnInputError'\n }\n}\n\nfunction partByteSize(part: ChatTurnPartInput): number {\n let bytes = 0\n if (part.type === 'text') return part.text.length\n if (part.url) bytes += part.url.length\n if (part.content) bytes += part.content.length\n if (part.path) bytes += part.path.length\n return bytes\n}\n\nexport function promptPartsByteSize(parts: ChatTurnPartInput[]): number {\n return parts.reduce((total, part) => total + partByteSize(part), 0)\n}\n\n/** Throws `ChatTurnInputError` (413) when the parts' inline payload would blow\n * the gateway cap. Path-ref parts are tiny by construction and always pass. */\nexport function assertPromptPartsWithinCap(\n parts: ChatTurnPartInput[],\n maxBytes = INLINE_PARTS_MAX_BYTES,\n): void {\n const total = promptPartsByteSize(parts)\n if (total <= maxBytes) return\n const largest = [...parts].sort((a, b) => partByteSize(b) - partByteSize(a))[0]\n const largestName = largest && largest.type !== 'text' ? largest.filename ?? largest.path ?? largest.type : 'text'\n throw new ChatTurnInputError(\n `Inline prompt parts total ${total}B, over the ${maxBytes}B budget (largest: ${largestName}, ${largest ? partByteSize(largest) : 0}B). ` +\n 'Upload large files through the upload route so they travel as sandbox path references.',\n 413,\n 'PROMPT_PARTS_TOO_LARGE',\n )\n}\n\n// ── file mentions ────────────────────────────────────────────────────────\n//\n// A file mention (`@`-picked in the composer, sandbox-ui#184) is a path\n// reference into the workspace sandbox — no byte upload. These helpers turn\n// a resolved mention list into wire parts and the prompt pointer block that\n// tells the agent where to read them from.\n\n/** A file mention resolved from the composer's `@`-picker: the\n * workspace-relative path plus enough metadata to build a prompt part and\n * pointer text. `path` is the canonical identity — the mention pill's\n * `MentionItem.id` for the file kind (`/web-react`'s `useFileMentions`). */\nexport interface FileMention {\n path: string\n name: string\n size?: number\n}\n\nconst MENTION_IMAGE_MEDIA_TYPES: ReadonlyMap<string, string> = new Map([\n ['.png', 'image/png'],\n ['.jpg', 'image/jpeg'],\n ['.jpeg', 'image/jpeg'],\n ['.gif', 'image/gif'],\n ['.webp', 'image/webp'],\n ['.svg', 'image/svg+xml'],\n ['.bmp', 'image/bmp'],\n ['.heic', 'image/heic'],\n ['.heif', 'image/heif'],\n ['.avif', 'image/avif'],\n])\n\nfunction extensionOf(path: string): string {\n const base = path.split('/').filter(Boolean).pop() ?? path\n const dot = base.lastIndexOf('.')\n return dot > 0 ? base.slice(dot).toLowerCase() : ''\n}\n\n/** The `image/*` mime for a mention path by extension, or `undefined` for\n * anything not in the known image set (dispatched as `type: 'file'`). */\nexport function mediaTypeForMentionPath(path: string): string | undefined {\n return MENTION_IMAGE_MEDIA_TYPES.get(extensionOf(path))\n}\n\nexport interface FileMentionsToPartsOptions {\n /** Resolve a mention's workspace-relative path to the absolute path the\n * dispatched part should carry (e.g. a host prefixing the in-box vault\n * root). Default: identity — the path travels unchanged. */\n resolvePath?: (path: string) => string\n}\n\n/** Maps resolved file mentions to path-only `ChatTurnFilePartInput`s —\n * `image` vs `file` by extension, and always a `path`, never a `url` (the\n * url/path XOR invariant: a mention is a sandbox path reference, never\n * inline bytes). */\nexport function fileMentionsToParts(\n mentions: readonly FileMention[],\n opts: FileMentionsToPartsOptions = {},\n): ChatTurnFilePartInput[] {\n const resolvePath = opts.resolvePath ?? ((path: string) => path)\n return mentions.map((mention) => {\n const mediaType = mediaTypeForMentionPath(mention.path)\n const part: ChatTurnFilePartInput = {\n type: mediaType ? 'image' : 'file',\n filename: mention.name,\n path: resolvePath(mention.path),\n }\n if (mediaType) part.mediaType = mediaType\n return part\n })\n}\n\n/** The agent-facing pointer block appended to the dispatched prompt — never\n * persisted in message `content`. Empty array → `''` so callers can append\n * unconditionally. This is the sole producer of that text: the current\n * turn's dispatch and any history projection built from the same mention\n * list both route through here, so the two can't drift apart. */\nexport function buildMentionPromptBlock(\n mentions: readonly Pick<FileMention, 'name' | 'path'>[],\n): string {\n if (mentions.length === 0) return ''\n const lines = mentions.map((m) => `- ${m.name} (${m.path})`)\n return `\\n\\nMentioned files — read them from these paths:\\n${lines.join('\\n')}`\n}\n\n/** Validates the untyped `parts` array off the wire. Returns the typed parts\n * or throws `ChatTurnInputError` (400) naming the offending entry. */\nexport function parseChatTurnParts(raw: unknown): ChatTurnFilePartInput[] {\n if (raw === undefined || raw === null) return []\n if (!Array.isArray(raw)) throw new ChatTurnInputError('parts must be an array')\n return raw.map((entry, index) => {\n const part = entry as Record<string, unknown> | null\n if (!part || typeof part !== 'object') {\n throw new ChatTurnInputError(`parts[${index}] must be an object`)\n }\n if (part.type !== 'image' && part.type !== 'file') {\n throw new ChatTurnInputError(`parts[${index}].type must be 'image' or 'file'`)\n }\n for (const key of ['filename', 'mediaType', 'url', 'path', 'content'] as const) {\n if (part[key] !== undefined && typeof part[key] !== 'string') {\n throw new ChatTurnInputError(`parts[${index}].${key} must be a string`)\n }\n }\n if (!part.url && !part.path && !part.content) {\n throw new ChatTurnInputError(`parts[${index}] needs a url, path, or content`)\n }\n return {\n type: part.type,\n ...(part.filename !== undefined ? { filename: part.filename as string } : {}),\n ...(part.mediaType !== undefined ? { mediaType: part.mediaType as string } : {}),\n ...(part.url !== undefined ? { url: part.url as string } : {}),\n ...(part.path !== undefined ? { path: part.path as string } : {}),\n ...(part.content !== undefined ? { content: part.content as string } : {}),\n }\n })\n}\n","/**\n * `createSandboxFileIndexRoute` — server side of `@`-file-mentions\n * (companion to sandbox-ui#184's composer mention primitive). Serves a flat,\n * ignore-filtered listing of the workspace sandbox so `useFileMentions`\n * (`/web-react`) can filter it client-side without a round trip per\n * keystroke.\n *\n * Same seam style as `createUploadRoute`: `authorize({ request })` resolves a\n * structural `{ tree(path, opts) }` handle (the shape of the sandbox SDK's\n * `box.fs.tree`) — no SDK import here. `authorize` also carries the\n * cold-box signal: a sandbox that isn't running yet answers `{ status:\n * 'warming' }` directly, never provisions-and-waits inside this route.\n */\n\nimport type { FileMention } from './wire'\n\n/** One entry from a structural `tree()` scan. Mirrors the sandbox SDK's\n * `FileTreeFile` (`path`, `size`, `mtime`) — `mtime` is unused here so it's\n * omitted from the structural match. */\nexport interface SandboxTreeFile {\n path: string\n size: number\n}\n\n/** Structural match of the sandbox SDK's `box.fs.tree` result shape\n * (`FileTreeResult`). `stats.truncated` is the only stat this route reads;\n * the rest ride through unread on the real SDK type. */\nexport interface SandboxTreeResult {\n root: string\n files: SandboxTreeFile[]\n stats: { truncated: boolean }\n}\n\n/** Structural match of the sandbox SDK's `box.fs` tree surface. */\nexport interface SandboxFileTreeSource {\n tree(path: string, options?: { maxDepth?: number }): Promise<SandboxTreeResult>\n}\n\nexport interface FileIndexReadyResponse {\n status: 'ready'\n /** Workspace-relative entries. Same shape as `FileMention` (`./wire`) so a\n * client can hand a response entry straight to `fileMentionsToParts` /\n * `buildMentionPromptBlock` without remapping. */\n files: FileMention[]\n /** True when either the underlying scan truncated (SDK-side cap) or this\n * route's own `maxEntries` cap trimmed the filtered list. The client\n * should show \"showing first N files\" rather than imply completeness. */\n truncated: boolean\n generatedAt: string\n}\n\n/** Cold-box answer: no provisioning happened, no files were scanned. The\n * client shows a warming state and retries — this route never blocks on a\n * box coming up. */\nexport interface FileIndexWarmingResponse {\n status: 'warming'\n}\n\nexport type FileIndexResponse = FileIndexReadyResponse | FileIndexWarmingResponse\n\n/** Short-TTL cache seam so repeat popover opens in the same session don't\n * re-scan the workspace. Host-provided (e.g. a KV binding); `key` is\n * whatever `authorize` returns as `cacheKey` — this route treats it opaquely. */\nexport interface FileIndexCache {\n get(key: string): Promise<FileIndexReadyResponse | null> | FileIndexReadyResponse | null\n put(key: string, value: FileIndexReadyResponse, options?: { ttlSeconds?: number }): Promise<void> | void\n}\n\nexport type FileIndexAuthorization =\n | {\n status: 'ready'\n /** Structural sandbox `fs` handle, usually `ensureWorkspaceSandbox(...)` → `box.fs`. */\n fs: SandboxFileTreeSource\n /** Workspace root to index (e.g. `/home/agent`). */\n root: string\n /** Extra ignore segments for this request, merged with the route's\n * defaults + `CreateSandboxFileIndexRouteOptions.ignore`. */\n ignore?: string[]\n /** Opaque cache key for the optional cache seam. Omit to skip caching\n * for this request (e.g. a workspace the host chooses not to cache). */\n cacheKey?: string\n }\n | { status: 'warming' }\n | { status: 'denied'; response: Response }\n\nexport interface CreateSandboxFileIndexRouteOptions {\n /** Authenticate the caller, resolve the sandbox `fs` handle, and signal a\n * cold box — never provisions or waits. */\n authorize(args: { request: Request }): Promise<FileIndexAuthorization>\n /** Extra ignore segments beyond the route's defaults (node_modules, .git,\n * dotfiles/dot-dirs, common build dirs). Matched as exact path-segment\n * names, same rule as the defaults. */\n ignore?: string[]\n /** Passed to `fs.tree` as `options.maxDepth`. Default 12. */\n maxDepth?: number\n /** Hard cap on entries returned after filtering. Default 5000. */\n maxEntries?: number\n /** Optional host-provided cache seam. */\n cache?: FileIndexCache\n /** Cache TTL in seconds when `cache` is set. Default 20. */\n cacheTtlSeconds?: number\n}\n\n/** Segment names ignored anywhere in a path, beyond the generic dotfile rule\n * below. Intentionally small and language/framework-agnostic — callers\n * extend it via `ignore` for anything domain-specific (e.g. a vault's\n * `uploads` dir). */\nconst DEFAULT_IGNORE_SEGMENTS = [\n 'node_modules',\n 'dist',\n 'build',\n 'out',\n 'coverage',\n 'target',\n '__pycache__',\n 'venv',\n]\n\n/** A path segment starting with `.` (`.git`, `.env`, `.next`, `.cache`, …) is\n * always ignored — this single rule covers most dot-prefixed VCS/tooling\n * dirs and dotfiles without enumerating them. */\nfunction isIgnored(relPath: string, ignoreSegments: ReadonlySet<string>): boolean {\n for (const segment of relPath.split('/')) {\n if (!segment) continue\n if (segment.startsWith('.')) return true\n if (ignoreSegments.has(segment)) return true\n }\n return false\n}\n\n/** Strips the tree result's echoed `root` prefix so entries are always\n * workspace-relative, whichever convention the structural `fs.tree` uses\n * (root-relative already, or root-prefixed). */\nfunction relativeTo(root: string, path: string): string {\n const prefix = root.endsWith('/') ? root : `${root}/`\n if (path.startsWith(prefix)) return path.slice(prefix.length)\n if (path === root) return ''\n return path\n}\n\nfunction basename(path: string): string {\n const segments = path.split('/').filter(Boolean)\n return segments[segments.length - 1] ?? path\n}\n\nexport function createSandboxFileIndexRoute(\n options: CreateSandboxFileIndexRouteOptions,\n): (request: Request) => Promise<Response> {\n const maxDepth = options.maxDepth ?? 12\n const maxEntries = options.maxEntries ?? 5000\n const cacheTtlSeconds = options.cacheTtlSeconds ?? 20\n const staticIgnore = new Set([...DEFAULT_IGNORE_SEGMENTS, ...(options.ignore ?? [])])\n\n return async function fileIndex(request: Request): Promise<Response> {\n const auth = await options.authorize({ request })\n if (auth.status === 'denied') return auth.response\n if (auth.status === 'warming') {\n return Response.json({ status: 'warming' } satisfies FileIndexWarmingResponse)\n }\n\n const cache = options.cache\n if (cache && auth.cacheKey) {\n const cached = await cache.get(auth.cacheKey)\n if (cached) return Response.json(cached)\n }\n\n const ignoreSegments = auth.ignore?.length\n ? new Set([...staticIgnore, ...auth.ignore])\n : staticIgnore\n\n const scan = await auth.fs.tree(auth.root, { maxDepth })\n const filtered = scan.files.filter((f) => !isIgnored(relativeTo(scan.root, f.path), ignoreSegments))\n const truncated = scan.stats.truncated || filtered.length > maxEntries\n const files: FileMention[] = filtered.slice(0, maxEntries).map((f) => {\n const path = relativeTo(scan.root, f.path)\n const entry: FileMention = { path, name: basename(path) }\n if (typeof f.size === 'number') entry.size = f.size\n return entry\n })\n\n const body: FileIndexReadyResponse = {\n status: 'ready',\n files,\n truncated,\n generatedAt: new Date().toISOString(),\n }\n\n if (cache && auth.cacheKey) await cache.put(auth.cacheKey, body, { ttlSeconds: cacheTtlSeconds })\n\n return Response.json(body)\n }\n}\n"],"mappings":"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","names":[]}
@@ -2,7 +2,7 @@ import {
2
2
  interactionFromWireRequest,
3
3
  isSafeInteractionFieldKey,
4
4
  questionInteractionContentSignature
5
- } from "./chunk-4TXDD6P2.js";
5
+ } from "./chunk-XAWFPMAR.js";
6
6
 
7
7
  // src/interactions/sidecar.ts
8
8
  var DEFAULT_TIMEOUT_MS = 5e3;
@@ -148,6 +148,10 @@ function createInteractionAnswerRoute(options) {
148
148
  }
149
149
  const validation = validateInteractionAnswerBody(body);
150
150
  if (!validation.ok) return Response.json({ error: validation.error }, { status: 400 });
151
+ const attemptKey = typeof body.attemptKey === "string" ? body.attemptKey.trim() : "";
152
+ if (options.durable && !attemptKey) {
153
+ return Response.json({ error: "Missing attemptKey for durable interaction answer" }, { status: 400 });
154
+ }
151
155
  const resolution = await options.resolveConnection({ request, intent: "answer", body });
152
156
  if (!resolution.ok) {
153
157
  if ("response" in resolution) return resolution.response;
@@ -162,21 +166,101 @@ function createInteractionAnswerRoute(options) {
162
166
  ...validation.data ? { data: validation.data } : {}
163
167
  };
164
168
  const before = await listSessionInteractions(connection);
169
+ if (!before.succeeded && (options.beforeAnswer || options.durable)) {
170
+ return mapInteractionRespondFailure(before.error, logger);
171
+ }
165
172
  const answeredRequest = before.succeeded ? before.value.find((item) => item.id === validation.id) : void 0;
173
+ if (options.beforeAnswer && !options.durable && !answeredRequest) {
174
+ return mapInteractionRespondFailure(
175
+ { code: "NOT_FOUND", message: "interaction not found", status: 404 },
176
+ logger
177
+ );
178
+ }
166
179
  const answeredSignature = answeredRequest ? questionInteractionContentSignature(interactionFromWireRequest(answeredRequest)) : null;
180
+ const duplicateRequests = before.succeeded && answeredSignature ? before.value.filter((item) => item.id !== validation.id && questionInteractionContentSignature(interactionFromWireRequest(item)) === answeredSignature) : [];
181
+ const lifecycleArgs = {
182
+ request,
183
+ body,
184
+ answer: validation,
185
+ connection,
186
+ outstanding: before.succeeded ? before.value : [],
187
+ ...answeredRequest ? { answeredRequest } : {},
188
+ duplicateRequests
189
+ };
190
+ if (options.beforeAnswer && answeredRequest) {
191
+ try {
192
+ await options.beforeAnswer(lifecycleArgs);
193
+ } catch (error) {
194
+ logger.error("[interactions] beforeAnswer failed:", error);
195
+ return Response.json(
196
+ { code: "INTERACTION_BEFORE_ANSWER_FAILED", error: "Could not save the answer. Try again." },
197
+ { status: 503 }
198
+ );
199
+ }
200
+ }
201
+ let prepared;
202
+ const durableArgs = options.durable ? { ...lifecycleArgs, attemptKey } : null;
203
+ if (options.durable && durableArgs) {
204
+ try {
205
+ prepared = await options.durable.prepare(durableArgs);
206
+ } catch (error) {
207
+ logger.error("[interactions] durable prepare failed:", error);
208
+ return Response.json(
209
+ { code: "INTERACTION_PREPARE_FAILED", error: "Could not prepare the answer. Try again." },
210
+ { status: 503 }
211
+ );
212
+ }
213
+ if (!answeredRequest) {
214
+ let reconciled;
215
+ try {
216
+ reconciled = await options.durable.reconcile({ ...durableArgs, prepared });
217
+ } catch (error) {
218
+ logger.error("[interactions] durable reconcile failed:", error);
219
+ reconciled = { settled: false };
220
+ }
221
+ if (reconciled.settled) return Response.json({ ok: true, idempotent: true });
222
+ return Response.json(
223
+ { code: "INTERACTION_RECONCILIATION_PENDING", error: "The answer status is still being checked. Retry with the same attempt." },
224
+ { status: 503 }
225
+ );
226
+ }
227
+ }
167
228
  const result = await respondToSessionInteraction(connection, { id: validation.id, ...answerPayload });
168
- if (!result.succeeded) return mapInteractionRespondFailure(result.error, logger);
229
+ if (!result.succeeded) {
230
+ if (options.durable && durableArgs) {
231
+ let reconciled;
232
+ try {
233
+ reconciled = await options.durable.reconcile({ ...durableArgs, prepared });
234
+ } catch {
235
+ reconciled = { settled: false };
236
+ }
237
+ if (reconciled.settled) return Response.json({ ok: true, idempotent: true });
238
+ if (result.error.status === 404) {
239
+ return Response.json(
240
+ { code: "INTERACTION_RECONCILIATION_PENDING", error: "The answer status is still being checked. Retry with the same attempt." },
241
+ { status: 503 }
242
+ );
243
+ }
244
+ try {
245
+ await options.durable.fail?.({ ...durableArgs, prepared, error: result.error });
246
+ } catch {
247
+ }
248
+ }
249
+ return mapInteractionRespondFailure(result.error, logger);
250
+ }
169
251
  let remaining = await listSessionInteractions(connection);
252
+ const acknowledgedDuplicateIds = [];
170
253
  if (remaining.succeeded && answeredSignature) {
171
- const duplicateRequests = remaining.value.filter((item) => {
254
+ const remainingDuplicates = remaining.value.filter((item) => {
172
255
  if (item.id === validation.id) return false;
173
256
  return questionInteractionContentSignature(interactionFromWireRequest(item)) === answeredSignature;
174
257
  });
175
- for (const duplicate of duplicateRequests) {
258
+ for (const duplicate of remainingDuplicates) {
176
259
  const duplicateResult = await respondToSessionInteraction(connection, { id: duplicate.id, ...answerPayload });
177
260
  if (!duplicateResult.succeeded) break;
261
+ acknowledgedDuplicateIds.push(duplicate.id);
178
262
  }
179
- if (duplicateRequests.length > 0) remaining = await listSessionInteractions(connection);
263
+ if (remainingDuplicates.length > 0) remaining = await listSessionInteractions(connection);
180
264
  }
181
265
  if (remaining.succeeded && remaining.value.some((item) => item.id === validation.id || answeredSignature && questionInteractionContentSignature(interactionFromWireRequest(item)) === answeredSignature)) {
182
266
  logger.error("[interactions] respond returned ok but interaction is still pending:", {
@@ -188,6 +272,26 @@ function createInteractionAnswerRoute(options) {
188
272
  { status: 503 }
189
273
  );
190
274
  }
275
+ if (options.durable && durableArgs) {
276
+ try {
277
+ await options.durable.acknowledge({
278
+ ...durableArgs,
279
+ prepared,
280
+ duplicateIds: acknowledgedDuplicateIds
281
+ });
282
+ await options.durable.finalize({
283
+ ...durableArgs,
284
+ prepared,
285
+ duplicateIds: acknowledgedDuplicateIds
286
+ });
287
+ } catch (error) {
288
+ logger.error("[interactions] durable finalize failed:", error);
289
+ return Response.json(
290
+ { code: "INTERACTION_FINALIZE_FAILED", error: "The answer was accepted but is still being saved. Retry to reconcile it." },
291
+ { status: 503 }
292
+ );
293
+ }
294
+ }
191
295
  return Response.json({ ok: true });
192
296
  }
193
297
  return { list, answer };
@@ -200,4 +304,4 @@ export {
200
304
  mapInteractionRespondFailure,
201
305
  createInteractionAnswerRoute
202
306
  };
203
- //# sourceMappingURL=chunk-Y4QHNQ75.js.map
307
+ //# sourceMappingURL=chunk-6KVH5SP5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/interactions/sidecar.ts","../src/interactions/route.ts"],"sourcesContent":["/**\n * Server-side client for the sandbox sidecar's generic interaction routes\n * (`GET/POST {runtimeUrl}/agents/sessions/{sessionId}/interactions`). The\n * pinned sandbox SDK exposes only the question-specific `session().answer()`\n * convenience; these raw calls are backend-agnostic (question/permission/plan,\n * any harness) and carry explicit outcomes (accepted/declined).\n *\n * Server-only: the sidecar bearer must never reach browser code. The caller\n * supplies the connection as a structural value (runtime URL + bearer +\n * session id) — no sandbox-SDK import, so any box-resolution strategy works.\n */\n\nimport type { InteractionData, InteractionOutcome, InteractionRequestWire } from './contract'\n\nexport interface SidecarInteractionsError {\n code: string\n message: string\n /** Upstream HTTP status; 0 when the sidecar was unreachable. */\n status: number\n}\n\nexport type SidecarInteractionsResult<T> =\n | { succeeded: true; value: T }\n | { succeeded: false; error: SidecarInteractionsError }\n\n/** Where and how to reach one session's interaction registry. */\nexport interface SidecarInteractionsConnection {\n runtimeUrl: string\n authToken?: string\n /** The sidecar agent-session id (the chat thread's session). */\n sessionId: string\n /** Request deadline. A pending interaction means the box is up and the\n * sidecar responsive; a short default keeps a wedged runtime from stalling\n * the answering request. */\n timeoutMs?: number\n /** Injection seam for tests; defaults to global fetch. */\n fetchImpl?: typeof fetch\n}\n\nconst DEFAULT_TIMEOUT_MS = 5_000\n\n/** Strips bearer tokens / key material before an upstream message is logged\n * or surfaced. */\nfunction sanitizeUpstreamMessage(input: unknown): string {\n const message = input instanceof Error ? input.message : String(input)\n return message\n .replace(/Bearer\\s+[^\\s]+/gi, 'Bearer [redacted]')\n .replace(/\\b(?:sk|pk|tc)[_-][A-Za-z0-9_-]{8,}\\b/g, '[redacted-key]')\n}\n\nasync function interactionsFetch(\n connection: SidecarInteractionsConnection,\n init: { method: 'GET' } | { method: 'POST'; body: Record<string, unknown> },\n): Promise<SidecarInteractionsResult<Record<string, unknown>>> {\n const doFetch = connection.fetchImpl ?? fetch\n const url = `${connection.runtimeUrl.replace(/\\/$/, '')}/agents/sessions/${encodeURIComponent(connection.sessionId)}/interactions`\n let response: Response\n try {\n response = await doFetch(url, {\n method: init.method,\n headers: {\n ...(connection.authToken ? { Authorization: `Bearer ${connection.authToken}` } : {}),\n ...(init.method === 'POST' ? { 'Content-Type': 'application/json' } : {}),\n },\n ...(init.method === 'POST' ? { body: JSON.stringify(init.body) } : {}),\n signal: AbortSignal.timeout(connection.timeoutMs ?? DEFAULT_TIMEOUT_MS),\n })\n } catch (err) {\n return {\n succeeded: false,\n error: { code: 'UPSTREAM_UNREACHABLE', message: sanitizeUpstreamMessage(err), status: 0 },\n }\n }\n const raw = await response.text().catch(() => '')\n let parsed: Record<string, unknown> = {}\n try {\n parsed = raw ? (JSON.parse(raw) as Record<string, unknown>) : {}\n } catch {\n // Non-JSON error bodies (proxy 502 pages) fall through to the status check.\n }\n if (!response.ok) {\n const upstreamError = (parsed.error ?? {}) as { code?: unknown; message?: unknown }\n return {\n succeeded: false,\n error: {\n code: typeof upstreamError.code === 'string' && upstreamError.code ? upstreamError.code : 'UPSTREAM_ERROR',\n message: sanitizeUpstreamMessage(\n typeof upstreamError.message === 'string' && upstreamError.message\n ? upstreamError.message\n : `sidecar interactions ${init.method} failed (${response.status})`,\n ),\n status: response.status,\n },\n }\n }\n return { succeeded: true, value: parsed }\n}\n\n/** Outstanding (unanswered) interactions for the session — the sidecar's\n * registry is authoritative, so this is the reconnect/reload source of truth. */\nexport async function listSessionInteractions(\n connection: SidecarInteractionsConnection,\n): Promise<SidecarInteractionsResult<InteractionRequestWire[]>> {\n const result = await interactionsFetch(connection, { method: 'GET' })\n if (!result.succeeded) return result\n const data = result.value.data as { interactions?: unknown } | undefined\n if (!Array.isArray(data?.interactions)) {\n return {\n succeeded: false,\n error: { code: 'MALFORMED_RESPONSE', message: 'sidecar list returned no interactions array', status: 200 },\n }\n }\n return { succeeded: true, value: data.interactions as InteractionRequestWire[] }\n}\n\n/** Resolves one interaction. `data` is required by the sidecar only for\n * `accepted` outcomes and is validated fail-closed against the answerSpec\n * (400 INVALID_INTERACTION_ANSWER on mismatch). */\nexport async function respondToSessionInteraction(\n connection: SidecarInteractionsConnection,\n response: { id: string; outcome: InteractionOutcome; data?: InteractionData },\n): Promise<SidecarInteractionsResult<void>> {\n const result = await interactionsFetch(connection, {\n method: 'POST',\n body: {\n id: response.id,\n outcome: response.outcome,\n ...(response.data ? { data: response.data } : {}),\n },\n })\n if (!result.succeeded) return result\n return { succeeded: true, value: undefined }\n}\n","/**\n * Framework-neutral interaction-answer endpoints, lifted out of the per-app\n * route files (gtm `api.chat.interactions`, legal `api.chat.interactions`,\n * tax `api.sessions.$id.interactions` — three byte-similar forks):\n *\n * list(request) — GET: outstanding asks for a live turn (reload restore)\n * answer(request) — POST `{ id, outcome, data? }`: resolve one ask\n *\n * The product supplies ONE seam, `resolveConnection`: authenticate the caller,\n * authorize the thread/session, and resolve the sidecar connection. Everything\n * behind the seam is mechanism the forks kept re-fixing:\n *\n * - body validation (safe field keys, typed values),\n * - sidecar error → client contract mapping (every \"the ask is gone\" shape\n * becomes 410 so the card flips to its expired state),\n * - duplicate resolution: after answering, every other outstanding ask with\n * the same content signature (a re-emitted duplicate) gets the same answer,\n * - unblock verification: re-list and fail loud (503 INTERACTION_STILL_PENDING)\n * when the sidecar accepted the POST but the ask is still open,\n * - best-effort list: sidecar failures return `{ interactions: [],\n * unavailable }` so a reload restore never breaks the live stream.\n *\n * Handlers return web-standard `Response`s (Workers, Node 18+, Deno) — no\n * router import anywhere.\n */\n\nimport {\n interactionFromWireRequest,\n isSafeInteractionFieldKey,\n questionInteractionContentSignature,\n type InteractionData,\n type InteractionRequestWire,\n} from './contract'\nimport {\n listSessionInteractions,\n respondToSessionInteraction,\n type SidecarInteractionsConnection,\n type SidecarInteractionsError,\n} from './sidecar'\n\n// A client resolves an ask by answering (`accepted`) or refusing (`declined`).\n// Withdrawal (`cancelled`) is an agent/broker outcome delivered via the\n// `interaction.cancel` event, never a client POST, so it is not accepted here.\nexport type InteractionClientOutcome = 'accepted' | 'declined'\n\nexport type InteractionAnswerBodyValidation =\n | { ok: true; id: string; outcome: InteractionClientOutcome; data?: InteractionData }\n | { ok: false; error: string }\n\n/** Validates the client POST body: `{ id, outcome, data? }` with\n * identifier-safe field keys and primitive/string-array values only. */\nexport function validateInteractionAnswerBody(body: Record<string, unknown>): InteractionAnswerBodyValidation {\n const id = typeof body.id === 'string' && body.id ? body.id : null\n if (!id) return { ok: false, error: 'Missing interaction id' }\n const outcome = body.outcome\n if (outcome !== 'accepted' && outcome !== 'declined') {\n return { ok: false, error: 'Invalid outcome: expected accepted or declined' }\n }\n if (body.data === undefined) return { ok: true, id, outcome }\n if (!body.data || typeof body.data !== 'object' || Array.isArray(body.data)) {\n return { ok: false, error: 'Invalid data: expected an object of field values' }\n }\n const data: InteractionData = {}\n for (const [key, value] of Object.entries(body.data)) {\n if (!isSafeInteractionFieldKey(key)) {\n return { ok: false, error: 'Invalid data: field names must contain only letters, numbers, underscores, or hyphens' }\n }\n const validValue =\n typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' ||\n (Array.isArray(value) && value.every((item) => typeof item === 'string'))\n if (!validValue) {\n return { ok: false, error: 'Invalid data: field values must be strings, numbers, booleans, or string arrays' }\n }\n data[key] = value as InteractionData[string]\n }\n return { ok: true, id, outcome, data }\n}\n\nexport type InteractionRouteLogger = Pick<Console, 'warn' | 'error'>\n\n/** Sidecar error → the client-actionable contract. Every \"the ask is gone\"\n * shape maps to 410 so the card flips to its expired state — a raw 404/409\n * must never surface. */\nexport function mapInteractionRespondFailure(\n error: SidecarInteractionsError,\n logger: InteractionRouteLogger = console,\n): Response {\n if (error.code === 'INVALID_INTERACTION_ANSWER') {\n return Response.json(\n { code: 'INVALID_INTERACTION_ANSWER', error: 'This question needs an answer from the card above — pick one of the listed options.' },\n { status: 400 },\n )\n }\n if (error.status === 404) {\n return Response.json(\n { code: 'INTERACTION_EXPIRED', error: 'This question is no longer waiting for an answer.' },\n { status: 410 },\n )\n }\n if (error.status === 501 || error.code === 'NOT_IMPLEMENTED') {\n return Response.json(\n { code: 'INTERACTIONS_UNSUPPORTED', error: 'This agent backend cannot accept answers this way.' },\n { status: 501 },\n )\n }\n logger.error('[interactions] respond failed:', error)\n return Response.json(\n { code: 'INTERACTION_UPSTREAM_FAILED', error: 'Could not reach the agent. Try again.' },\n { status: 503 },\n )\n}\n\n/** The product seam's verdict for one request. `response` short-circuits with\n * a product-authored Response (401/404/429…); `unavailable` means the caller\n * is fine but the sandbox runtime is not reachable — the factory shapes that\n * per intent (empty list for `list`, 503 for `answer`). */\nexport type InteractionConnectionResolution =\n | { ok: true; connection: SidecarInteractionsConnection }\n | { ok: false; response: Response }\n | { ok: false; unavailable: string }\n\nexport interface ResolveInteractionConnectionArgs {\n request: Request\n intent: 'list' | 'answer'\n /** The parsed, validated POST body (answer intent only) so the resolver can\n * read product routing fields (workspaceId/threadId) without re-parsing. */\n body?: Record<string, unknown>\n}\n\nexport interface BeforeInteractionAnswerArgs {\n request: Request\n /** Original parsed body, including product routing fields. */\n body: Record<string, unknown>\n /** Shared validation result; products never need to parse the answer again. */\n answer: Extract<InteractionAnswerBodyValidation, { ok: true }>\n connection: SidecarInteractionsConnection\n /** The route's single authoritative pre-answer sidecar snapshot. */\n outstanding: InteractionRequestWire[]\n answeredRequest?: InteractionRequestWire\n /** Content-identical questions that the shared route will also answer. */\n duplicateRequests: InteractionRequestWire[]\n}\n\nexport interface DurableInteractionRouteArgs extends BeforeInteractionAnswerArgs {\n /** Caller-created opaque identifier, stable across an ambiguous retry. */\n attemptKey: string\n}\n\n/** Crash-recoverable persistence lifecycle for the answer route. The product\n * binds this structural port to `/durable-chat` (or an equivalent durable\n * store). Existing `beforeAnswer` behavior remains independent and unchanged. */\nexport interface DurableInteractionRoutePersistence<TPrepared = unknown> {\n guarantee: 'reconciled' | 'best-effort'\n prepare(args: DurableInteractionRouteArgs): TPrepared | Promise<TPrepared>\n /** Resolve an ambiguous retry (for example, the sidecar says the ask is\n * already gone). Only `settled:true` permits the route to report success. */\n reconcile(args: DurableInteractionRouteArgs & { prepared: TPrepared }):\n | { settled: boolean }\n | Promise<{ settled: boolean }>\n /** Record the authority acknowledgement before terminal projection. */\n acknowledge(args: DurableInteractionRouteArgs & {\n prepared: TPrepared\n duplicateIds: string[]\n }): void | Promise<void>\n /** Idempotently materialize terminal status and accepted values. */\n finalize(args: DurableInteractionRouteArgs & {\n prepared: TPrepared\n duplicateIds: string[]\n }): void | Promise<void>\n fail?(args: DurableInteractionRouteArgs & { prepared: TPrepared; error: unknown }): void | Promise<void>\n}\n\nexport interface InteractionAnswerRouteOptions {\n /** Authenticate + authorize the caller and resolve the sidecar connection.\n * This is the only product-supplied step: session auth, workspace/thread\n * access, rate limiting, and box resolution all live here. */\n resolveConnection: (args: ResolveInteractionConnectionArgs) => Promise<InteractionConnectionResolution>\n /** Product persistence seam that runs before the answer can unblock and\n * finalize the agent turn. A throw aborts the request before any answer POST. */\n beforeAnswer?: (args: BeforeInteractionAnswerArgs) => void | Promise<void>\n /** Additive crash-recoverable settlement. When configured, POST requires an\n * `attemptKey`; accepted values are finalized only after sidecar ack. */\n durable?: DurableInteractionRoutePersistence\n logger?: InteractionRouteLogger\n}\n\nexport interface InteractionAnswerRoute {\n /** GET — outstanding interactions for a live turn. Failures return an empty\n * list with an explicit `unavailable` code: the caller is a best-effort\n * reload restore, and the live/replayed stream must stay untouched when the\n * sidecar cannot answer. */\n list: (request: Request) => Promise<Response>\n /** POST `{ id, outcome, data?, ...productFields }` — resolve one ask, answer\n * content-identical duplicates the same way, then re-list to prove the run\n * actually unblocked. */\n answer: (request: Request) => Promise<Response>\n}\n\nexport function createInteractionAnswerRoute(options: InteractionAnswerRouteOptions): InteractionAnswerRoute {\n const logger = options.logger ?? console\n\n async function list(request: Request): Promise<Response> {\n const resolution = await options.resolveConnection({ request, intent: 'list' })\n if (!resolution.ok) {\n if ('response' in resolution) return resolution.response\n return Response.json({ interactions: [], unavailable: resolution.unavailable })\n }\n const result = await listSessionInteractions(resolution.connection)\n if (!result.succeeded) {\n logger.warn('[interactions] list failed:', result.error)\n return Response.json({ interactions: [], unavailable: result.error.code })\n }\n return Response.json({ interactions: result.value })\n }\n\n async function answer(request: Request): Promise<Response> {\n const body = await request.json().catch(() => null) as Record<string, unknown> | null\n if (!body || typeof body !== 'object' || Array.isArray(body)) {\n return Response.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n const validation = validateInteractionAnswerBody(body)\n if (!validation.ok) return Response.json({ error: validation.error }, { status: 400 })\n const attemptKey = typeof body.attemptKey === 'string' ? body.attemptKey.trim() : ''\n if (options.durable && !attemptKey) {\n return Response.json({ error: 'Missing attemptKey for durable interaction answer' }, { status: 400 })\n }\n\n const resolution = await options.resolveConnection({ request, intent: 'answer', body })\n if (!resolution.ok) {\n if ('response' in resolution) return resolution.response\n return mapInteractionRespondFailure(\n { code: resolution.unavailable, message: 'sandbox runtime unavailable', status: 0 },\n logger,\n )\n }\n const connection = resolution.connection\n const answerPayload = {\n outcome: validation.outcome,\n ...(validation.data ? { data: validation.data } : {}),\n }\n\n // Snapshot the answered ask's content signature BEFORE resolving it, so any\n // content-identical duplicates still outstanding afterwards (the agent may\n // have re-emitted the same question N times) can be answered the same way.\n const before = await listSessionInteractions(connection)\n if (!before.succeeded && (options.beforeAnswer || options.durable)) {\n return mapInteractionRespondFailure(before.error, logger)\n }\n const answeredRequest = before.succeeded ? before.value.find((item) => item.id === validation.id) : undefined\n if (options.beforeAnswer && !options.durable && !answeredRequest) {\n return mapInteractionRespondFailure(\n { code: 'NOT_FOUND', message: 'interaction not found', status: 404 },\n logger,\n )\n }\n const answeredSignature = answeredRequest\n ? questionInteractionContentSignature(interactionFromWireRequest(answeredRequest))\n : null\n const duplicateRequests = before.succeeded && answeredSignature\n ? before.value.filter((item) => item.id !== validation.id && (\n questionInteractionContentSignature(interactionFromWireRequest(item)) === answeredSignature\n ))\n : []\n\n const lifecycleArgs: BeforeInteractionAnswerArgs = {\n request,\n body,\n answer: validation,\n connection,\n outstanding: before.succeeded ? before.value : [],\n ...(answeredRequest ? { answeredRequest } : {}),\n duplicateRequests,\n }\n\n if (options.beforeAnswer && answeredRequest) {\n try {\n await options.beforeAnswer(lifecycleArgs)\n } catch (error) {\n logger.error('[interactions] beforeAnswer failed:', error)\n return Response.json(\n { code: 'INTERACTION_BEFORE_ANSWER_FAILED', error: 'Could not save the answer. Try again.' },\n { status: 503 },\n )\n }\n }\n\n let prepared: unknown\n const durableArgs = options.durable\n ? { ...lifecycleArgs, attemptKey }\n : null\n if (options.durable && durableArgs) {\n try {\n prepared = await options.durable.prepare(durableArgs)\n } catch (error) {\n logger.error('[interactions] durable prepare failed:', error)\n return Response.json(\n { code: 'INTERACTION_PREPARE_FAILED', error: 'Could not prepare the answer. Try again.' },\n { status: 503 },\n )\n }\n if (!answeredRequest) {\n let reconciled: { settled: boolean }\n try {\n reconciled = await options.durable.reconcile({ ...durableArgs, prepared })\n } catch (error) {\n logger.error('[interactions] durable reconcile failed:', error)\n reconciled = { settled: false }\n }\n if (reconciled.settled) return Response.json({ ok: true, idempotent: true })\n return Response.json(\n { code: 'INTERACTION_RECONCILIATION_PENDING', error: 'The answer status is still being checked. Retry with the same attempt.' },\n { status: 503 },\n )\n }\n }\n\n const result = await respondToSessionInteraction(connection, { id: validation.id, ...answerPayload })\n if (!result.succeeded) {\n if (options.durable && durableArgs) {\n let reconciled: { settled: boolean }\n try {\n reconciled = await options.durable.reconcile({ ...durableArgs, prepared })\n } catch {\n reconciled = { settled: false }\n }\n if (reconciled.settled) return Response.json({ ok: true, idempotent: true })\n if (result.error.status === 404) {\n return Response.json(\n { code: 'INTERACTION_RECONCILIATION_PENDING', error: 'The answer status is still being checked. Retry with the same attempt.' },\n { status: 503 },\n )\n }\n try {\n await options.durable.fail?.({ ...durableArgs, prepared, error: result.error })\n } catch {\n // The upstream failure remains authoritative; a persistence cleanup\n // failure must not replace its client-facing mapping.\n }\n }\n return mapInteractionRespondFailure(result.error, logger)\n }\n\n let remaining = await listSessionInteractions(connection)\n const acknowledgedDuplicateIds: string[] = []\n if (remaining.succeeded && answeredSignature) {\n const remainingDuplicates = remaining.value.filter((item) => {\n if (item.id === validation.id) return false\n return questionInteractionContentSignature(interactionFromWireRequest(item)) === answeredSignature\n })\n for (const duplicate of remainingDuplicates) {\n const duplicateResult = await respondToSessionInteraction(connection, { id: duplicate.id, ...answerPayload })\n if (!duplicateResult.succeeded) break\n acknowledgedDuplicateIds.push(duplicate.id)\n }\n if (remainingDuplicates.length > 0) remaining = await listSessionInteractions(connection)\n }\n\n // Prove the run actually unblocked: neither the answered id nor any\n // content-duplicate may still be outstanding. If one is, the sidecar\n // accepted the POST but did not release the run — report it rather than\n // tell the user it was answered.\n if (remaining.succeeded && remaining.value.some((item) => item.id === validation.id || (\n answeredSignature && questionInteractionContentSignature(interactionFromWireRequest(item)) === answeredSignature\n ))) {\n logger.error('[interactions] respond returned ok but interaction is still pending:', {\n sessionId: connection.sessionId,\n interactionId: validation.id,\n })\n return Response.json(\n { code: 'INTERACTION_STILL_PENDING', error: 'The agent did not accept the answer. Try answering again.' },\n { status: 503 },\n )\n }\n\n if (options.durable && durableArgs) {\n try {\n await options.durable.acknowledge({\n ...durableArgs,\n prepared,\n duplicateIds: acknowledgedDuplicateIds,\n })\n await options.durable.finalize({\n ...durableArgs,\n prepared,\n duplicateIds: acknowledgedDuplicateIds,\n })\n } catch (error) {\n logger.error('[interactions] durable finalize failed:', error)\n return Response.json(\n { code: 'INTERACTION_FINALIZE_FAILED', error: 'The answer was accepted but is still being saved. Retry to reconcile it.' },\n { status: 503 },\n )\n }\n }\n\n return Response.json({ ok: true })\n }\n\n return { list, answer }\n}\n"],"mappings":"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","names":[]}