@vellumai/assistant 0.12.2 → 0.12.3-staging.1

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 (298) hide show
  1. package/.env.example +2 -0
  2. package/AGENTS.md +1 -1
  3. package/docs/architecture/integrations.md +7 -0
  4. package/docs/architecture/memory.md +17 -2
  5. package/docs/credential-execution-service.md +1 -1
  6. package/docs/desktop-browser-cli.md +7 -3
  7. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
  9. package/node_modules/@vellumai/environments/src/shell.test.ts +21 -0
  10. package/node_modules/@vellumai/environments/src/shell.ts +24 -0
  11. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
  12. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
  13. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +8 -2
  14. package/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
  15. package/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
  16. package/openapi.yaml +54 -5
  17. package/package.json +1 -1
  18. package/scripts/smoke-desktop-browser-cli.ts +1 -0
  19. package/scripts/sync-llm-catalog.ts +3 -0
  20. package/scripts/voice-ttft-spike.ts +2 -2
  21. package/src/__tests__/agent-loop.test.ts +259 -0
  22. package/src/__tests__/agent-wake-delegation-prompt.test.ts +64 -2
  23. package/src/__tests__/anthropic-provider.test.ts +55 -0
  24. package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
  25. package/src/__tests__/attachments-store.test.ts +22 -3
  26. package/src/__tests__/channel-approval.test.ts +9 -14
  27. package/src/__tests__/channel-reply-delivery.test.ts +56 -0
  28. package/src/__tests__/chat-credential-redaction.test.ts +23 -0
  29. package/src/__tests__/computer-use-screenshot-attachments.test.ts +411 -0
  30. package/src/__tests__/computer-use-screenshot-selection.test.ts +50 -0
  31. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
  32. package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
  33. package/src/__tests__/conversation-agent-loop.test.ts +257 -6
  34. package/src/__tests__/conversation-attachments.test.ts +106 -0
  35. package/src/__tests__/conversation-fork-crud.test.ts +191 -0
  36. package/src/__tests__/conversation-rendered-delegation-state.test.ts +276 -0
  37. package/src/__tests__/credential-execution-client.test.ts +70 -53
  38. package/src/__tests__/db-conversation-tool-surface.test.ts +221 -0
  39. package/src/__tests__/events-tail-route.test.ts +33 -0
  40. package/src/__tests__/history-repair.test.ts +53 -0
  41. package/src/__tests__/inference-profile-session-handler.test.ts +26 -0
  42. package/src/__tests__/list-messages-tool-merge.test.ts +85 -1
  43. package/src/__tests__/llm-catalog-parity.test.ts +27 -4
  44. package/src/__tests__/oauth-apps-routes.test.ts +1 -0
  45. package/src/__tests__/oauth-commands-routes.test.ts +114 -101
  46. package/src/__tests__/oauth-connect-orchestrator.test.ts +2 -0
  47. package/src/__tests__/oauth-provider-serializer.test.ts +1 -0
  48. package/src/__tests__/oauth-providers-routes.test.ts +2 -0
  49. package/src/__tests__/persist-media-references.test.ts +50 -0
  50. package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
  51. package/src/__tests__/run-conversation-turn-persistence.test.ts +138 -1
  52. package/src/__tests__/schedule-routes.test.ts +20 -0
  53. package/src/__tests__/scheduler-result-notification.test.ts +23 -4
  54. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  55. package/src/__tests__/secret-routes-platform-proxy.test.ts +86 -31
  56. package/src/__tests__/secret-routes-scrub.test.ts +22 -27
  57. package/src/__tests__/secret-scanner.test.ts +20 -0
  58. package/src/__tests__/secure-keys-managed-failover.test.ts +8 -0
  59. package/src/__tests__/secure-keys.test.ts +7 -3
  60. package/src/__tests__/server-tool-pairing.test.ts +107 -0
  61. package/src/__tests__/skills.test.ts +5 -4
  62. package/src/__tests__/subagent-tool-gate-mode.test.ts +226 -0
  63. package/src/__tests__/terminal-tools.test.ts +8 -0
  64. package/src/__tests__/tool-result-follow-up.test.ts +99 -0
  65. package/src/__tests__/tool-result-metadata-plumbing.test.ts +63 -0
  66. package/src/__tests__/unicode.test.ts +36 -0
  67. package/src/agent/loop.ts +37 -12
  68. package/src/agent/tool-result-follow-up.ts +70 -0
  69. package/src/api/attachment-provenance.test.ts +68 -0
  70. package/src/api/computer-use-tool.test.ts +55 -0
  71. package/src/api/computer-use-tool.ts +27 -0
  72. package/src/api/events/assistant-outbound-attachment.ts +3 -0
  73. package/src/api/events/desktop-activity-changed.ts +10 -0
  74. package/src/api/events/question-request.ts +1 -0
  75. package/src/api/index.ts +11 -0
  76. package/src/api/responses/conversation-message.ts +3 -0
  77. package/src/approvals/approval-primitive.ts +5 -2
  78. package/src/approvals/scoped-approval-grants.ts +6 -2
  79. package/src/browser/virtual-desktop-target.ts +1 -2
  80. package/src/cli/commands/__tests__/cli-test-harness.ts +21 -3
  81. package/src/cli/commands/__tests__/plugins.test.ts +20 -1
  82. package/src/cli/commands/__tests__/schedules.test.ts +14 -0
  83. package/src/cli/commands/bash.help.ts +4 -3
  84. package/src/cli/commands/browser.help.ts +5 -1
  85. package/src/cli/commands/credentials.help.ts +3 -3
  86. package/src/cli/commands/oauth/index.help.ts +10 -0
  87. package/src/cli/commands/oauth/providers.ts +12 -0
  88. package/src/cli/commands/oauth/request.test.ts +179 -2
  89. package/src/cli/commands/oauth/request.ts +41 -10
  90. package/src/cli/commands/plugins.ts +12 -5
  91. package/src/cli/commands/schedules.ts +2 -0
  92. package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
  93. package/src/cli/lib/__tests__/install-from-github.test.ts +41 -0
  94. package/src/cli/lib/__tests__/local-plugin-upgrade.test.ts +133 -51
  95. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +8 -2
  96. package/src/cli/lib/bundled-marketplace.json +855 -0
  97. package/src/cli/lib/bundled-plugin-packages.json +783 -1
  98. package/src/cli/lib/inspect-plugin.ts +11 -4
  99. package/src/cli/lib/upgrade-plugin.ts +19 -11
  100. package/src/cli/output.ts +18 -4
  101. package/src/config/__tests__/default-provider.test.ts +4 -0
  102. package/src/config/bundled-skills/schedule/SKILL.md +6 -6
  103. package/src/config/loader.ts +9 -17
  104. package/src/config/platform-identity.ts +8 -8
  105. package/src/config/profile-text-generation.test.ts +51 -0
  106. package/src/config/profile-text-generation.ts +51 -0
  107. package/src/config/schemas/__tests__/memory-v3.test.ts +12 -0
  108. package/src/config/schemas/llm.ts +7 -2
  109. package/src/config/schemas/mcp.ts +5 -1
  110. package/src/config/schemas/memory-v3.ts +12 -0
  111. package/src/credential-execution/ces-runtime.ts +2 -2
  112. package/src/credential-execution/executable-discovery.ts +36 -134
  113. package/src/credential-execution/process-manager.test.ts +20 -24
  114. package/src/credential-execution/process-manager.ts +24 -19
  115. package/src/daemon/__tests__/conversation-tool-setup.test.ts +43 -0
  116. package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +36 -0
  117. package/src/daemon/assistant-attachments.ts +20 -12
  118. package/src/daemon/chat-credential-redaction.ts +16 -1
  119. package/src/daemon/conversation-agent-loop-handlers.ts +74 -3
  120. package/src/daemon/conversation-agent-loop.ts +12 -0
  121. package/src/daemon/conversation-attachments.ts +93 -12
  122. package/src/daemon/conversation-tool-setup.ts +71 -3
  123. package/src/daemon/conversation-turn-finalize.ts +26 -13
  124. package/src/daemon/conversation.ts +72 -17
  125. package/src/daemon/daemon-control.ts +3 -7
  126. package/src/daemon/lifecycle.ts +4 -4
  127. package/src/daemon/mcp-reload-service.ts +1 -1
  128. package/src/daemon/message-types/shared.ts +1 -0
  129. package/src/daemon/orphan-reaper.ts +4 -3
  130. package/src/daemon/persist-media-references.ts +20 -3
  131. package/src/daemon/tool-setup-types.ts +6 -0
  132. package/src/daemon/wake-conversation-ops.ts +50 -15
  133. package/src/desktop/desktop-automation-lease.test.ts +230 -0
  134. package/src/desktop/desktop-automation-lease.ts +90 -6
  135. package/src/desktop/desktop-help.ts +15 -0
  136. package/src/desktop/desktop-stream-bridge.test.ts +65 -10
  137. package/src/desktop/desktop-stream-bridge.ts +3 -3
  138. package/src/desktop/desktop-wallpaper-renderer.ts +132 -0
  139. package/src/desktop/desktop-wallpaper-worker.ts +17 -0
  140. package/src/desktop/desktop-wallpaper.test.ts +48 -7
  141. package/src/desktop/desktop-wallpaper.ts +34 -102
  142. package/src/desktop/virtual-desktop-feature.ts +1 -1
  143. package/src/mcp/__tests__/manager-tool-caps.test.ts +111 -0
  144. package/src/mcp/__tests__/startup.test.ts +30 -7
  145. package/src/mcp/__tests__/tool-caps.test.ts +107 -0
  146. package/src/mcp/manager.ts +168 -101
  147. package/src/mcp/startup.ts +33 -13
  148. package/src/mcp/tool-caps.ts +176 -0
  149. package/src/messaging/provider-message-metadata.ts +3 -3
  150. package/src/notifications/__tests__/copy-composer.test.ts +70 -0
  151. package/src/notifications/copy-composer.ts +11 -3
  152. package/src/notifications/schedule-result-producer.ts +4 -6
  153. package/src/oauth/AGENTS.md +2 -0
  154. package/src/oauth/__tests__/identity-verifier.test.ts +36 -1
  155. package/src/oauth/identity-verifier.ts +24 -0
  156. package/src/oauth/oauth-store.ts +11 -1
  157. package/src/oauth/provider-serializer.ts +1 -0
  158. package/src/oauth/seed-providers.ts +12 -0
  159. package/src/permissions/confirmation-guardian-request.test.ts +16 -0
  160. package/src/permissions/confirmation-guardian-request.ts +2 -3
  161. package/src/permissions/question-prompter.test.ts +31 -0
  162. package/src/permissions/question-prompter.ts +2 -0
  163. package/src/persistence/attachments-store.ts +84 -44
  164. package/src/persistence/conversation-crud.ts +111 -20
  165. package/src/persistence/conversation-plugin-facade.ts +30 -0
  166. package/src/persistence/conversation-tool-surface.ts +135 -0
  167. package/src/persistence/conversation-types.test.ts +32 -0
  168. package/src/persistence/conversation-types.ts +26 -4
  169. package/src/persistence/migrations/378-create-conversation-tool-surfaces.test.ts +78 -0
  170. package/src/persistence/migrations/378-create-conversation-tool-surfaces.ts +29 -0
  171. package/src/persistence/migrations/379-oauth-providers-response-ok-field.test.ts +92 -0
  172. package/src/persistence/migrations/379-oauth-providers-response-ok-field.ts +32 -0
  173. package/src/persistence/migrations/380-conversation-tool-surfaces-delegate-independent-tasks.test.ts +90 -0
  174. package/src/persistence/migrations/380-conversation-tool-surfaces-delegate-independent-tasks.ts +34 -0
  175. package/src/persistence/schema/conversation-tool-surfaces.ts +32 -0
  176. package/src/persistence/schema/index.ts +1 -0
  177. package/src/persistence/schema/oauth.ts +1 -0
  178. package/src/persistence/steps.ts +24 -3
  179. package/src/plugin-api/conversation-turn.ts +31 -7
  180. package/src/plugin-api/index.ts +11 -1
  181. package/src/plugin-api/plugin-channel-turn-trust.test.ts +133 -0
  182. package/src/plugin-api/plugin-channel-turn-trust.ts +71 -0
  183. package/src/plugins/defaults/memory/AGENTS.md +39 -5
  184. package/src/plugins/defaults/memory/__tests__/buffer-file.test.ts +365 -0
  185. package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +43 -0
  186. package/src/plugins/defaults/memory/__tests__/conversation-memory-purge.test.ts +1 -0
  187. package/src/plugins/defaults/memory/__tests__/db-memory-attach.test.ts +2 -0
  188. package/src/plugins/defaults/memory/__tests__/fixtures/buffer-appender.ts +17 -0
  189. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +74 -0
  190. package/src/plugins/defaults/memory/__tests__/memory-run-evidence.test.ts +161 -0
  191. package/src/plugins/defaults/memory/__tests__/relocated-memory-test-rows.ts +10 -0
  192. package/src/plugins/defaults/memory/buffer-file.ts +354 -0
  193. package/src/plugins/defaults/memory/buffer-format.ts +40 -0
  194. package/src/plugins/defaults/memory/context-search/agent-runner.ts +1 -2
  195. package/src/plugins/defaults/memory/context-search/format.ts +2 -1
  196. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +2 -1
  197. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +2 -1
  198. package/src/plugins/defaults/memory/conversation-memory-purge.ts +4 -0
  199. package/src/plugins/defaults/memory/graph/capability-seed.ts +1 -2
  200. package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -42
  201. package/src/plugins/defaults/memory/host-utils.ts +0 -10
  202. package/src/plugins/defaults/memory/injectors.ts +4 -3
  203. package/src/plugins/defaults/memory/memory-retrospective-job.ts +72 -183
  204. package/src/plugins/defaults/memory/memory-run-evidence.ts +213 -0
  205. package/src/plugins/defaults/memory/src/memory-item-routes.test.ts +1 -1
  206. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +407 -99
  207. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +10 -0
  208. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +107 -7
  209. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +307 -86
  210. package/src/plugins/defaults/memory/substrate/consolidation-tool-surface.ts +34 -0
  211. package/src/plugins/defaults/memory/substrate/page-index.ts +2 -1
  212. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +89 -49
  213. package/src/plugins/defaults/memory/substrate/sweep-job.ts +1 -1
  214. package/src/plugins/defaults/memory/tools.ts +1 -1
  215. package/src/plugins/defaults/memory/v1/graph/consolidation.ts +2 -2
  216. package/src/plugins/defaults/memory/v1/graph/extraction.ts +2 -1
  217. package/src/plugins/defaults/memory/v1/graph/retriever.ts +1 -1
  218. package/src/plugins/defaults/memory/v2/__tests__/migration.test.ts +5 -0
  219. package/src/plugins/defaults/memory/v2/__tests__/reranker.test.ts +5 -2
  220. package/src/plugins/defaults/memory/v2/reranker.ts +2 -1
  221. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +81 -1
  222. package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +87 -0
  223. package/src/plugins/defaults/memory/v3/__tests__/plugin-schema.test.ts +12 -0
  224. package/src/plugins/defaults/memory/v3/__tests__/pool-log-store.test.ts +183 -4
  225. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +103 -2
  226. package/src/plugins/defaults/memory/v3/card.ts +2 -1
  227. package/src/plugins/defaults/memory/v3/injector.ts +212 -178
  228. package/src/plugins/defaults/memory/v3/orchestrate.ts +112 -25
  229. package/src/plugins/defaults/memory/v3/plugin-schema.ts +54 -4
  230. package/src/plugins/defaults/memory/v3/pool-log-store.ts +253 -1
  231. package/src/plugins/defaults/memory/v3/pool-select.test.ts +35 -2
  232. package/src/plugins/defaults/memory/v3/pool-select.ts +31 -19
  233. package/src/plugins/defaults/memory/v3/sections.ts +2 -1
  234. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +41 -14
  235. package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +4 -1
  236. package/src/plugins/defaults/tool-result-truncate/terminal.ts +1 -46
  237. package/src/prompts/__tests__/parallel-tasks-section.test.ts +25 -0
  238. package/src/prompts/delegation-gate.ts +57 -0
  239. package/src/prompts/system-prompt.ts +14 -32
  240. package/src/providers/inference/adapter-factory.ts +6 -0
  241. package/src/providers/jev/client.test.ts +260 -0
  242. package/src/providers/jev/client.ts +518 -0
  243. package/src/providers/model-catalog.ts +58 -3
  244. package/src/providers/server-tool-pairing.ts +16 -7
  245. package/src/runtime/AGENTS.md +2 -2
  246. package/src/runtime/__tests__/agent-wake.test.ts +94 -1
  247. package/src/runtime/agent-wake.ts +31 -4
  248. package/src/runtime/guardian-action-service.ts +2 -17
  249. package/src/runtime/guardian-reply-router.ts +1 -8
  250. package/src/runtime/http-server.ts +2 -2
  251. package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +89 -0
  252. package/src/runtime/migrations/vbundle-import-policy.ts +33 -11
  253. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +24 -2
  254. package/src/runtime/routes/channel-route-shared.ts +1 -9
  255. package/src/runtime/routes/conversation-routes.ts +29 -4
  256. package/src/runtime/routes/desktop-setup-routes.test.ts +2 -2
  257. package/src/runtime/routes/desktop-setup-routes.ts +8 -5
  258. package/src/runtime/routes/guardian-approval-interception.ts +24 -0
  259. package/src/runtime/routes/inbound-message-handler.ts +2 -3
  260. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +1 -1
  261. package/src/runtime/routes/inbound-stages/background-dispatch.ts +7 -4
  262. package/src/runtime/routes/inference-profile-session-handler.ts +11 -1
  263. package/src/runtime/routes/inference-profiles-routes.ts +9 -0
  264. package/src/runtime/routes/oauth-commands-routes.ts +57 -54
  265. package/src/runtime/routes/oauth-providers.ts +4 -0
  266. package/src/runtime/routes/oauth-request-hints.test.ts +246 -0
  267. package/src/runtime/routes/oauth-request-hints.ts +149 -0
  268. package/src/runtime/routes/schedule-routes.ts +2 -0
  269. package/src/runtime/routes/secret-routes.ts +99 -119
  270. package/src/schedule/__tests__/worker-mcp-bootstrap.test.ts +107 -0
  271. package/src/schedule/__tests__/worker-mcp-readiness.test.ts +173 -0
  272. package/src/schedule/__tests__/worker-mcp-tools.test.ts +13 -47
  273. package/src/schedule/run-script.ts +2 -2
  274. package/src/schedule/scheduler.ts +47 -11
  275. package/src/schedule/tool-surface-readiness.ts +65 -0
  276. package/src/schedule/worker-mcp.ts +85 -0
  277. package/src/schedule/worker.ts +9 -31
  278. package/src/security/secure-keys.ts +60 -48
  279. package/src/tools/ask-question/ask-question-tool.test.ts +167 -5
  280. package/src/tools/ask-question/ask-question-tool.ts +119 -23
  281. package/src/tools/browser/browser-execution.ts +7 -1
  282. package/src/tools/host-terminal/host-shell.ts +12 -6
  283. package/src/tools/shared/filesystem/file-ops-service.ts +1 -31
  284. package/src/tools/shared/shell-output.test.ts +10 -0
  285. package/src/tools/shared/shell-output.ts +14 -2
  286. package/src/tools/skills/sandbox-runner.ts +13 -2
  287. package/src/tools/skills/scaffold-managed.ts +2 -1
  288. package/src/tools/terminal/__tests__/safe-env.test.ts +33 -4
  289. package/src/tools/terminal/__tests__/sanitized-bash.test.ts +0 -14
  290. package/src/tools/terminal/safe-env.ts +35 -24
  291. package/src/tools/terminal/sanitized-bash.ts +15 -2
  292. package/src/tools/terminal/shell-launch.test.ts +162 -0
  293. package/src/tools/terminal/shell.test.ts +29 -0
  294. package/src/tools/terminal/shell.ts +13 -7
  295. package/src/util/browser-human-verification.ts +14 -0
  296. package/src/util/host-process.test.ts +17 -1
  297. package/src/util/host-process.ts +24 -0
  298. package/src/util/unicode.ts +29 -0
