@vellumai/assistant 0.10.2 → 0.10.3-dev.202606252046.9075fd5

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 (546) hide show
  1. package/bun.lock +20 -0
  2. package/docs/workspace-tools.md +42 -33
  3. package/eslint-rules/cli-no-daemon-internals.js +6 -0
  4. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-delivery-contract.test.ts +91 -0
  5. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +31 -0
  6. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +44 -0
  7. package/node_modules/@vellumai/gateway-client/src/guardian-delivery-contract.ts +48 -0
  8. package/node_modules/@vellumai/gateway-client/src/index.ts +14 -0
  9. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +17 -0
  10. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  11. package/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +28 -0
  12. package/node_modules/@vellumai/service-contracts/src/channels.ts +41 -0
  13. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  14. package/openapi.yaml +196 -56
  15. package/package.json +4 -1
  16. package/scripts/test.sh +36 -15
  17. package/src/__tests__/actor-token-service.test.ts +36 -14
  18. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +83 -31
  19. package/src/__tests__/agent-loop-override-profile.test.ts +1 -0
  20. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  21. package/src/__tests__/agent-wake-override-profile.test.ts +2 -0
  22. package/src/__tests__/annotate-activity-metadata.test.ts +2 -0
  23. package/src/__tests__/annotate-risk-options.test.ts +2 -0
  24. package/src/__tests__/approval-cascade.test.ts +2 -0
  25. package/src/__tests__/assistant-attachments.test.ts +42 -0
  26. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -0
  27. package/src/__tests__/btw-routes.test.ts +2 -0
  28. package/src/__tests__/build-persisted-content.test.ts +2 -0
  29. package/src/__tests__/call-controller.test.ts +19 -0
  30. package/src/__tests__/channel-approval-routes.test.ts +21 -26
  31. package/src/__tests__/channel-delivery-store.test.ts +28 -0
  32. package/src/__tests__/channel-guardian.test.ts +164 -78
  33. package/src/__tests__/channel-inbound-disk-pressure.test.ts +11 -19
  34. package/src/__tests__/channel-reply-delivery.test.ts +2 -0
  35. package/src/__tests__/code-search-tool.test.ts +585 -0
  36. package/src/__tests__/compaction-events.test.ts +2 -0
  37. package/src/__tests__/compaction-ledger-store.test.ts +128 -0
  38. package/src/__tests__/compaction.benchmark.test.ts +2 -0
  39. package/src/__tests__/compactor-call-site-logging.test.ts +2 -0
  40. package/src/__tests__/compactor-low-watermark-cut.test.ts +2 -0
  41. package/src/__tests__/compactor-preserved-tail-count.test.ts +2 -0
  42. package/src/__tests__/compactor-summary-call-truncation.test.ts +2 -0
  43. package/src/__tests__/compactor-web-search-strip.test.ts +2 -0
  44. package/src/__tests__/config-loader-backfill.test.ts +123 -10
  45. package/src/__tests__/config-schema.test.ts +1 -0
  46. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +31 -29
  47. package/src/__tests__/consult-deadline.test.ts +60 -0
  48. package/src/__tests__/contact-store-interaction-info.test.ts +156 -0
  49. package/src/__tests__/contact-store-user-file.test.ts +7 -10
  50. package/src/__tests__/contacts-relay-reads.test.ts +19 -24
  51. package/src/__tests__/contacts-write.test.ts +0 -2
  52. package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
  53. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +2 -0
  54. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
  55. package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
  56. package/src/__tests__/conversation-agent-loop.test.ts +134 -0
  57. package/src/__tests__/conversation-analysis-routes.test.ts +2 -0
  58. package/src/__tests__/conversation-app-control-lifecycle.test.ts +2 -0
  59. package/src/__tests__/conversation-attention-telegram.test.ts +9 -11
  60. package/src/__tests__/conversation-confirmation-signals.test.ts +2 -0
  61. package/src/__tests__/conversation-fork-crud.test.ts +354 -24
  62. package/src/__tests__/conversation-history-web-search.test.ts +2 -0
  63. package/src/__tests__/conversation-load-history-repair.test.ts +2 -0
  64. package/src/__tests__/conversation-load-history-stripped.test.ts +2 -0
  65. package/src/__tests__/conversation-pairing.test.ts +2 -0
  66. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +2 -0
  67. package/src/__tests__/conversation-process-callsite.test.ts +2 -0
  68. package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
  69. package/src/__tests__/conversation-queue.test.ts +91 -0
  70. package/src/__tests__/conversation-routes-guardian-reply.test.ts +14 -0
  71. package/src/__tests__/conversation-routes-slash-commands.test.ts +14 -0
  72. package/src/__tests__/conversation-slash-queue.test.ts +2 -0
  73. package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
  74. package/src/__tests__/conversation-speed-override.test.ts +2 -0
  75. package/src/__tests__/conversation-surfaces-task-progress.test.ts +29 -0
  76. package/src/__tests__/conversation-title-service.test.ts +2 -0
  77. package/src/__tests__/conversation-tool-setup-attribution.test.ts +47 -0
  78. package/src/__tests__/conversation-usage.test.ts +2 -0
  79. package/src/__tests__/conversation-workspace-cache-state.test.ts +2 -0
  80. package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
  81. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
  82. package/src/__tests__/credential-security-invariants.test.ts +1 -1
  83. package/src/__tests__/db-compaction-events-migration.test.ts +129 -0
  84. package/src/__tests__/db-migration-rollback.test.ts +205 -171
  85. package/src/__tests__/db-test-helpers.ts +5 -4
  86. package/src/__tests__/delete-propagation.test.ts +5 -3
  87. package/src/__tests__/deterministic-verification-control-plane.test.ts +4 -2
  88. package/src/__tests__/disk-pressure-guard.test.ts +41 -0
  89. package/src/__tests__/dm-backfill.test.ts +6 -4
  90. package/src/__tests__/dm-persistence.test.ts +2 -0
  91. package/src/__tests__/emit-signal-routing-intent.test.ts +7 -6
  92. package/src/__tests__/events-dev-bypass-actor.test.ts +7 -1
  93. package/src/__tests__/exploration-drift-hook.test.ts +3 -2
  94. package/src/__tests__/filing-service.test.ts +2 -0
  95. package/src/__tests__/guardian-binding-drift-heal.test.ts +104 -19
  96. package/src/__tests__/guardian-dispatch.test.ts +140 -1
  97. package/src/__tests__/guardian-expiry-notifier.test.ts +282 -0
  98. package/src/__tests__/guardian-outbound-http.test.ts +13 -0
  99. package/src/__tests__/guardian-routing-state.test.ts +6 -10
  100. package/src/__tests__/heartbeat-disk-pressure.test.ts +2 -0
  101. package/src/__tests__/heartbeat-service.test.ts +2 -0
  102. package/src/__tests__/helpers/channel-test-adapter.ts +46 -19
  103. package/src/__tests__/helpers/create-guardian-binding.ts +15 -23
  104. package/src/__tests__/helpers/mock-logger.ts +1 -0
  105. package/src/__tests__/helpers/seed-contact-channel.ts +96 -0
  106. package/src/__tests__/host-app-control-routes.test.ts +24 -30
  107. package/src/__tests__/host-bash-routes.test.ts +31 -41
  108. package/src/__tests__/host-browser-routes.test.ts +26 -32
  109. package/src/__tests__/host-cu-routes-targeted.test.ts +25 -33
  110. package/src/__tests__/host-file-routes-targeted.test.ts +40 -52
  111. package/src/__tests__/host-transfer-routes-targeted.test.ts +31 -43
  112. package/src/__tests__/http-user-message-parity.test.ts +290 -8
  113. package/src/__tests__/inbound-invite-redemption.test.ts +115 -10
  114. package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
  115. package/src/__tests__/invite-redemption-service.test.ts +471 -53
  116. package/src/__tests__/invite-routes-http.test.ts +34 -0
  117. package/src/__tests__/invite-service-ipc.test.ts +65 -2
  118. package/src/__tests__/llm-context-normalization.test.ts +105 -0
  119. package/src/__tests__/llm-request-log-error-payload.test.ts +71 -9
  120. package/src/__tests__/llm-usage-store.test.ts +25 -0
  121. package/src/__tests__/mcp-config-secret-boundary.test.ts +392 -0
  122. package/src/__tests__/mcp-health-check.test.ts +2 -1
  123. package/src/__tests__/media-stream-server-integration.test.ts +127 -0
  124. package/src/__tests__/memory-retrieval-hook.test.ts +2 -0
  125. package/src/__tests__/messaging-send-tool.test.ts +2 -0
  126. package/src/__tests__/migration-import-from-url.test.ts +2 -2
  127. package/src/__tests__/mtime-cache.test.ts +275 -5
  128. package/src/__tests__/native-web-search.test.ts +2 -0
  129. package/src/__tests__/non-member-access-request.test.ts +191 -17
  130. package/src/__tests__/notification-broadcaster.test.ts +4 -0
  131. package/src/__tests__/notification-decision-recipient-context.test.ts +33 -32
  132. package/src/__tests__/notification-deep-link.test.ts +6 -0
  133. package/src/__tests__/notification-guardian-path.test.ts +19 -0
  134. package/src/__tests__/onboarding-persona-write.test.ts +52 -22
  135. package/src/__tests__/openai-provider.test.ts +22 -12
  136. package/src/__tests__/openai-responses-provider.test.ts +12 -2
  137. package/src/__tests__/outbound-slack-persistence.test.ts +2 -0
  138. package/src/__tests__/pending-interactions-resolved-event.test.ts +7 -4
  139. package/src/__tests__/persist-onboarding-artifacts.test.ts +1 -0
  140. package/src/__tests__/persistence-secret-redaction.test.ts +2 -0
  141. package/src/__tests__/persona-resolver.test.ts +75 -45
  142. package/src/__tests__/plugin-bootstrap.test.ts +21 -83
  143. package/src/__tests__/plugin-disabled-state.test.ts +190 -0
  144. package/src/__tests__/plugin-pipeline.test.ts +96 -0
  145. package/src/__tests__/plugin-route-contribution.test.ts +6 -19
  146. package/src/__tests__/plugin-tool-contribution.test.ts +5 -20
  147. package/src/__tests__/plugin-types.test.ts +2 -4
  148. package/src/__tests__/process-message-background-slack.test.ts +2 -0
  149. package/src/__tests__/process-message-display-content.test.ts +2 -0
  150. package/src/__tests__/provider-error-scenarios.test.ts +5 -4
  151. package/src/__tests__/provider-usage-tracking.test.ts +39 -0
  152. package/src/__tests__/reaction-intercept-cold-cache-warm.test.ts +135 -0
  153. package/src/__tests__/reaction-intercept-member-verdict-warm.test.ts +158 -0
  154. package/src/__tests__/reaction-persistence.test.ts +51 -4
  155. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +2 -0
  156. package/src/__tests__/registry.test.ts +4 -1
  157. package/src/__tests__/relay-server.test.ts +758 -32
  158. package/src/__tests__/runtime-attachment-metadata.test.ts +9 -12
  159. package/src/__tests__/secret-ingress-http.test.ts +14 -0
  160. package/src/__tests__/send-endpoint-busy.test.ts +30 -8
  161. package/src/__tests__/settings-routes.test.ts +32 -0
  162. package/src/__tests__/skills.test.ts +44 -0
  163. package/src/__tests__/slack-inbound-verification.test.ts +47 -2
  164. package/src/__tests__/sse-actor-principal-guardian-source.test.ts +79 -0
  165. package/src/__tests__/steer-on-enqueue-question.test.ts +181 -0
  166. package/src/__tests__/stt-hints.test.ts +49 -15
  167. package/src/__tests__/subagent-detail.test.ts +27 -0
  168. package/src/__tests__/subagent-disposal.test.ts +65 -0
  169. package/src/__tests__/subagent-fork-prompt-role.test.ts +195 -0
  170. package/src/__tests__/subagent-fork-spawn.test.ts +6 -7
  171. package/src/__tests__/subagent-notify-parent.test.ts +2 -0
  172. package/src/__tests__/subagent-role-registry.test.ts +24 -6
  173. package/src/__tests__/subagent-spawn-and-await.test.ts +546 -0
  174. package/src/__tests__/subagent-spawn-tool-fork.test.ts +2 -0
  175. package/src/__tests__/subagent-tools.test.ts +398 -1
  176. package/src/__tests__/suggestion-routes.test.ts +2 -0
  177. package/src/__tests__/thread-backfill.test.ts +3 -3
  178. package/src/__tests__/title-generate-hook.test.ts +2 -0
  179. package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -0
  180. package/src/__tests__/tool-executor.test.ts +16 -11
  181. package/src/__tests__/tool-preview-lifecycle.test.ts +2 -0
  182. package/src/__tests__/tool-result-metadata-plumbing.test.ts +2 -0
  183. package/src/__tests__/tool-start-timestamp.test.ts +2 -0
  184. package/src/__tests__/trusted-contact-approval-notifier.test.ts +37 -51
  185. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +12 -12
  186. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +9 -7
  187. package/src/__tests__/trusted-contact-multichannel.test.ts +16 -7
  188. package/src/__tests__/trusted-contact-verification.test.ts +79 -54
  189. package/src/__tests__/twilio-routes.test.ts +96 -0
  190. package/src/__tests__/ui-file-upload-surface.test.ts +86 -0
  191. package/src/__tests__/verification-control-plane-policy.test.ts +2 -0
  192. package/src/__tests__/voice-guardian-cold-cache-warm.test.ts +137 -0
  193. package/src/__tests__/voice-invite-redemption.test.ts +216 -20
  194. package/src/__tests__/web-search-backend-failure.test.ts +2 -0
  195. package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +3 -3
  196. package/src/__tests__/workspace-migration-111-prune-seeded-callsite-defaults.test.ts +208 -0
  197. package/src/__tests__/workspace-migration-112-remove-advisor-callsite-override.test.ts +170 -0
  198. package/src/__tests__/workspace-migration-drop-user-md.test.ts +196 -238
  199. package/src/__tests__/workspace-migration-remove-hooks.test.ts +14 -35
  200. package/src/__tests__/workspace-tool-loader.test.ts +195 -2
  201. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +35 -47
  202. package/src/agent/loop-exclusive-tool.test.ts +154 -0
  203. package/src/agent/loop-native-web-search.test.ts +200 -0
  204. package/src/agent/loop.ts +164 -1
  205. package/src/api/constants/sse-replay.ts +41 -0
  206. package/src/api/events/conversation-notice.ts +26 -0
  207. package/src/api/index.ts +19 -1
  208. package/src/api/responses/llm-request-log-entry.ts +29 -0
  209. package/src/api/responses/subagent-detail.ts +17 -0
  210. package/src/api/surfaces.ts +39 -3
  211. package/src/approvals/guardian-channel-delivery.ts +30 -0
  212. package/src/approvals/guardian-expiry-notifier.ts +148 -0
  213. package/src/approvals/guardian-request-resolvers.ts +17 -15
  214. package/src/calls/__tests__/relay-setup-router.test.ts +267 -17
  215. package/src/calls/call-domain.ts +3 -3
  216. package/src/calls/guardian-dispatch.ts +14 -9
  217. package/src/calls/inbound-trust-reader.ts +24 -2
  218. package/src/calls/media-stream-server.ts +21 -0
  219. package/src/calls/relay-access-wait.ts +6 -6
  220. package/src/calls/relay-server.ts +188 -51
  221. package/src/calls/relay-setup-router.ts +47 -17
  222. package/src/calls/relay-verification.ts +4 -4
  223. package/src/calls/stt-hints.ts +9 -12
  224. package/src/calls/twilio-routes.ts +14 -4
  225. package/src/channels/types.ts +10 -20
  226. package/src/cli/commands/__tests__/cache.test.ts +8 -1
  227. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
  228. package/src/cli/commands/cache.ts +194 -181
  229. package/src/cli/commands/contacts.ts +10 -7
  230. package/src/cli/commands/db/__tests__/repair.test.ts +6 -5
  231. package/src/cli/commands/db/status.ts +37 -1
  232. package/src/cli/commands/mcp.ts +252 -218
  233. package/src/cli/commands/memory/__tests__/worker.test.ts +432 -0
  234. package/src/cli/commands/memory/index.ts +2 -0
  235. package/src/cli/commands/memory/worker.ts +242 -0
  236. package/src/cli/commands/plugins.ts +125 -13
  237. package/src/cli/lib/__tests__/install-from-github.test.ts +102 -0
  238. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +160 -1
  239. package/src/cli/lib/__tests__/publish-plugin.test.ts +404 -0
  240. package/src/cli/lib/list-installed-plugins.ts +179 -1
  241. package/src/cli/lib/publish-plugin.ts +633 -0
  242. package/src/config/__tests__/loader-callsite-strip-fallback.test.ts +143 -0
  243. package/src/config/__tests__/sync-gated-profiles.test.ts +16 -10
  244. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +27 -17
  245. package/src/config/bundled-skills/contacts/tools/contact-search.ts +13 -3
  246. package/src/config/bundled-skills/subagent/SKILL.md +17 -2
  247. package/src/config/bundled-skills/subagent/TOOLS.json +5 -4
  248. package/src/config/call-site-defaults.ts +0 -6
  249. package/src/config/feature-flag-registry.json +5 -13
  250. package/src/config/llm-resolver.ts +0 -3
  251. package/src/config/loader.ts +38 -5
  252. package/src/config/prune-seeded-callsite-defaults.ts +110 -0
  253. package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
  254. package/src/config/schemas/call-site-catalog.ts +0 -7
  255. package/src/config/schemas/heartbeat.ts +2 -5
  256. package/src/config/schemas/llm.ts +3 -12
  257. package/src/config/schemas/memory-lifecycle.ts +12 -0
  258. package/src/config/schemas/memory-v3.ts +7 -0
  259. package/src/config/schemas/memory.ts +4 -0
  260. package/src/config/schemas/timeouts.ts +8 -0
  261. package/src/config/seed-inference-profiles.ts +30 -34
  262. package/src/config/skills.ts +27 -5
  263. package/src/config/sync-gated-profiles.ts +12 -16
  264. package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +128 -0
  265. package/src/contacts/__tests__/guardian-delivery-reader.test.ts +312 -0
  266. package/src/contacts/__tests__/member-write-relay.test.ts +226 -0
  267. package/src/contacts/contact-store.ts +27 -237
  268. package/src/contacts/contacts-write.ts +18 -55
  269. package/src/contacts/gateway-channel-read.ts +51 -0
  270. package/src/contacts/guardian-delivery-reader.ts +223 -0
  271. package/src/contacts/member-write-relay.ts +183 -0
  272. package/src/contacts/types.ts +3 -15
  273. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +35 -0
  274. package/src/daemon/__tests__/conversation-tool-setup.test.ts +0 -44
  275. package/src/daemon/assistant-attachments.ts +27 -4
  276. package/src/daemon/conversation-agent-loop.ts +28 -0
  277. package/src/daemon/conversation-notices.ts +60 -0
  278. package/src/daemon/conversation-process.ts +35 -16
  279. package/src/daemon/conversation-surfaces.ts +111 -38
  280. package/src/daemon/conversation-tool-setup.ts +71 -30
  281. package/src/daemon/conversation.ts +23 -1
  282. package/src/daemon/disk-pressure-guard.ts +12 -2
  283. package/src/daemon/event-loop-watchdog.ts +28 -1
  284. package/src/daemon/external-plugins-bootstrap.ts +17 -41
  285. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +0 -1
  286. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -2
  287. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +25 -2
  288. package/src/daemon/handlers/__tests__/config-channels.test.ts +220 -0
  289. package/src/daemon/handlers/config-a2a.ts +6 -14
  290. package/src/daemon/handlers/config-channels.ts +67 -26
  291. package/src/daemon/handlers/conversations.ts +77 -0
  292. package/src/daemon/lifecycle.ts +16 -0
  293. package/src/daemon/mcp-reload-service.ts +10 -0
  294. package/src/daemon/memory-v2-startup.test.ts +72 -0
  295. package/src/daemon/memory-v2-startup.ts +87 -19
  296. package/src/daemon/message-types/conversations.ts +2 -0
  297. package/src/daemon/message-types/surfaces.ts +12 -12
  298. package/src/daemon/server.ts +0 -4
  299. package/src/daemon/shutdown-handlers.ts +20 -0
  300. package/src/daemon/tool-setup-types.ts +9 -0
  301. package/src/heartbeat/heartbeat-service.ts +5 -0
  302. package/src/home/relationship-state-writer.ts +5 -0
  303. package/src/hooks/hook-loader.ts +341 -0
  304. package/src/ipc/__tests__/clients-list-ipc.test.ts +1 -1
  305. package/src/ipc/assistant-server.ts +2 -2
  306. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -0
  307. package/src/mcp/client.ts +15 -1
  308. package/src/mcp/mcp-auth-orchestrator.ts +6 -1
  309. package/src/mcp/mcp-header-store.ts +134 -0
  310. package/src/mcp/mcp-oauth-provider.ts +19 -8
  311. package/src/memory/__tests__/301-create-watchdog-events.test.ts +110 -0
  312. package/src/memory/__tests__/memory-retrospective-job.test.ts +8 -0
  313. package/src/memory/__tests__/prompt-override.test.ts +192 -0
  314. package/src/memory/__tests__/watchdog-events-store.test.ts +161 -0
  315. package/src/memory/compaction-ledger-store.ts +107 -0
  316. package/src/memory/conversation-crud.ts +129 -61
  317. package/src/memory/db-connection.ts +22 -3
  318. package/src/memory/db-init.ts +37 -503
  319. package/src/memory/db-singleton.ts +6 -4
  320. package/src/memory/jobs-worker.ts +104 -0
  321. package/src/memory/llm-request-log-store.ts +26 -1
  322. package/src/memory/llm-usage-store.ts +48 -20
  323. package/src/memory/memory-retrospective-job.ts +14 -8
  324. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +152 -56
  325. package/src/memory/migrations/300-add-processing-started-at.ts +30 -0
  326. package/src/memory/migrations/301-create-watchdog-events.ts +45 -0
  327. package/src/memory/migrations/302-create-compaction-events.ts +107 -0
  328. package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +297 -0
  329. package/src/memory/migrations/__tests__/run-migrations.test.ts +2 -2
  330. package/src/memory/migrations/run-migrations.ts +106 -10
  331. package/src/memory/migrations/validate-migration-state.ts +105 -67
  332. package/src/memory/prompt-override.ts +129 -0
  333. package/src/memory/schema/contacts.ts +6 -2
  334. package/src/memory/schema/conversations.ts +37 -0
  335. package/src/memory/schema/infrastructure.ts +20 -0
  336. package/src/memory/steps.ts +1294 -0
  337. package/src/memory/v2/__tests__/cli-command-store.test.ts +25 -0
  338. package/src/memory/v2/__tests__/skill-store.test.ts +80 -0
  339. package/src/memory/v2/cli-command-store.ts +75 -38
  340. package/src/memory/v2/prompts/consolidation.ts +13 -82
  341. package/src/memory/v2/prompts/router.ts +21 -93
  342. package/src/memory/v2/skill-store.ts +68 -31
  343. package/src/memory/watchdog-events-store.ts +87 -0
  344. package/src/memory/worker-control.ts +204 -0
  345. package/src/memory/worker-process.ts +89 -0
  346. package/src/messaging/channel-binding-metadata.ts +31 -0
  347. package/src/messaging/provider-types.ts +8 -0
  348. package/src/messaging/providers/slack/binding-metadata.ts +60 -0
  349. package/src/notifications/__tests__/broadcaster.test.ts +8 -8
  350. package/src/notifications/__tests__/connected-channels.test.ts +86 -0
  351. package/src/notifications/__tests__/decision-engine.test.ts +78 -9
  352. package/src/notifications/__tests__/destination-resolver.test.ts +151 -0
  353. package/src/notifications/broadcaster.ts +8 -1
  354. package/src/notifications/decision-engine.ts +15 -7
  355. package/src/notifications/destination-resolver.ts +53 -25
  356. package/src/notifications/emit-signal.ts +34 -15
  357. package/src/onboarding/checkin-event.test.ts +222 -0
  358. package/src/onboarding/checkin-event.ts +321 -0
  359. package/src/onboarding/schedule-checkin.ts +190 -0
  360. package/src/permissions/question-prompter.test.ts +1 -1
  361. package/src/permissions/question-prompter.ts +7 -4
  362. package/src/plugin-api/index.ts +12 -12
  363. package/src/plugin-api/types.ts +12 -14
  364. package/src/plugin-api/vision-support.test.ts +28 -4
  365. package/src/plugin-api/vision-support.ts +66 -31
  366. package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +2 -2
  367. package/src/plugins/defaults/empty-response/hooks/stop.ts +2 -2
  368. package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +4 -3
  369. package/src/plugins/defaults/history-repair/hooks/post-model-call.ts +2 -2
  370. package/src/plugins/defaults/history-repair/hooks/stop.ts +2 -2
  371. package/src/plugins/defaults/history-repair/hooks/user-prompt-submit.ts +2 -2
  372. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +47 -7
  373. package/src/plugins/defaults/image-fallback/hooks/post-tool-use.ts +12 -13
  374. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +14 -22
  375. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +42 -11
  376. package/src/plugins/defaults/image-recovery/hooks/post-model-call.ts +2 -2
  377. package/src/plugins/defaults/image-recovery/hooks/stop.ts +2 -2
  378. package/src/plugins/defaults/index.ts +0 -35
  379. package/src/plugins/defaults/max-tokens-continue/hooks/post-model-call.ts +2 -2
  380. package/src/plugins/defaults/max-tokens-continue/hooks/stop.ts +2 -2
  381. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +2 -2
  382. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +2 -2
  383. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +33 -3
  384. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +48 -4
  385. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +4 -8
  386. package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +2 -2
  387. package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +2 -2
  388. package/src/plugins/defaults/memory-v3-shadow/injector.ts +43 -15
  389. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +11 -2
  390. package/src/plugins/defaults/memory-v3-shadow/pool-select.test.ts +146 -0
  391. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +77 -13
  392. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +12 -11
  393. package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +2 -2
  394. package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +2 -2
  395. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +2 -2
  396. package/src/plugins/defaults/title-generate/hooks/stop.ts +2 -2
  397. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +2 -2
  398. package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +2 -2
  399. package/src/plugins/defaults/tool-result-truncate/hooks/post-tool-use.ts +2 -2
  400. package/src/plugins/disabled-state.ts +31 -0
  401. package/src/plugins/external-plugin-loader.ts +2 -2
  402. package/src/plugins/mtime-cache.ts +186 -330
  403. package/src/plugins/pipeline.ts +111 -13
  404. package/src/plugins/registry.ts +59 -16
  405. package/src/plugins/surface-import.ts +121 -0
  406. package/src/plugins/types.ts +7 -9
  407. package/src/prompts/persona-resolver.ts +43 -11
  408. package/src/providers/anthropic/client.ts +5 -0
  409. package/src/providers/call-site-routing.ts +45 -0
  410. package/src/providers/model-catalog.ts +16 -0
  411. package/src/providers/openai/__tests__/api-error-normalization.test.ts +321 -0
  412. package/src/providers/openai/api-error-normalization.ts +270 -0
  413. package/src/providers/openai/chat-completions-provider.ts +37 -83
  414. package/src/providers/openai/responses-provider.ts +50 -46
  415. package/src/providers/openrouter/client.ts +5 -0
  416. package/src/providers/provider-send-message.ts +10 -0
  417. package/src/providers/ratelimit.ts +10 -0
  418. package/src/providers/registry.ts +1 -1
  419. package/src/providers/retry.ts +10 -0
  420. package/src/providers/types.ts +22 -0
  421. package/src/providers/usage-tracking.ts +10 -0
  422. package/src/runtime/__tests__/channel-verification-service.test.ts +133 -0
  423. package/src/runtime/__tests__/guardian-vellum-migration.test.ts +184 -0
  424. package/src/runtime/__tests__/is-guardian-bound-for-channel.test.ts +66 -0
  425. package/src/runtime/__tests__/local-principal-trust.test.ts +162 -0
  426. package/src/runtime/__tests__/member-verdict-cache.test.ts +119 -0
  427. package/src/runtime/__tests__/trust-verdict-consumer.test.ts +402 -123
  428. package/src/runtime/access-request-helper.ts +18 -39
  429. package/src/runtime/actor-trust-resolver.ts +46 -19
  430. package/src/runtime/anchored-guardian.test.ts +109 -0
  431. package/src/runtime/anchored-guardian.ts +86 -0
  432. package/src/runtime/assistant-event-hub.ts +1 -1
  433. package/src/runtime/assistant-stream-state.ts +9 -2
  434. package/src/runtime/auth/__tests__/require-bound-guardian.test.ts +99 -0
  435. package/src/runtime/auth/require-bound-guardian.ts +21 -11
  436. package/src/runtime/channel-verification-service.ts +56 -31
  437. package/src/runtime/confirmation-request-guardian-bridge.ts +3 -3
  438. package/src/runtime/guardian-vellum-migration.ts +69 -8
  439. package/src/runtime/invite-redemption-service.ts +213 -187
  440. package/src/runtime/local-actor-identity.test.ts +108 -0
  441. package/src/runtime/local-actor-identity.ts +85 -13
  442. package/src/runtime/local-principal-trust.ts +52 -0
  443. package/src/runtime/member-verdict-cache.ts +0 -0
  444. package/src/runtime/pending-interactions.ts +11 -1
  445. package/src/runtime/routes/__tests__/channel-verification-revoke.test.ts +56 -5
  446. package/src/runtime/routes/__tests__/channel-verification-routes.test.ts +1 -1
  447. package/src/runtime/routes/__tests__/contact-routes.test.ts +305 -0
  448. package/src/runtime/routes/__tests__/global-search-routes.test.ts +92 -0
  449. package/src/runtime/routes/__tests__/http-adapter-actor-header.test.ts +129 -0
  450. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +188 -0
  451. package/src/runtime/routes/browser-routes.ts +1 -1
  452. package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +13 -5
  453. package/src/runtime/routes/channel-verification-routes.ts +3 -3
  454. package/src/runtime/routes/contact-routes.ts +48 -57
  455. package/src/runtime/routes/conversation-cli-routes.ts +4 -5
  456. package/src/runtime/routes/conversation-list-routes.ts +4 -7
  457. package/src/runtime/routes/conversation-query-routes.ts +72 -0
  458. package/src/runtime/routes/conversation-routes.ts +84 -85
  459. package/src/runtime/routes/events-routes.ts +2 -2
  460. package/src/runtime/routes/global-search-routes.ts +3 -1
  461. package/src/runtime/routes/guardian-action-routes.ts +4 -5
  462. package/src/runtime/routes/host-app-control-routes.ts +5 -4
  463. package/src/runtime/routes/host-bash-routes.ts +5 -4
  464. package/src/runtime/routes/host-browser-routes.ts +9 -11
  465. package/src/runtime/routes/host-cu-routes.ts +5 -4
  466. package/src/runtime/routes/host-file-routes.ts +5 -4
  467. package/src/runtime/routes/host-transfer-routes.ts +6 -6
  468. package/src/runtime/routes/http-adapter.ts +16 -1
  469. package/src/runtime/routes/identity-routes.ts +3 -2
  470. package/src/runtime/routes/inbound-message-handler.ts +5 -5
  471. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +97 -5
  472. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +61 -59
  473. package/src/runtime/routes/inbound-stages/background-dispatch.ts +13 -5
  474. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +7 -7
  475. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +21 -8
  476. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +14 -3
  477. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +19 -0
  478. package/src/runtime/routes/index.ts +2 -0
  479. package/src/runtime/routes/llm-context-normalization.ts +83 -0
  480. package/src/runtime/routes/mcp-auth-routes.ts +171 -19
  481. package/src/runtime/routes/migration-rollback-routes.ts +4 -3
  482. package/src/runtime/routes/migration-routes.ts +4 -1
  483. package/src/runtime/routes/onboarding-checkin-routes.ts +86 -0
  484. package/src/runtime/routes/settings-routes.ts +8 -3
  485. package/src/runtime/routes/subagents-routes.ts +5 -0
  486. package/src/runtime/routes/surface-action-routes.ts +42 -56
  487. package/src/runtime/services/__tests__/conversation-serializer.test.ts +1 -0
  488. package/src/runtime/services/conversation-serializer.ts +13 -58
  489. package/src/runtime/tool-grant-request-helper.ts +3 -3
  490. package/src/runtime/trust-verdict-consumer.ts +111 -40
  491. package/src/runtime/verification-outbound-actions.ts +18 -18
  492. package/src/signals/user-message.ts +16 -0
  493. package/src/subagent/__tests__/consult-prompt.test.ts +35 -0
  494. package/src/{plugins/defaults/advisor/__tests__/transcript.test.ts → subagent/__tests__/consult-transcript.test.ts} +47 -10
  495. package/src/{plugins/defaults/advisor/steering.ts → subagent/consult-prompt.ts} +22 -32
  496. package/src/{plugins/defaults/advisor/transcript.ts → subagent/consult-transcript.ts} +18 -8
  497. package/src/subagent/index.ts +1 -1
  498. package/src/subagent/manager.ts +254 -33
  499. package/src/subagent/types.ts +11 -4
  500. package/src/telemetry/types.ts +34 -1
  501. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  502. package/src/telemetry/usage-telemetry-reporter.ts +87 -3
  503. package/src/tools/ask-question/ask-question-tool.test.ts +29 -0
  504. package/src/tools/ask-question/ask-question-tool.ts +13 -0
  505. package/src/tools/executor.ts +4 -4
  506. package/src/tools/filesystem/search.ts +543 -0
  507. package/src/tools/registry.ts +38 -0
  508. package/src/tools/shared/filesystem/path-policy.ts +12 -5
  509. package/src/tools/subagent/consult-deadline.ts +49 -0
  510. package/src/tools/subagent/spawn.ts +234 -5
  511. package/src/tools/tool-approval-handler.ts +1 -1
  512. package/src/tools/tool-defaults.ts +9 -2
  513. package/src/tools/tool-manifest.ts +3 -0
  514. package/src/tools/types.ts +131 -25
  515. package/src/tools/workspace-tools/loader.ts +348 -244
  516. package/src/util/errors.ts +26 -1
  517. package/src/util/logger.ts +9 -0
  518. package/src/util/platform.ts +19 -0
  519. package/src/util/telemetry-db-path.ts +24 -0
  520. package/src/workflows/library.test.ts +140 -0
  521. package/src/workflows/library.ts +82 -28
  522. package/src/workspace/migrations/017-seed-persona-dirs.ts +3 -34
  523. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +3 -24
  524. package/src/workspace/migrations/031-drop-user-md.ts +232 -148
  525. package/src/workspace/migrations/048-remove-workspace-hooks.ts +14 -66
  526. package/src/workspace/migrations/111-prune-seeded-callsite-defaults.ts +134 -0
  527. package/src/workspace/migrations/112-remove-advisor-callsite-override.ts +64 -0
  528. package/src/workspace/migrations/registry.ts +4 -0
  529. package/src/__tests__/workspace-tools-watcher-flag.test.ts +0 -70
  530. package/src/daemon/workspace-tools-watcher.ts +0 -328
  531. package/src/memory/migrations/registry.ts +0 -573
  532. package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +0 -56
  533. package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +0 -43
  534. package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +0 -137
  535. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +0 -153
  536. package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +0 -138
  537. package/src/plugins/defaults/advisor/advisor-gate.ts +0 -29
  538. package/src/plugins/defaults/advisor/advisor-state-store.ts +0 -94
  539. package/src/plugins/defaults/advisor/config.ts +0 -21
  540. package/src/plugins/defaults/advisor/consult.ts +0 -93
  541. package/src/plugins/defaults/advisor/hooks/post-model-call.ts +0 -34
  542. package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +0 -30
  543. package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +0 -19
  544. package/src/plugins/defaults/advisor/package.json +0 -14
  545. package/src/plugins/defaults/advisor/tools/advisor.ts +0 -65
  546. package/src/providers/openai/__tests__/api-error-detail.test.ts +0 -120