@@ -0,0 +1,78 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { describe, expect, test } from "bun:test";
3
+
4
+ import { drizzle } from "drizzle-orm/bun-sqlite";
5
+
6
+ import * as schema from "../schema.js";
7
+ import { migrateCreateConversationToolSurfaces } from "./378-create-conversation-tool-surfaces.js";
8
+
9
+ function createTestDb() {
10
+ const sqlite = new Database(":memory:");
11
+ sqlite.run("PRAGMA foreign_keys = ON");
12
+ // Only the parent table the FK points at.
13
+ sqlite.run(/*sql*/ `CREATE TABLE conversations (id TEXT PRIMARY KEY)`);
14
+ return { sqlite, db: drizzle(sqlite, { schema }) };
15
+ }
16
+
17
+ function tableDdl(sqlite: Database): string | undefined {
18
+ const row = sqlite
19
+ .query(
20
+ `SELECT sql FROM sqlite_master WHERE type='table' AND name='conversation_tool_surfaces'`,
21
+ )
22
+ .get() as { sql: string } | null;
23
+ return row?.sql;
24
+ }
25
+
26
+ function insertSurface(sqlite: Database, conversationId: string): void {
27
+ sqlite
28
+ .query(
29
+ /*sql*/ `INSERT INTO conversation_tool_surfaces (conversation_id, tools_json, tools_hash, updated_at) VALUES (?, ?, ?, ?)`,
30
+ )
31
+ .run(conversationId, "[]", "hash", 1);
32
+ }
33
+
34
+ function surfaceCount(sqlite: Database): number {
35
+ const row = sqlite
36
+ .query(`SELECT COUNT(*) AS n FROM conversation_tool_surfaces`)
37
+ .get() as { n: number };
38
+ return row.n;
39
+ }
40
+
41
+ describe("migration 378: conversation_tool_surfaces", () => {
42
+ test("creates the table keyed by conversation with a cascading FK", () => {
43
+ const { sqlite, db } = createTestDb();
44
+ expect(tableDdl(sqlite)).toBeUndefined();
45
+
46
+ migrateCreateConversationToolSurfaces(db);
47
+
48
+ const ddl = tableDdl(sqlite);
49
+ expect(ddl).toContain("conversation_id TEXT PRIMARY KEY");
50
+ expect(ddl).toContain("REFERENCES conversations(id) ON DELETE CASCADE");
51
+ expect(ddl).toContain("tools_json TEXT NOT NULL");
52
+ expect(ddl).toContain("tools_hash TEXT NOT NULL");
53
+ expect(ddl).toContain("updated_at INTEGER NOT NULL");
54
+ });
55
+
56
+ test("deleting a conversation cascades to its surface row", () => {
57
+ const { sqlite, db } = createTestDb();
58
+ migrateCreateConversationToolSurfaces(db);
59
+ sqlite.query(`INSERT INTO conversations (id) VALUES (?)`).run("conv-1");
60
+ insertSurface(sqlite, "conv-1");
61
+ expect(surfaceCount(sqlite)).toBe(1);
62
+
63
+ sqlite.query(`DELETE FROM conversations WHERE id = ?`).run("conv-1");
64
+
65
+ expect(surfaceCount(sqlite)).toBe(0);
66
+ });
67
+
68
+ test("is idempotent: a second run keeps the table and its rows", () => {
69
+ const { sqlite, db } = createTestDb();
70
+ migrateCreateConversationToolSurfaces(db);
71
+ sqlite.query(`INSERT INTO conversations (id) VALUES (?)`).run("conv-1");
72
+ insertSurface(sqlite, "conv-1");
73
+
74
+ expect(() => migrateCreateConversationToolSurfaces(db)).not.toThrow();
75
+
76
+ expect(surfaceCount(sqlite)).toBe(1);
77
+ });
78
+ });
@@ -0,0 +1,29 @@
1
+ import type { DrizzleDb } from "../db-connection.js";
2
+
3
+ const TABLE = "conversation_tool_surfaces";
4
+
5
+ /**
6
+ * Create `conversation_tool_surfaces`: one row per conversation holding the
7
+ * tool definitions its most recent live turn sent to the provider, keyed by
8
+ * conversation with a content hash so an unchanged surface is never rewritten.
9
+ *
10
+ * Its own table rather than a column on `conversations` because the payload
11
+ * is a JSON array of every tool definition (tens of kilobytes) that only fork
12
+ * wakes read, while `conversations` rows are read whole on every hot path.
13
+ * The row cascades with its conversation, so a deleted conversation leaves
14
+ * no surface behind.
15
+ *
16
+ * Idempotent via IF NOT EXISTS.
17
+ */
18
+ export function migrateCreateConversationToolSurfaces(
19
+ database: DrizzleDb,
20
+ ): void {
21
+ database.run(
22
+ `CREATE TABLE IF NOT EXISTS ${TABLE} (
23
+ conversation_id TEXT PRIMARY KEY REFERENCES conversations(id) ON DELETE CASCADE,
24
+ tools_json TEXT NOT NULL,
25
+ tools_hash TEXT NOT NULL,
26
+ updated_at INTEGER NOT NULL
27
+ )`,
28
+ );
29
+ }
@@ -0,0 +1,92 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { describe, expect, test } from "bun:test";
3
+
4
+ import { drizzle } from "drizzle-orm/bun-sqlite";
5
+
6
+ import * as schema from "../schema.js";
7
+ import { migrateOAuthProvidersResponseOkField } from "./379-oauth-providers-response-ok-field.js";
8
+
9
+ function createTestDb(options: { withTable?: boolean } = {}) {
10
+ const sqlite = new Database(":memory:");
11
+ if (options.withTable !== false) {
12
+ // Pre-379 shape: only the columns the migration and tests touch.
13
+ sqlite.exec(/*sql*/ `
14
+ CREATE TABLE oauth_providers (
15
+ provider_key TEXT PRIMARY KEY,
16
+ identity_ok_field TEXT,
17
+ created_at INTEGER NOT NULL,
18
+ updated_at INTEGER NOT NULL
19
+ );
20
+ `);
21
+ }
22
+ return { sqlite, db: drizzle(sqlite, { schema }) };
23
+ }
24
+
25
+ function columnNames(sqlite: Database): string[] {
26
+ return (
27
+ sqlite.query("PRAGMA table_info(oauth_providers)").all() as Array<{
28
+ name: string;
29
+ }>
30
+ ).map((c) => c.name);
31
+ }
32
+
33
+ describe("migration 379: oauth_providers.response_ok_field", () => {
34
+ test("adds the nullable column beside the identity one", () => {
35
+ const { sqlite, db } = createTestDb();
36
+ expect(columnNames(sqlite)).not.toContain("response_ok_field");
37
+
38
+ migrateOAuthProvidersResponseOkField(db);
39
+
40
+ const column = (
41
+ sqlite.query("PRAGMA table_info(oauth_providers)").all() as Array<{
42
+ name: string;
43
+ notnull: number;
44
+ }>
45
+ ).find((c) => c.name === "response_ok_field");
46
+ expect(column).toBeDefined();
47
+ expect(column?.notnull).toBe(0);
48
+ expect(columnNames(sqlite)).toContain("identity_ok_field");
49
+ });
50
+
51
+ test("rows written before the column existed read back null", () => {
52
+ // A provider that declared nothing is judged by status alone, which is
53
+ // what it was before the column existed.
54
+ const { sqlite, db } = createTestDb();
55
+ sqlite.exec(/*sql*/ `
56
+ INSERT INTO oauth_providers (provider_key, identity_ok_field, created_at, updated_at)
57
+ VALUES ('slack', 'ok', 1000, 1000)
58
+ `);
59
+
60
+ migrateOAuthProvidersResponseOkField(db);
61
+
62
+ const row = sqlite
63
+ .query(
64
+ "SELECT response_ok_field FROM oauth_providers WHERE provider_key = 'slack'",
65
+ )
66
+ .get() as { response_ok_field: unknown };
67
+ expect(row.response_ok_field).toBeNull();
68
+ });
69
+
70
+ test("is idempotent: re-run is a no-op", () => {
71
+ const { sqlite, db } = createTestDb();
72
+
73
+ migrateOAuthProvidersResponseOkField(db);
74
+ expect(() => migrateOAuthProvidersResponseOkField(db)).not.toThrow();
75
+
76
+ expect(
77
+ columnNames(sqlite).filter((n) => n === "response_ok_field"),
78
+ ).toHaveLength(1);
79
+ });
80
+
81
+ test("a missing table is a failure, never a silent success", () => {
82
+ // The runner checkpoints a throwing step as failed and retries it, and
83
+ // the registry defers this step behind the table's own. Swallowing the
84
+ // error here would checkpoint the step as done against no table, and the
85
+ // column would never be added.
86
+ // The driver wraps SQLite's "no such table" in its own message, so the
87
+ // assertion is that the step throws at all.
88
+ const { db } = createTestDb({ withTable: false });
89
+
90
+ expect(() => migrateOAuthProvidersResponseOkField(db)).toThrow();
91
+ });
92
+ });
@@ -0,0 +1,32 @@
1
+ import type { DrizzleDb } from "../db-connection.js";
2
+ import { tableHasColumn } from "./schema-introspection.js";
3
+
4
+ const TABLE_NAME = "oauth_providers";
5
+ const COLUMN_NAME = "response_ok_field";
6
+ const COLUMN_DEFINITION = "response_ok_field TEXT";
7
+
8
+ /**
9
+ * Add `response_ok_field` to `oauth_providers` (migration 149): the dot path
10
+ * of the boolean a provider's API puts in every response body to report
11
+ * success (Slack: `ok`). Its own column, distinct from `identity_ok_field`,
12
+ * because the two declare different facts: the identity field governs the
13
+ * one call the provider row names, this one governs any request made with
14
+ * the credential.
15
+ *
16
+ * Nullable with no backfill: seeding upserts the value for the built-in rows
17
+ * on the next start, and a custom provider that declares nothing is judged by
18
+ * status alone, which is what it was before the column existed.
19
+ *
20
+ * Idempotent: guarded with `tableHasColumn` so a crash between the `ALTER
21
+ * TABLE` and the checkpoint write doesn't cause a duplicate-column error on
22
+ * the next boot. A missing table is not swallowed: the registry declares the
23
+ * dependency on the table's own step, so this one is deferred rather than
24
+ * checkpointed as done against no table.
25
+ */
26
+ export function migrateOAuthProvidersResponseOkField(
27
+ database: DrizzleDb,
28
+ ): void {
29
+ if (!tableHasColumn(database, TABLE_NAME, COLUMN_NAME)) {
30
+ database.run(`ALTER TABLE ${TABLE_NAME} ADD COLUMN ${COLUMN_DEFINITION}`);
31
+ }
32
+ }
@@ -0,0 +1,90 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { describe, expect, test } from "bun:test";
3
+
4
+ import { drizzle } from "drizzle-orm/bun-sqlite";
5
+
6
+ import * as schema from "../schema.js";
7
+ import { migrateCreateConversationToolSurfaces } from "./378-create-conversation-tool-surfaces.js";
8
+ import { migrateConversationToolSurfacesDelegateIndependentTasks } from "./380-conversation-tool-surfaces-delegate-independent-tasks.js";
9
+
10
+ function createTestDb(options: { withTable?: boolean } = {}) {
11
+ const sqlite = new Database(":memory:");
12
+ sqlite.run("PRAGMA foreign_keys = ON");
13
+ // Only the parent table the FK points at.
14
+ sqlite.run(/*sql*/ `CREATE TABLE conversations (id TEXT PRIMARY KEY)`);
15
+ const db = drizzle(sqlite, { schema });
16
+ if (options.withTable !== false) {
17
+ // Pre-380 shape: the table as migration 378 creates it.
18
+ migrateCreateConversationToolSurfaces(db);
19
+ }
20
+ return { sqlite, db };
21
+ }
22
+
23
+ function columns(sqlite: Database): Array<{ name: string; notnull: number }> {
24
+ return sqlite
25
+ .query("PRAGMA table_info(conversation_tool_surfaces)")
26
+ .all() as Array<{ name: string; notnull: number }>;
27
+ }
28
+
29
+ describe("migration 380: conversation_tool_surfaces.delegate_independent_tasks", () => {
30
+ test("adds the nullable column", () => {
31
+ const { sqlite, db } = createTestDb();
32
+ expect(columns(sqlite).map((c) => c.name)).not.toContain(
33
+ "delegate_independent_tasks",
34
+ );
35
+
36
+ migrateConversationToolSurfacesDelegateIndependentTasks(db);
37
+
38
+ const column = columns(sqlite).find(
39
+ (c) => c.name === "delegate_independent_tasks",
40
+ );
41
+ expect(column).toBeDefined();
42
+ expect(column?.notnull).toBe(0);
43
+ });
44
+
45
+ test("rows written before the column existed read back null", () => {
46
+ // A fork replaying such a row derives the section for itself, which is
47
+ // what it did before the column existed.
48
+ const { sqlite, db } = createTestDb();
49
+ sqlite.query(`INSERT INTO conversations (id) VALUES (?)`).run("conv-1");
50
+ sqlite
51
+ .query(
52
+ /*sql*/ `INSERT INTO conversation_tool_surfaces (conversation_id, tools_json, tools_hash, updated_at) VALUES (?, ?, ?, ?)`,
53
+ )
54
+ .run("conv-1", "[]", "hash", 1);
55
+
56
+ migrateConversationToolSurfacesDelegateIndependentTasks(db);
57
+
58
+ const row = sqlite
59
+ .query(
60
+ "SELECT delegate_independent_tasks FROM conversation_tool_surfaces WHERE conversation_id = 'conv-1'",
61
+ )
62
+ .get() as { delegate_independent_tasks: unknown };
63
+ expect(row.delegate_independent_tasks).toBeNull();
64
+ });
65
+
66
+ test("is idempotent: re-run is a no-op", () => {
67
+ const { sqlite, db } = createTestDb();
68
+
69
+ migrateConversationToolSurfacesDelegateIndependentTasks(db);
70
+ expect(() =>
71
+ migrateConversationToolSurfacesDelegateIndependentTasks(db),
72
+ ).not.toThrow();
73
+
74
+ expect(
75
+ columns(sqlite).filter((c) => c.name === "delegate_independent_tasks"),
76
+ ).toHaveLength(1);
77
+ });
78
+
79
+ test("a missing table is a failure, never a silent success", () => {
80
+ // The runner checkpoints a throwing step as failed and retries it, and
81
+ // the registry defers this step behind the table's own. Swallowing the
82
+ // error here would checkpoint the step as done against no table, and the
83
+ // column would never be added.
84
+ const { db } = createTestDb({ withTable: false });
85
+
86
+ expect(() =>
87
+ migrateConversationToolSurfacesDelegateIndependentTasks(db),
88
+ ).toThrow();
89
+ });
90
+ });
@@ -0,0 +1,34 @@
1
+ import type { DrizzleDb } from "../db-connection.js";
2
+ import { tableHasColumn } from "./schema-introspection.js";
3
+
4
+ const TABLE_NAME = "conversation_tool_surfaces";
5
+ const COLUMN_NAME = "delegate_independent_tasks";
6
+ const COLUMN_DEFINITION = "delegate_independent_tasks INTEGER";
7
+
8
+ /**
9
+ * Add `delegate_independent_tasks` to `conversation_tool_surfaces`: whether
10
+ * the system prompt of the turn that recorded the surface rendered the
11
+ * parallel-delegation section (`01-parallel-tasks`). The prompt derives that
12
+ * section from the same resolved tool surface, so a fork wake replaying the
13
+ * surface needs the rendered state too: the section sits in the system
14
+ * prompt, the second tier of the provider cache prefix, and a fork that
15
+ * derives it from its own restricted scope renders it off where the source
16
+ * rendered it on, missing the source's cached prefix from that byte on.
17
+ *
18
+ * Nullable with no backfill: a row written before the column existed reads
19
+ * null and the fork derives the section for itself, exactly as it did before
20
+ * the column; the next live turn records the state.
21
+ *
22
+ * Idempotent: guarded with `tableHasColumn` so a crash between the `ALTER
23
+ * TABLE` and the checkpoint write doesn't cause a duplicate-column error on
24
+ * the next boot. A missing table is not swallowed: the registry declares the
25
+ * dependency on the table's own step, so this one is deferred rather than
26
+ * checkpointed as done against no table.
27
+ */
28
+ export function migrateConversationToolSurfacesDelegateIndependentTasks(
29
+ database: DrizzleDb,
30
+ ): void {
31
+ if (!tableHasColumn(database, TABLE_NAME, COLUMN_NAME)) {
32
+ database.run(`ALTER TABLE ${TABLE_NAME} ADD COLUMN ${COLUMN_DEFINITION}`);
33
+ }
34
+ }
@@ -0,0 +1,32 @@
1
+ import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
2
+
3
+ import { conversations } from "./conversations.js";
4
+
5
+ /**
6
+ * The tool definitions a conversation's most recent live turn sent to the
7
+ * provider, serialized exactly as resolved, plus whether that turn's system
8
+ * prompt rendered the parallel-delegation section (which the prompt derives
9
+ * from the same resolved surface and the turn's channel). Written at the
10
+ * agent loop's send boundary when either changes; read by fork wakes that
11
+ * replay both so their provider prompt-cache prefix matches the source's live
12
+ * turns. Cascades with its conversation.
13
+ */
14
+ export const conversationToolSurfaces = sqliteTable(
15
+ "conversation_tool_surfaces",
16
+ {
17
+ conversationId: text("conversation_id")
18
+ .primaryKey()
19
+ .references(() => conversations.id, { onDelete: "cascade" }),
20
+ toolsJson: text("tools_json").notNull(),
21
+ toolsHash: text("tools_hash").notNull(),
22
+ /**
23
+ * Whether the turn's system prompt rendered the `01-parallel-tasks`
24
+ * section. Null when unknown: the turn ran on a system-prompt override,
25
+ * or the row predates the column.
26
+ */
27
+ delegateIndependentTasks: integer("delegate_independent_tasks", {
28
+ mode: "boolean",
29
+ }),
30
+ updatedAt: integer("updated_at").notNull(),
31
+ },
32
+ );
@@ -5,6 +5,7 @@ export * from "./calls.js";
5
5
  export * from "./contacts.js";