@@ -22,21 +22,27 @@ mock.module("../util/logger.js", () => ({
22
22
  }),
23
23
  }));
24
24
 
25
- import {
26
- findContactChannel,
27
- findGuardianForChannel,
28
- } from "../contacts/contact-store.js";
25
+ import { and, desc, eq } from "drizzle-orm";
26
+
27
+ import type { ChannelId } from "../channels/types.js";
28
+ import { findContactChannel } from "../contacts/contact-store.js";
29
29
  import {
30
30
  revokeMember,
31
31
  upsertContactChannel,
32
32
  } from "../contacts/contacts-write.js";
33
+ import type { ChannelStatus } from "../contacts/types.js";
33
34
  import { getDb } from "../memory/db-connection.js";
34
35
  import { initializeDb } from "../memory/db-init.js";
36
+ import { contactChannels, contacts } from "../memory/schema.js";
35
37
  import { resolveActorTrust } from "../runtime/actor-trust-resolver.js";
36
38
  import {
37
39
  createOutboundSession,
38
40
  validateAndConsumeVerification,
39
41
  } from "../runtime/channel-verification-service.js";
42
+ import {
43
+ __resetMemberVerdictCacheForTest,
44
+ setMemberVerdict,
45
+ } from "../runtime/member-verdict-cache.js";
40
46
  import { createGuardianBinding } from "./helpers/create-guardian-binding.js";