6
6
  export * from "./conversation-groups.js";
7
7
  export * from "./conversation-starters.js";
8
+ export * from "./conversation-tool-surfaces.js";
8
9
  export * from "./conversations.js";
9
10
  export * from "./documents.js";
10
11
  export * from "./guardian.js";
@@ -50,6 +50,7 @@ export const oauthProviders = sqliteTable("oauth_providers", {
50
50
  identityResponsePaths: text("identity_response_paths"),
51
51
  identityFormat: text("identity_format"),
52
52
  identityOkField: text("identity_ok_field"),
53
+ responseOkField: text("response_ok_field"),
53
54
  featureFlag: text("feature_flag"),
54
55
  createdAt: integer("created_at").notNull(),
55
56
  updatedAt: integer("updated_at").notNull(),
@@ -485,6 +485,9 @@ import { migrateChannelInboundMessageIdIndex } from "./migrations/374-channel-in
485
485
  import { migrateCreateChannelOutboundPosts } from "./migrations/375-create-channel-outbound-posts.js";
486
486
  import { migrateNotificationDeliveriesCanonicalMessageId } from "./migrations/376-notification-deliveries-canonical-message-id.js";
487
487
  import { migrateAddSubagentBudgetStopReason } from "./migrations/377-add-subagent-budget-stop-reason.js";
488
+ import { migrateCreateConversationToolSurfaces } from "./migrations/378-create-conversation-tool-surfaces.js";
489
+ import { migrateOAuthProvidersResponseOkField } from "./migrations/379-oauth-providers-response-ok-field.js";
490
+ import { migrateConversationToolSurfacesDelegateIndependentTasks } from "./migrations/380-conversation-tool-surfaces-delegate-independent-tasks.js";
488
491
  import type { MigrationStep } from "./migrations/run-migrations.js";
489
492
 
490
493
  export const migrationSteps: MigrationStep[] = [
@@ -1606,9 +1609,27 @@ export const migrationSteps: MigrationStep[] = [
1606
1609
  {
1607
1610
  name: "migrateAddSubagentBudgetStopReason",
1608
1611
  run: migrateAddSubagentBudgetStopReason,
1609
- // The column guard treats a missing table as nothing-to-do, so the table
1610
- // must be checkpointed first or a repair flow could permanently checkpoint
1611
- // this as a no-op.
1612
+ // The column guard reads the table's columns and the ALTER throws on a
1613
+ // missing table, so the table must be checkpointed first or a repair flow
1614
+ // that failed to create it would fail this step needlessly.
1612
1615
  dependsOn: ["migrateCreateSubagentsTable"],
1613
1616
  },
1617
+ migrateCreateConversationToolSurfaces,
1618
+ {
1619
+ name: "migrateOAuthProvidersResponseOkField",
1620
+ run: migrateOAuthProvidersResponseOkField,
1621
+ // The column guard reads the table's columns, and an `ALTER` on a missing
1622
+ // table throws, so the table must be checkpointed first or a repair flow
1623
+ // that failed to create it would either fail this step needlessly or, if
1624
+ // the error were swallowed, checkpoint it as done against no table.
1625
+ dependsOn: ["createOAuthTables"],
1626
+ },
1627
+ {
1628
+ name: "migrateConversationToolSurfacesDelegateIndependentTasks",
1629
+ run: migrateConversationToolSurfacesDelegateIndependentTasks,
1630
+ // Same column-guard shape as the step above: the guard reads the table's
1631
+ // columns and the `ALTER` throws on a missing table, so the table's own
1632
+ // step must be checkpointed first.
1633
+ dependsOn: ["migrateCreateConversationToolSurfaces"],
1634
+ },
1614
1635
  ];
@@ -284,13 +284,20 @@ export async function runConversationTurn(
284
284
  const { resolveChannelCapabilities } =
285
285
  await import("../daemon/conversation-runtime-assembly.js");
286
286
 
287
- // Plugin-driven turns run as the guardian: plugins are installed by the
288
- // guardian, so their conversations inherit guardian trust. This lets the
289
- // existing non-interactive auto-approve machinery handle tool permissions
290
- // (the conversation is already non-interactive via `isInteractive: false`
291
- // below) without requiring a client to approve prompts.
287
+ // Channel-addressed turns are inbound: the gateway classifies the sender
288
+ // and the turn runs only if that actor clears the channel admission floor.
289
+ // Internal plugin jobs (no channel address) use guardian trust because they
290
+ // are assistant-owned work, not an external sender.
292
291
  const { INTERNAL_GUARDIAN_TRUST_CONTEXT } =
293
292
  await import("../daemon/trust-context.js");
293
+ const { resolvePluginChannelTurnTrust } =
294
+ await import("./plugin-channel-turn-trust.js");
295
+ const { prepareChannelInboundContent } =
296
+ await import("../runtime/routes/inbound-stages/inbound-content-prep.js");
297
+
298
+ const trustContext = options.channel
299
+ ? await resolvePluginChannelTurnTrust(options.channel)
300
+ : INTERNAL_GUARDIAN_TRUST_CONTEXT;
294
301
 
295
302
  // A channel address resolves through the same binding an inbound message
296
303
  // uses, so a turn addressed by chat lands in that chat's conversation
@@ -311,7 +318,7 @@ export async function runConversationTurn(
311
318
  ? !channelConversation.created
312
319
  : getConversation(conversationId) != null;
313
320
  const conversation = await getOrCreateConversation(conversationId, {
314
- trustContext: INTERNAL_GUARDIAN_TRUST_CONTEXT,
321
+ trustContext,
315
322
  ...(options.conversationType
316
323
  ? { conversationType: options.conversationType }
317
324
  : {}),
@@ -336,10 +343,23 @@ export async function runConversationTurn(
336
343
 
337
344
  // Convert ContentBlock[] input to the text + attachments shape the
338
345
  // conversation's processMessage path expects.
339
- const { text, attachments } = extractContentAndAttachments(
346
+ const { text: rawText, attachments } = extractContentAndAttachments(
340
347
  options.content,
341
348
  resolveMediaSourceData,
342
349
  );
350
+ const inboundContent = options.channel
351
+ ? prepareChannelInboundContent({
352
+ trimmedContent: rawText,
353
+ trustClass: trustContext.trustClass,
354
+ sourceChannel: options.channel.sourceChannel,
355
+ requesterIdentifier:
356
+ options.channel.username ??
357
+ options.channel.externalUserId ??
358
+ undefined,
359
+ })
360
+ : { content: rawText, displayContent: undefined };
361
+ const text = inboundContent.content;
362
+ const displayContent = inboundContent.displayContent;
343
363
 
344
364
  // The channel this turn speaks on. Runtime assembly reads the per-turn
345
365
  // context first and falls back to the conversation's `originChannel`, then
@@ -409,6 +429,8 @@ export async function runConversationTurn(
409
429
  requestId,
410
430
  isInteractive: false,
411
431
  metadata,
432
+ trustContext,
433
+ ...(displayContent ? { displayContent } : {}),
412
434
  });
413
435
  if (enqueueResult.rejected) {
414
436
  throw new Error(
@@ -432,6 +454,8 @@ export async function runConversationTurn(
432
454
  onEvent,
433
455
  isInteractive: false,
434
456
  metadata,
457
+ trustContext,
458
+ ...(displayContent ? { displayContent } : {}),
435
459
  ...(options.callSite ? { callSite: options.callSite } : {}),
436
460
  });
437
461
 
@@ -286,6 +286,11 @@ export {
286
286
  // writes files under the workspace (e.g. its own `plugins/<name>/data/`
287
287
  // directory) resolves them against this instead of hardcoding a base path.
288
288
  export { getWorkspaceDir } from "../util/platform.js";
289
+ // `String.prototype.slice` that never cuts a UTF-16 surrogate pair in half.
290
+ // Any text a plugin truncates by character budget and hands to a model must
291
+ // go through this: an orphaned half is invalid UTF-16 that strict provider
292
+ // parsers reject.
293
+ export { safeStringSlice } from "../util/unicode.js";
289
294
  // Declarative help for the top-level `assistant` CLI commands that have adopted
290
295
  // the static-help split. Plugins (e.g. the memory capability indexer) read this
291
296
  // to embed CLI command capabilities without importing the CLI action graph.
@@ -359,7 +364,8 @@ export {
359
364
  stringifyMessageContent,
360
365
  } from "../persistence/message-content.js";
361
366
  // Conversation history — reads and writes on the host conversation store
362
- // (rows, message history, processing state, disk-view paths) plus the lexical
367
+ // (rows, message history, processing state, the recorded wire tool surface,
368
+ // disk-view paths) plus the lexical
363
369
  // message-search surface. Every operation takes explicit parameters; nothing
364
370
  // is resolved from config. Async because the facade loads the DB store graph
365
371
  // lazily on first call.
@@ -371,7 +377,9 @@ export {
371
377
  getConversation,
372
378
  getConversationDirPath,
373
379
  getConversationProcessingStartedAt,
380
+ getConversationToolSurface,
374
381
  getMessages,
382
+ getRecordedConversationToolSurface,
375
383
  hasLexicalTokens,
376
384
  isConversationProcessing,
377
385
  listConversations,
@@ -380,6 +388,7 @@ export {
380
388
  syncMessageToDisk,
381
389
  updateMessageMetadata,
382
390
  } from "../persistence/conversation-plugin-facade.js";
391
+ export type { ConversationToolSurface } from "../persistence/conversation-tool-surface.js";
383
392
  // System cards: a transcript notice authored by the daemon rather than the
384
393
  // assistant persona, for telling the user something a turn did to their input
385
394
  // that the model's reply cannot explain (e.g. an attachment that could not be
@@ -438,6 +447,7 @@ export type {
438
447
  RunConversationTurnResult,
439
448
  } from "./conversation-turn.js";
440
449
  export { runConversationTurn } from "./conversation-turn.js";
450
+ export { PluginTurnNotAdmittedError } from "./plugin-channel-turn-trust.js";
441
451
  // Live voice — drive a single client's real-time voice session (STT → agent
442
452
  // turn → TTS, with server-VAD turn-taking, pauses, and barge-in) over a
443
453
  // transport the plugin owns. The plugin brings only a `send` callback (e.g.