41
47
 
42
48
  await initializeDb();
@@ -53,6 +59,48 @@ function resetTables(): void {
53
59
  db.run("DELETE FROM contacts");
54
60
  }
55
61
 
62
+ // Mirror a warmed gateway verdict so the sync resolveActorTrust fallback
63
+ // resolves the member with the given status; the local ACL columns are no
64
+ // longer read.
65
+ function warmMemberVerdict(
66
+ channelType: ChannelId,
67
+ address: string,
68
+ status: ChannelStatus = "unverified",
69
+ ): void {
70
+ const found = findContactChannel({ channelType, address });
71
+ if (!found) return;
72
+ setMemberVerdict(channelType, address, {
73
+ trustClass: "unknown",
74
+ canonicalSenderId: address,
75
+ contactId: found.contact.id,
76
+ channelId: found.channel.id,
77
+ status,
78
+ policy: "allow",
79
+ });
80
+ }
81
+
82
+ /**
83
+ * Read the local active guardian channel for a channel type, mirroring the
84
+ * gateway's role/status resolution. Used by assertions that confirm the local
85
+ * guardian binding state after verification flows.
86
+ */
87
+ function localGuardianForChannel(channelType: string) {
88
+ const row = getDb()
89
+ .select({ contact: contacts, channel: contactChannels })
90
+ .from(contacts)
91
+ .innerJoin(contactChannels, eq(contacts.id, contactChannels.contactId))
92
+ .where(
93
+ and(
94
+ eq(contacts.role, "guardian"),
95
+ eq(contactChannels.type, channelType),
96
+ eq(contactChannels.status, "active"),
97
+ ),
98
+ )
99
+ .orderBy(desc(contactChannels.verifiedAt))
100
+ .get();
101
+ return row ? { contact: row.contact, channel: row.channel } : null;
102
+ }
103
+
56
104
  // ---------------------------------------------------------------------------
57
105
  // Tests
58
106
  // ---------------------------------------------------------------------------
@@ -60,6 +108,7 @@ function resetTables(): void {
60
108
  describe("trusted contact verification → member activation", () => {
61
109
  beforeEach(() => {
62
110
  resetTables();
111
+ __resetMemberVerdictCacheForTest();
63
112
  });
64
113
 
65
114
  test("successful verification creates active member with allow policy", () => {
@@ -88,26 +137,24 @@ describe("trusted contact verification → member activation", () => {
88
137
  expect(result.verificationType).toBe("trusted_contact");
89
138
  }
90
139
 
91
- // Simulate the member upsert that inbound-message-handler performs on success
140
+ // Simulate the member upsert that inbound-message-handler performs on
141
+ // success. The local mirror persists identity only; the gateway owns the
142
+ // ACL verdict, so the channel lands at the schema-default status.
92
143
  upsertContactChannel({
93
144
  sourceChannel: "telegram",
94
145
  externalUserId: "requester-user-123",
95
146
  externalChatId: "requester-chat-123",
96
- status: "active",
97
- policy: "allow",
98
147
  displayName: "Requester Name",
99
148
  username: "requester_username",
100
149
  });
101
150
 
102
- // Verify: active member record exists
151
+ // Verify: member identity record exists
103
152
  const contactResult = findContactChannel({
104
153
  channelType: "telegram",
105
154
  address: "requester-user-123",
106
155
  });
107
156
 
108
157
  expect(contactResult).not.toBeNull();
109
- expect(contactResult!.channel.status).toBe("active");
110
- expect(contactResult!.channel.policy).toBe("allow");
111
158
  expect(contactResult!.channel.address).toBe("requester-user-123");
112
159
  expect(contactResult!.channel.externalChatId).toBe("requester-chat-123");
113
160
  expect(contactResult!.contact.displayName).toBe("Requester Name");
@@ -119,11 +166,10 @@ describe("trusted contact verification → member activation", () => {
119
166
  sourceChannel: "telegram",
120
167
  externalUserId: "requester-user-jeff",
121
168
  externalChatId: "requester-chat-jeff",
122
- status: "active",
123
- policy: "allow",
124
169
  displayName: "Jeff",
125
170
  username: "jeff_handle",
126
171
  });
172
+ warmMemberVerdict("telegram", "requester-user-jeff");
127
173
 
128
174
  const trust = resolveActorTrust({
129
175
  assistantId: "self",
@@ -132,7 +178,9 @@ describe("trusted contact verification → member activation", () => {
132
178
  actorExternalId: "requester-user-jeff",
133
179
  });
134
180
 
135
- expect(trust.trustClass).toBe("trusted_contact");
181
+ // The local mirror persists identity only; the schema-default status places
182
+ // the contact in the unverified_contact tier (gateway owns elevation).
183
+ expect(trust.trustClass).toBe("unverified_contact");
136
184
  expect(trust.actorMetadata.displayName).toBe("Jeff");
137
185
  expect(trust.actorMetadata.senderDisplayName).toBeUndefined();
138
186
  expect(trust.actorMetadata.memberDisplayName).toBe("Jeff");
@@ -147,11 +195,10 @@ describe("trusted contact verification → member activation", () => {
147
195
  sourceChannel: "telegram",
148
196
  externalUserId: "requester-user-jeff-priority",
149
197
  externalChatId: "requester-chat-jeff-priority",
150
- status: "active",
151
- policy: "allow",
152
198
  displayName: "Jeff",
153
199
  username: "jeff_handle",
154
200
  });
201
+ warmMemberVerdict("telegram", "requester-user-jeff-priority");
155
202
 
156
203
  const trust = resolveActorTrust({
157
204
  assistantId: "self",
@@ -162,7 +209,7 @@ describe("trusted contact verification → member activation", () => {
162
209
  actorDisplayName: "Jeffrey",
163
210
  });
164
211
 
165
- expect(trust.trustClass).toBe("trusted_contact");
212
+ expect(trust.trustClass).toBe("unverified_contact");
166
213
  expect(trust.actorMetadata.displayName).toBe("Jeff");
167
214
  expect(trust.actorMetadata.senderDisplayName).toBe("Jeffrey");
168
215
  expect(trust.actorMetadata.memberDisplayName).toBe("Jeff");
@@ -179,8 +226,6 @@ describe("trusted contact verification → member activation", () => {
179
226
  sourceChannel: "telegram",
180
227
  externalUserId: "other-user-in-group",
181
228
  externalChatId: "shared-group-chat",
182
- status: "active",
183
- policy: "allow",
184
229
  displayName: "Other User",
185
230
  username: "other_handle",
186
231
  });
@@ -229,20 +274,17 @@ describe("trusted contact verification → member activation", () => {
229
274
  sourceChannel: "telegram",
230
275
  externalUserId: "requester-user-456",
231
276
  externalChatId: "requester-chat-456",
232
- status: "active",
233
- policy: "allow",
234
277
  });
235
278
 
236
- // Simulate the ACL check that inbound-message-handler performs
279
+ // The local mirror persists the member identity; the gateway owns the ACL
280
+ // verdict the inbound handler enforces.
237
281
  const contactResult = findContactChannel({
238
282
  channelType: "telegram",
239
283
  address: "requester-user-456",
240
284
  });
241
285
 
242
286
  expect(contactResult).not.toBeNull();
243
- expect(contactResult!.channel.status).toBe("active");
244
- expect(contactResult!.channel.policy).toBe("allow");
245
- // ACL check passes: member exists, is active, and has allow policy
287
+ expect(contactResult!.channel.address).toBe("requester-user-456");
246
288
  });
247
289
 
248
290
  test("member lookup is scoped by channel type", () => {
@@ -267,8 +309,6 @@ describe("trusted contact verification → member activation", () => {
267
309
  sourceChannel: "telegram",
268
310
  externalUserId: "user-cross-test",
269
311
  externalChatId: "chat-cross-test",
270
- status: "active",
271
- policy: "allow",
272
312
  });
273
313
 
274
314
  // Member should be found via contacts
@@ -277,7 +317,7 @@ describe("trusted contact verification → member activation", () => {
277
317
  address: "user-cross-test",
278
318
  });
279
319
  expect(contactResult).not.toBeNull();
280
- expect(contactResult!.channel.status).toBe("active");
320
+ expect(contactResult!.channel.address).toBe("user-cross-test");
281
321
 
282
322
  // Member should NOT be found for a different channel type
283
323
  const otherChannel = findContactChannel({
@@ -287,31 +327,22 @@ describe("trusted contact verification → member activation", () => {
287
327
  expect(otherChannel).toBeNull();
288
328
  });
289
329
 
290
- test("re-verification of previously revoked member reactivates them", () => {
291
- // Create and activate a member
330
+ test("revokeMember resolves the member identity without mutating local ACL", () => {
331
+ // Create a member identity row.
292
332
  const member = upsertContactChannel({
293
333
  sourceChannel: "telegram",
294
334
  externalUserId: "user-revoked",
295
335
  externalChatId: "chat-revoked",
296
- status: "active",
297
- policy: "allow",
298
336
  displayName: "Revoked User",
299
337
  });
300
338
 
301
- // Revoke the member
302
- const revoked = revokeMember(member!.channel.id, "testing revocation");
339
+ // The local revoke is a pure resolver now — the gateway owns the ACL
340
+ // downgrade; revokeMember returns the resolved native contact/channel.
341
+ const revoked = revokeMember(member!.channel.id);
303
342
  expect(revoked).not.toBeNull();
304
- expect(revoked!.channel.status).toBe("revoked");
305
-
306
- // Verify the member is indeed revoked (ACL would reject)
307
- const revokedResult = findContactChannel({
308
- channelType: "telegram",
309
- address: "user-revoked",
310
- });
311
- expect(revokedResult).not.toBeNull();
312
- expect(revokedResult!.channel.status).toBe("revoked");
343
+ expect(revoked!.channel.id).toBe(member!.channel.id);
313
344
 
314
- // Guardian re-approves, new outbound session created
345
+ // Re-upsert on the same identity is idempotent on the identity row.
315
346
  const session = createOutboundSession({
316
347
  channel: "telegram",
317
348
  expectedExternalUserId: "user-revoked",
@@ -321,7 +352,6 @@ describe("trusted contact verification → member activation", () => {
321
352
  verificationPurpose: "trusted_contact",
322
353
  });
323
354
 
324
- // Requester enters the new code
325
355
  const result = validateAndConsumeVerification(
326
356
  "telegram",
327
357
  session.secret,
@@ -333,23 +363,18 @@ describe("trusted contact verification → member activation", () => {
333
363
  expect(result.verificationType).toBe("trusted_contact");
334
364
  }
335
365
 
336
- // upsertContactChannel reactivates the existing record
337
366
  upsertContactChannel({
338
367
  sourceChannel: "telegram",
339
368
  externalUserId: "user-revoked",
340
369
  externalChatId: "chat-revoked",
341
- status: "active",
342
- policy: "allow",
343
370
  });
344
371
 
345
- // Verify: member is now active again
346
- const reactivatedResult = findContactChannel({
372
+ const reResolved = findContactChannel({
347
373
  channelType: "telegram",
348
374
  address: "user-revoked",
349
375
  });
350
- expect(reactivatedResult).not.toBeNull();
351
- expect(reactivatedResult!.channel.status).toBe("active");
352
- expect(reactivatedResult!.channel.policy).toBe("allow");
376
+ expect(reResolved).not.toBeNull();
377
+ expect(reResolved!.channel.id).toBe(member!.channel.id);
353
378
  });
354
379
 
355
380
  test("trusted contact verification does NOT create a guardian binding", () => {
@@ -388,7 +413,7 @@ describe("trusted contact verification → member activation", () => {
388
413
  }
389
414
 
390
415
  // The original guardian binding should remain intact
391
- const guardianResult = findGuardianForChannel("telegram");
416
+ const guardianResult = localGuardianForChannel("telegram");
392
417
  expect(guardianResult).not.toBeNull();
393
418
  expect(guardianResult!.channel.address).toBe("guardian-user-original");
394
419
  });
@@ -412,7 +437,7 @@ describe("trusted contact verification → member activation", () => {
412
437
  expect(result.verificationType).toBe("guardian");
413
438
  }
414
439
 
415
- const guardianResult = findGuardianForChannel("telegram");
440
+ const guardianResult = localGuardianForChannel("telegram");
416
441
  expect(guardianResult).toBeNull();
417
442
  });
418
443
  });
@@ -109,6 +109,25 @@ mock.module("../calls/channel-admission-reader.js", () => ({
109
109
  getChannelAdmissionPolicy: async () => mockAdmissionPolicy,
110
110
  }));
111
111
 
112
+ // Mock the inbound trust reader used by the media-stream preflight. Captures
113
+ // its args so tests can assert the inbound caller's verdict is fetched, and
114
+ // returns mockInboundVerdict which is threaded into the preflight routeSetup.
115
+ let mockInboundVerdict: unknown = null;
116
+ let lastInboundVerdictArgs: Record<string, unknown> | null = null;
117
+ mock.module("../calls/inbound-trust-reader.js", () => ({
118
+ getInboundTrustVerdict: async (args: Record<string, unknown>) => {
119
+ lastInboundVerdictArgs = args;
120
+ return mockInboundVerdict;
121
+ },
122
+ getPhoneCallerVerdict: async (otherPartyNumber: string | undefined) => {
123
+ lastInboundVerdictArgs = {
124
+ channelType: "phone",
125
+ actorExternalId: otherPartyNumber || undefined,
126
+ };
127
+ return mockInboundVerdict;
128
+ },
129
+ }));
130
+
112
131
  mock.module("../config/env.js", () => ({
113
132
  isHttpAuthDisabled: () => true,
114
133
  getGatewayInternalBaseUrl: () => "http://gateway.internal:7830",
@@ -470,6 +489,8 @@ describe("twilio webhook routes", () => {
470
489
  // Reset admission policy + captured routeSetup context between tests
471
490
  mockAdmissionPolicy = null;
472
491
  lastRouteSetupCtx = null;
492
+ mockInboundVerdict = null;
493
+ lastInboundVerdictArgs = null;
473
494
  // Reset routeSetup mock to default normal_call
474
495
  mockRouteSetupResult = {
475
496
  outcome: { action: "normal_call", isInbound: true },
@@ -1269,6 +1290,81 @@ describe("twilio webhook routes", () => {
1269
1290
  expect(lastRouteSetupCtx?.admissionPolicy).toBe("guardian_only");
1270
1291
  });
1271
1292
 
1293
+ test("media-stream inbound: fetches the caller's verdict (From) and threads it into the preflight routeSetup", async () => {
1294
+ mockConfigObj.services.stt.provider = "openai-whisper" as any;
1295
+ mockInboundVerdict = {
1296
+ channelType: "phone",
1297
+ actorExternalId: "+14155551234",
1298
+ contactId: "contact-1",
1299
+ channelId: "channel-1",
1300
+ status: "verified",
1301
+ policy: "allow",
1302
+ resolutionFailed: false,
1303
+ };
1304
+
1305
+ const req = makeInboundVoiceRequest({
1306
+ CallSid: "CA_ms_verdict_inbound_1",
1307
+ From: "+14155551234",
1308
+ To: "+15550001111",
1309
+ });
1310
+
1311
+ const res = await handleVoiceWebhook(req);
1312
+ expect(res.status).toBe(200);
1313
+
1314
+ // Inbound: verdict fetched for the caller (From) on the phone channel.
1315
+ expect(lastInboundVerdictArgs).toEqual({
1316
+ channelType: "phone",
1317
+ actorExternalId: "+14155551234",
1318
+ });
1319
+ // Verdict threaded into the preflight routeSetup.
1320
+ expect(lastRouteSetupCtx?.verdict).toEqual(mockInboundVerdict);
1321
+ });
1322
+
1323
+ test("media-stream inbound: a blocked/denied member verdict is classified deny in the preflight", async () => {
1324
+ mockConfigObj.services.stt.provider = "openai-whisper" as any;
1325
+ mockInboundVerdict = {
1326
+ channelType: "phone",
1327
+ actorExternalId: "+14155551234",
1328
+ contactId: "contact-1",
1329
+ channelId: "channel-1",
1330
+ status: "blocked",
1331
+ policy: "deny",
1332
+ resolutionFailed: false,
1333
+ };
1334
+ // The real router returns `deny` for a blocked member verdict; the mock
1335
+ // reflects that outcome. deny is supported on media-stream, so the
1336
+ // preflight still emits Stream TwiML (denial spoken at stream start).
1337
+ mockRouteSetupResult = {
1338
+ outcome: {
1339
+ action: "deny",
1340
+ message:
1341
+ "This number is not authorized to reach the assistant right now.",
1342
+ logReason: "Inbound voice ACL: member blocked",
1343
+ },
1344
+ resolved: {
1345
+ assistantId: "self",
1346
+ isInbound: true,
1347
+ otherPartyNumber: "+14155551234",
1348
+ actorTrust: { trustClass: "unknown", memberRecord: null },
1349
+ },
1350
+ };
1351
+
1352
+ const req = makeInboundVoiceRequest({
1353
+ CallSid: "CA_ms_verdict_deny_1",
1354
+ From: "+14155551234",
1355
+ To: "+15550001111",
1356
+ });
1357
+
1358
+ const res = await handleVoiceWebhook(req);
1359
+ expect(res.status).toBe(200);
1360
+
1361
+ // Verdict was threaded into routeSetup, which denied the caller.
1362
+ expect(lastRouteSetupCtx?.verdict).toEqual(mockInboundVerdict);
1363
+ const twiml = await res.text();
1364
+ expect(twiml).toContain("<Stream");
1365
+ expect(twiml).not.toContain("<ConversationRelay");
1366
+ });
1367
+
1272
1368
  test("media-stream: floor-denied caller classified deny still produces Stream TwiML (deny handled at stream level)", async () => {
1273
1369
  mockConfigObj.services.stt.provider = "openai-whisper" as any;
1274
1370
  mockAdmissionPolicy = "guardian_only";
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
+ import { FileUploadSurfaceDataSchema } from "../api/surfaces.js";
3
4
  import type {
4
5
  FileUploadSurfaceData,
5
6
  UiSurfaceShowFileUpload,
@@ -103,3 +104,88 @@ describe("UI surface tool registration", () => {
103
104
  ]);
104
105
  });
105
106
  });
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // FileUploadSurfaceDataSchema coercion
110
+ // ---------------------------------------------------------------------------
111
+ //
112
+ // `acceptedTypes` is contractually a string[], but the model frequently emits a
113
+ // comma-joined string or a bare string. The renderer calls `.join`/`.some` on
114
+ // it, so the schema coerces every shape to the array contract.
115
+
116
+ describe("FileUploadSurfaceDataSchema coercion", () => {
117
+ test("passes a well-formed payload through unchanged", () => {
118
+ expect(
119
+ FileUploadSurfaceDataSchema.parse({
120
+ prompt: "Share the receipt PDF",
121
+ acceptedTypes: ["image/*", "application/pdf"],
122
+ maxFiles: 3,
123
+ }),
124
+ ).toEqual({
125
+ prompt: "Share the receipt PDF",
126
+ acceptedTypes: ["image/*", "application/pdf"],
127
+ maxFiles: 3,
128
+ });
129
+ });
130
+
131
+ test("coerces a comma-joined acceptedTypes string into an array", () => {
132
+ expect(
133
+ FileUploadSurfaceDataSchema.parse({
134
+ prompt: "p",
135
+ acceptedTypes: "image/*, application/pdf",
136
+ }).acceptedTypes,
137
+ ).toEqual(["image/*", "application/pdf"]);
138
+ });
139
+
140
+ test("wraps a single acceptedTypes string into a one-element array", () => {
141
+ expect(
142
+ FileUploadSurfaceDataSchema.parse({ acceptedTypes: "application/pdf" })
143
+ .acceptedTypes,
144
+ ).toEqual(["application/pdf"]);
145
+ });
146
+
147
+ test("the parsed acceptedTypes always supports .join", () => {
148
+ const parsed = FileUploadSurfaceDataSchema.parse({
149
+ acceptedTypes: "image/*,application/pdf",
150
+ });
151
+ // `.join` is the call the renderer makes on `acceptedTypes`.
152
+ expect(parsed.acceptedTypes?.join(",")).toBe("image/*,application/pdf");
153
+ });
154
+
155
+ test("drops blanks and non-string entries from an array", () => {
156
+ expect(
157
+ FileUploadSurfaceDataSchema.parse({
158
+ acceptedTypes: [" application/pdf ", "", null, 42, {}],
159
+ }).acceptedTypes,
160
+ ).toEqual(["application/pdf", "42"]);
161
+ });
162
+
163
+ test("treats a non-string, non-array acceptedTypes as absent", () => {
164
+ expect(
165
+ FileUploadSurfaceDataSchema.parse({ acceptedTypes: { pdf: true } })
166
+ .acceptedTypes,
167
+ ).toBeUndefined();
168
+ expect(
169
+ FileUploadSurfaceDataSchema.parse({ acceptedTypes: null }).acceptedTypes,
170
+ ).toBeUndefined();
171
+ });
172
+
173
+ test("coerces numeric-string maxFiles and drops invalid numbers", () => {
174
+ const parsed = FileUploadSurfaceDataSchema.parse({
175
+ maxFiles: "2",
176
+ maxSizeBytes: "not-a-number",
177
+ });
178
+ expect(parsed.maxFiles).toBe(2);
179
+ expect(parsed.maxSizeBytes).toBeUndefined();
180
+ });
181
+
182
+ test("never rejects a fully malformed payload", () => {
183
+ expect(
184
+ FileUploadSurfaceDataSchema.safeParse({
185
+ prompt: 5,
186
+ acceptedTypes: 99,
187
+ maxFiles: -1,
188
+ }).success,
189
+ ).toBe(true);
190
+ });
191
+ });
@@ -72,6 +72,8 @@ mock.module("../permissions/checker.js", () => ({
72
72
  }));
73
73
 
74
74
  mock.module("../memory/conversation-crud.js", () => ({
75
+ setConversationProcessingStartedAt: () => {},
76
+ isConversationProcessing: () => false,
75
77
  createConversation: (title: string) => ({ id: "conversation-1", title }),
76
78
  reserveMessage: mock(async () => ({ id: "msg-reserve" })),
77
79
  }));
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Cold-cache guardian voice/phone regression.
3
+ *
4
+ * The sync trust resolver (`resolveActorTrust`) reads the IO-free guardian-
5
+ * delivery cache snapshot (`peekCachedGuardianDelivery`) keyed per channel
6
+ * filter. On a cold process only `vellum` is warmed at daemon startup, so for
7
+ * `phone` the snapshot is empty until some read warms that exact channel key.
8
+ * The voice setup path therefore awaits
9
+ * `getGuardianDeliveryFresh({ channelTypes: ["phone"] })` BEFORE the sync
10
+ * resolve so an inbound guardian call classifies as `guardian` rather than
11
+ * misclassifying during a gateway verdict blip. It reads FRESH because gateway-
12
+ * side binding writes don't invalidate the daemon cache: a stale empty snapshot
13
+ * from an earlier setup would otherwise survive the TTL.
14
+ *
15
+ * This test drives the REAL guardian-delivery reader cache (mocking only the
16
+ * gateway `ipcCall`) so the cold→warm transition is exercised end to end.
17
+ */
18
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
19
+
20
+ const GUARDIAN_PHONE = "+15550100";
21
+
22
+ // Gateway IPC stub: returns the phone guardian delivery. The real reader caches
23
+ // the result under the `phone` key, so a subsequent sync `peek` finds it. When
24
+ // `guardianBound` is false the gateway reports no guardian — simulating the
25
+ // pre-binding state whose empty result the cache would otherwise pin.
26
+ let guardianBound = true;
27
+ let ipcCalls: Array<{ route: string; input: unknown }> = [];
28
+ mock.module("../ipc/gateway-client.js", () => ({
29
+ ipcCall: async (route: string, input: unknown) => {
30
+ ipcCalls.push({ route, input });
31
+ return {
32
+ guardians: guardianBound
33
+ ? [
34
+ {
35
+ channelType: "phone",
36
+ contactId: "guardian-contact",
37
+ principalId: "P_GUARDIAN_COLD",
38
+ address: GUARDIAN_PHONE,
39
+ externalChatId: null,
40
+ status: "active",
41
+ },
42
+ ]
43
+ : [],
44
+ };
45
+ },
46
+ }));
47
+
48
+ // Member lookup is irrelevant to guardian classification (address match on the
49
+ // cached delivery decides it); return null so the member path is a no-op.
50
+ mock.module("../contacts/contact-store.js", () => ({
51
+ findContactByAddress: () => null,
52
+ }));
53
+
54
+ import {
55
+ __resetGuardianDeliveryCacheForTest,
56
+ getGuardianDelivery,
57
+ getGuardianDeliveryFresh,
58
+ peekCachedGuardianDelivery,
59
+ } from "../contacts/guardian-delivery-reader.js";
60
+ import { resolveActorTrust } from "../runtime/actor-trust-resolver.js";
61
+
62
+ describe("voice path warms the phone guardian cache before sync trust", () => {
63
+ beforeEach(() => {
64
+ __resetGuardianDeliveryCacheForTest();
65
+ ipcCalls = [];
66
+ guardianBound = true;
67
+ });
68
+
69
+ test("cold phone cache: guardian call misclassifies until upstream warm", async () => {
70
+ // Precondition: cold cache for phone — the sync peek would miss.
71
+ expect(
72
+ peekCachedGuardianDelivery({ channelTypes: ["phone"] }),
73
+ ).toBeUndefined();
74
+
75
+ // Sync resolve on a cold cache: no guardian snapshot → classified unknown.
76
+ const cold = resolveActorTrust({
77
+ assistantId: "asst-1",
78
+ sourceChannel: "phone",
79
+ conversationExternalId: GUARDIAN_PHONE,
80
+ actorExternalId: GUARDIAN_PHONE,
81
+ });
82
+ expect(cold.trustClass).toBe("unknown");
83
+
84
+ // The voice setup path warms the phone-specific key via the fresh reader.
85
+ await getGuardianDeliveryFresh({ channelTypes: ["phone"] });
86
+ expect(
87
+ ipcCalls.some(
88
+ (c) =>
89
+ c.route === "resolve_guardian_delivery" &&
90
+ JSON.stringify(c.input) ===
91
+ JSON.stringify({ channelTypes: ["phone"] }),
92
+ ),
93
+ ).toBe(true);
94
+
95
+ // The sync resolve, reading the now-warm snapshot, classifies the caller as
96
+ // the guardian — not misclassified as `unknown`.
97
+ const warm = resolveActorTrust({
98
+ assistantId: "asst-1",
99
+ sourceChannel: "phone",
100
+ conversationExternalId: GUARDIAN_PHONE,
101
+ actorExternalId: GUARDIAN_PHONE,
102
+ });
103
+ expect(warm.trustClass).toBe("guardian");
104
+ });
105
+
106
+ test("fresh warm bypasses a stale empty phone snapshot after a gateway-side binding", async () => {
107
+ // An earlier setup cached an empty phone snapshot (no guardian yet). Gateway-
108
+ // side binding writes don't invalidate the daemon cache, so the entry stays
109
+ // warm-but-stale until the TTL.
110
+ guardianBound = false;
111
+ await getGuardianDelivery({ channelTypes: ["phone"] });
112
+ expect(peekCachedGuardianDelivery({ channelTypes: ["phone"] })).toEqual([]);
113
+
114
+ // Guardian binding now exists gateway-side. A non-force read would still
115
+ // return the pinned empty snapshot; the fresh read bypasses it.
116
+ guardianBound = true;
117
+ expect(await getGuardianDelivery({ channelTypes: ["phone"] })).toEqual([]);
118
+ await getGuardianDeliveryFresh({ channelTypes: ["phone"] });
119
+
120
+ // The sync resolve now reads the refreshed snapshot and classifies guardian.
121
+ const warm = resolveActorTrust({
122
+ assistantId: "asst-1",
123
+ sourceChannel: "phone",
124
+ conversationExternalId: GUARDIAN_PHONE,
125
+ actorExternalId: GUARDIAN_PHONE,
126
+ });
127
+ expect(warm.trustClass).toBe("guardian");
128
+ });
129
+
130
+ test("startup vellum-only warm leaves the phone key cold", async () => {
131
+ // Daemon startup warms only `vellum`; that must not populate the `phone` key.
132
+ await getGuardianDelivery({ channelTypes: ["vellum"] });
133
+ expect(
134
+ peekCachedGuardianDelivery({ channelTypes: ["phone"] }),
135
+ ).toBeUndefined();
136
+ });
137
+ });