@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
@@ -7,11 +7,16 @@
7
7
  * persisted, or returned in the outcome.
8
8
  */
9
9
 
10
+ import {
11
+ getInboundTrustVerdict,
12
+ getPhoneCallerVerdict,
13
+ } from "../calls/inbound-trust-reader.js";
10
14
  import type { ChannelId } from "../channels/types.js";
11
15
  import { findContactChannel, getContact } from "../contacts/contact-store.js";
12
- import { upsertContactChannel } from "../contacts/contacts-write.js";
16
+ import { gatewayContactChannelState } from "../contacts/gateway-channel-read.js";
17
+ import { activateMemberChannel } from "../contacts/member-write-relay.js";
18
+ import type { ChannelStatus, ContactChannel } from "../contacts/types.js";
13
19
  import { ipcCallPersistent } from "../ipc/gateway-client.js";
14
- import { getSqlite } from "../memory/db-connection.js";
15
20
  import {
16
21
  findActiveVoiceInvites,
17
22
  findByInviteCodeHash,
@@ -23,9 +28,41 @@ import {
23
28
  import { canonicalizeInboundIdentity } from "../util/canonicalize-identity.js";
24
29
  import { getLogger } from "../util/logger.js";
25
30
  import { hashVoiceCode } from "../util/voice-code.js";
31
+ import { verdictMemberFromVerdict } from "./trust-verdict-consumer.js";
26
32
 
27
33
  const log = getLogger("invite-redemption-service");
28
34
 
35
+ /**
36
+ * Resolve the sender's existing member status for the already_member/blocked
37
+ * gate from the gateway trust verdict. Falls back to the gateway-sourced channel
38
+ * status when the verdict is absent or carries no resolvable member status (e.g.
39
+ * an externalChatId-only match or a resolutionFailed verdict), so a blocked
40
+ * contact can't bypass the gate.
41
+ */
42
+ export async function resolveMemberGateStatus(
43
+ verdict: Awaited<ReturnType<typeof getInboundTrustVerdict>>,
44
+ fallbackStatus: ChannelStatus | null,
45
+ ): Promise<ChannelStatus | null> {
46
+ const memberStatus = verdict
47
+ ? verdictMemberFromVerdict(verdict)?.status
48
+ : null;
49
+ return memberStatus ?? fallbackStatus;
50
+ }
51
+
52
+ /**
53
+ * Gateway-sourced status for an existing local channel, used as the gate-status
54
+ * fallback when the verdict resolves no member. The local row is only located by
55
+ * identity; its status is read from the gateway (ACL source of truth), never the
56
+ * local column.
57
+ */
58
+ async function gatewayFallbackStatus(
59
+ channel: Pick<ContactChannel, "contactId" | "type" | "address"> | null,
60
+ ): Promise<ChannelStatus | null> {
61
+ if (!channel) return null;
62
+ const state = await gatewayContactChannelState(channel);
63
+ return (state?.status as ChannelStatus | undefined) ?? null;
64
+ }
65
+
29
66
  // ---------------------------------------------------------------------------
30
67
  // Gateway lifecycle bridge (shared by all redemption paths)
31
68
  // ---------------------------------------------------------------------------
@@ -218,18 +255,22 @@ export async function redeemInvite(params: {
218
255
  const targetMismatch =
219
256
  existingContact && existingContact.id !== invite.contactId;
220
257
 
221
- if (
222
- existingChannel &&
223
- existingChannel.status === "active" &&
224
- !targetMismatch
225
- ) {
258
+ const gateStatus = await resolveMemberGateStatus(
259
+ await getInboundTrustVerdict({
260
+ channelType: sourceChannel as ChannelId,
261
+ actorExternalId: canonicalUserId,
262
+ }),
263
+ await gatewayFallbackStatus(existingChannel),
264
+ );
265
+
266
+ if (existingChannel && gateStatus === "active" && !targetMismatch) {
226
267
  return { ok: true, type: "already_member", memberId: existingChannel.id };
227
268
  }
228
269
 
229
270
  // Blocked members cannot bypass the guardian's explicit block via invite
230
271
  // links. Return the same generic failure as an invalid token to avoid
231
272
  // leaking membership status to the caller.
232
- if (existingChannel && existingChannel.status === "blocked") {
273
+ if (existingChannel && gateStatus === "blocked") {
233
274
  return { ok: false, reason: "invalid_token" };
234
275
  }
235
276
 
@@ -247,14 +288,10 @@ export async function redeemInvite(params: {
247
288
  return { ok: false, reason: "invalid_token" };
248
289
  }
249
290
 
250
- // Inactive member reactivation: when the user already has a member record
251
- // in a non-active state (revoked/pending), reactivate it via upsertContactChannel
252
- // and consume an invite use atomically. The fresh-member path below also
253
- // uses upsertContactChannel to keep contacts in sync.
291
+ // Inactive member reactivation: when the user already has a member record in
292
+ // a non-active state (revoked/pending), reactivate it gateway-first then
293
+ // consume an invite use. The fresh-member path below mirrors this.
254
294
  if (existingChannel && !targetMismatch) {
255
- // Sentinel error used to trigger a transaction rollback when the invite
256
- // was concurrently revoked/expired between pre-validation and write time.
257
- const STALE_INVITE = Symbol("stale_invite");
258
295
  const canonicalMemberId = existingChannel.address;
259
296
  const canonicalCallerId = externalUserId
260
297
  ? canonicalizeInboundIdentity(sourceChannel as ChannelId, externalUserId)
@@ -266,42 +303,17 @@ export async function redeemInvite(params: {
266
303
  ? existingContact.displayName
267
304
  : displayName;
268
305
 
269
- let reactivated: ReturnType<typeof upsertContactChannel> | undefined;
306
+ // Consume the assistant invite use BEFORE activating the member, so a
307
+ // concurrent revoke/exhaustion (recordInviteUse returns false) leaves no
308
+ // active member behind.
270
309
  try {
271
- getSqlite()
272
- .transaction(() => {
273
- reactivated = upsertContactChannel({
274
- sourceChannel,
275
- externalUserId,
276
- externalChatId,
277
- // Reactivation should not overwrite a guardian-managed nickname.
278
- displayName: preservedDisplayName,
279
- username,
280
- role: "contact",
281
- status: "active",
282
- policy: "allow",
283
- inviteId: invite.id,
284
- verifiedAt: Date.now(),
285
- verifiedVia: "invite",
286
- contactId: invite.contactId,
287
- });
288
-
289
- const recorded = recordInviteUse({
290
- inviteId: invite.id,
291
- externalUserId,
292
- externalChatId,
293
- });
294
-
295
- // If the invite was revoked/expired between pre-validation and this
296
- // write, recordInviteUse returns false — throw to roll back the
297
- // member reactivation so the DB stays consistent.
298
- if (!recorded) throw STALE_INVITE;
299
- })
300
- .immediate();
301
- } catch (err) {
302
- if (err === STALE_INVITE) {
310
+ if (
311
+ !recordInviteUse({ inviteId: invite.id, externalUserId, externalChatId })
312
+ ) {
313
+ // Invite revoked/expired between pre-validation and write.
303
314
  return { ok: false, reason: "invalid_token" };
304
315
  }
316
+ } catch (err) {
305
317
  // Rare: the gateway claim already consumed the row but the assistant
306
318
  // mutation failed — a recoverable wasted gateway use; no cross-process
307
319
  // rollback is attempted.
@@ -312,10 +324,30 @@ export async function redeemInvite(params: {
312
324
  throw err;
313
325
  }
314
326
 
327
+ // Gateway-first: activate the member channel on the authoritative gateway
328
+ // before the assistant DB; the local mirror is best-effort.
329
+ const reactivated = await activateMemberChannel({
330
+ sourceChannel,
331
+ externalUserId,
332
+ externalChatId,
333
+ // Reactivation should not overwrite a guardian-managed nickname.
334
+ displayName: preservedDisplayName,
335
+ username,
336
+ policy: "allow",
337
+ inviteId: invite.id,
338
+ verifiedAt: Date.now(),
339
+ verifiedVia: "invite",
340
+ contactId: invite.contactId,
341
+ });
342
+
343
+ if (reactivated.status === "refused") {
344
+ return { ok: false, reason: "invalid_token" };
345
+ }
346
+
315
347
  return {
316
348
  ok: true,
317
349
  type: "redeemed",
318
- memberId: reactivated!.channel.id,
350
+ memberId: reactivated.memberId,
319
351
  inviteId: invite.id,
320
352
  };
321
353
  }
@@ -332,39 +364,17 @@ export async function redeemInvite(params: {
332
364
  }
333
365
  }
334
366
 
335
- const STALE_INVITE_FRESH = Symbol("stale_invite_fresh");
336
- let freshResult: ReturnType<typeof upsertContactChannel> | undefined;
367
+ // Consume the assistant invite use BEFORE activating the member, so a
368
+ // concurrent revoke/exhaustion (recordInviteUse returns false) leaves no
369
+ // active member behind.
337
370
  try {
338
- getSqlite()
339
- .transaction(() => {
340
- freshResult = upsertContactChannel({
341
- sourceChannel,
342
- externalUserId,
343
- externalChatId,
344
- displayName: freshDisplayName,
345
- username,
346
- role: "contact",
347
- status: "active",
348
- policy: "allow",
349
- inviteId: invite.id,
350
- verifiedAt: Date.now(),
351
- verifiedVia: "invite",
352
- contactId: invite.contactId,
353
- });
354
-
355
- const recorded = recordInviteUse({
356
- inviteId: invite.id,
357
- externalUserId,
358
- externalChatId,
359
- });
360
-
361
- if (!recorded) throw STALE_INVITE_FRESH;
362
- })
363
- .immediate();
364
- } catch (err) {
365
- if (err === STALE_INVITE_FRESH) {
371
+ if (
372
+ !recordInviteUse({ inviteId: invite.id, externalUserId, externalChatId })
373
+ ) {
374
+ // Invite revoked/expired between pre-validation and write.
366
375
  return { ok: false, reason: "invalid_token" };
367
376
  }
377
+ } catch (err) {
368
378
  // Rare: gateway claim succeeded but the assistant mutation failed — a
369
379
  // recoverable wasted gateway use; no cross-process rollback attempted.
370
380
  log.error(
@@ -374,10 +384,29 @@ export async function redeemInvite(params: {
374
384
  throw err;
375
385
  }
376
386
 
387
+ // Gateway-first: activate the member channel on the authoritative gateway
388
+ // before the assistant DB; the local mirror is best-effort.
389
+ const freshResult = await activateMemberChannel({
390
+ sourceChannel,
391
+ externalUserId,
392
+ externalChatId,
393
+ displayName: freshDisplayName,
394
+ username,
395
+ policy: "allow",
396
+ inviteId: invite.id,
397
+ verifiedAt: Date.now(),
398
+ verifiedVia: "invite",
399
+ contactId: invite.contactId,
400
+ });
401
+
402
+ if (freshResult.status === "refused") {
403
+ return { ok: false, reason: "invalid_token" };
404
+ }
405
+
377
406
  return {
378
407
  ok: true,
379
408
  type: "redeemed",
380
- memberId: freshResult!.channel.id,
409
+ memberId: freshResult.memberId,
381
410
  inviteId: invite.id,
382
411
  };
383
412
  }
@@ -465,11 +494,12 @@ export async function redeemVoiceInviteCode(params: {
465
494
  // should bind the sender's identity to the target contact, not the existing one.
466
495
  const targetMismatch = voiceContact && voiceContact.id !== invite.contactId;
467
496
 
468
- if (
469
- existingVoiceChannel &&
470
- existingVoiceChannel.status === "active" &&
471
- !targetMismatch
472
- ) {
497
+ const gateStatus = await resolveMemberGateStatus(
498
+ await getPhoneCallerVerdict(canonicalCallerId),
499
+ await gatewayFallbackStatus(existingVoiceChannel),
500
+ );
501
+
502
+ if (existingVoiceChannel && gateStatus === "active" && !targetMismatch) {
473
503
  return {
474
504
  ok: true,
475
505
  type: "already_member",
@@ -478,7 +508,7 @@ export async function redeemVoiceInviteCode(params: {
478
508
  }
479
509
 
480
510
  // Blocked members cannot bypass the guardian's explicit block
481
- if (existingVoiceChannel && existingVoiceChannel.status === "blocked") {
511
+ if (existingVoiceChannel && gateStatus === "blocked") {
482
512
  return { ok: false, reason: "invalid_or_expired" };
483
513
  }
484
514
 
@@ -496,10 +526,6 @@ export async function redeemVoiceInviteCode(params: {
496
526
  return { ok: false, reason: "invalid_or_expired" };
497
527
  }
498
528
 
499
- // Atomic redemption: upsert member + consume invite use in a transaction
500
- const STALE_INVITE = Symbol("stale_invite");
501
- let memberId: string | undefined;
502
-
503
529
  // When the invite targets a specific contact, preserve the target contact's
504
530
  // guardian-assigned display name if it has one.
505
531
  let preservedDisplayName = voiceContact?.displayName?.trim().length
@@ -512,36 +538,20 @@ export async function redeemVoiceInviteCode(params: {
512
538
  }
513
539
  }
514
540
 
541
+ // Consume the assistant invite use BEFORE activating the member, so a
542
+ // concurrent revoke/exhaustion (recordInviteUse returns false) leaves no
543
+ // active member behind.
515
544
  try {
516
- getSqlite()
517
- .transaction(() => {
518
- const writeResult = upsertContactChannel({
519
- sourceChannel: "phone",
520
- externalUserId: callerExternalUserId,
521
- externalChatId: callerExternalUserId,
522
- displayName: preservedDisplayName,
523
- role: "contact",
524
- status: "active",
525
- policy: "allow",
526
- inviteId: invite.id,
527
- verifiedAt: Date.now(),
528
- verifiedVia: "invite",
529
- contactId: invite.contactId,
530
- });
531
- memberId = writeResult!.channel.id;
532
-
533
- const recorded = recordInviteUse({
534
- inviteId: invite.id,
535
- externalUserId: callerExternalUserId,
536
- });
537
-
538
- if (!recorded) throw STALE_INVITE;
545
+ if (
546
+ !recordInviteUse({
547
+ inviteId: invite.id,
548
+ externalUserId: callerExternalUserId,
539
549
  })
540
- .immediate();
541
- } catch (err) {
542
- if (err === STALE_INVITE) {
550
+ ) {
551
+ // Invite revoked/expired between pre-validation and write.
543
552
  return { ok: false, reason: "invalid_or_expired" };
544
553
  }
554
+ } catch (err) {
545
555
  // Rare: gateway claim succeeded but the assistant mutation failed — a
546
556
  // recoverable wasted gateway use; no cross-process rollback attempted.
547
557
  log.error(
@@ -551,10 +561,28 @@ export async function redeemVoiceInviteCode(params: {
551
561
  throw err;
552
562
  }
553
563
 
564
+ // Gateway-first: activate the member channel on the authoritative gateway
565
+ // before the assistant DB; the local mirror is best-effort.
566
+ const writeResult = await activateMemberChannel({
567
+ sourceChannel: "phone",
568
+ externalUserId: callerExternalUserId,
569
+ externalChatId: callerExternalUserId,
570
+ displayName: preservedDisplayName,
571
+ policy: "allow",
572
+ inviteId: invite.id,
573
+ verifiedAt: Date.now(),
574
+ verifiedVia: "invite",
575
+ contactId: invite.contactId,
576
+ });
577
+
578
+ if (writeResult.status === "refused") {
579
+ return { ok: false, reason: "invalid_or_expired" };
580
+ }
581
+
554
582
  return {
555
583
  ok: true,
556
584
  type: "redeemed",
557
- memberId: memberId!,
585
+ memberId: writeResult.memberId,
558
586
  inviteId: invite.id,
559
587
  };
560
588
  }
@@ -639,18 +667,22 @@ export async function redeemInviteByCode(params: {
639
667
  const targetMismatch =
640
668
  existingContact && existingContact.id !== invite.contactId;
641
669
 
642
- if (
643
- existingChannel &&
644
- existingChannel.status === "active" &&
645
- !targetMismatch
646
- ) {
670
+ const gateStatus = await resolveMemberGateStatus(
671
+ await getInboundTrustVerdict({
672
+ channelType: sourceChannel as ChannelId,
673
+ actorExternalId: canonicalUserId,
674
+ }),
675
+ await gatewayFallbackStatus(existingChannel),
676
+ );
677
+
678
+ if (existingChannel && gateStatus === "active" && !targetMismatch) {
647
679
  return { ok: true, type: "already_member", memberId: existingChannel.id };
648
680
  }
649
681
 
650
682
  // Blocked members cannot bypass the guardian's explicit block via invite
651
683
  // codes. Return the same generic failure as an invalid token to avoid
652
684
  // leaking membership status to the caller.
653
- if (existingChannel && existingChannel.status === "blocked") {
685
+ if (existingChannel && gateStatus === "blocked") {
654
686
  return { ok: false, reason: "invalid_token" };
655
687
  }
656
688
 
@@ -667,10 +699,9 @@ export async function redeemInviteByCode(params: {
667
699
  return { ok: false, reason: "invalid_token" };
668
700
  }
669
701
 
670
- // Inactive member reactivation: reactivate via upsertContactChannel and consume
671
- // an invite use atomically.
702
+ // Inactive member reactivation: reactivate gateway-first, then consume an
703
+ // invite use.
672
704
  if (existingChannel && !targetMismatch) {
673
- const STALE_INVITE_REACTIVATE = Symbol("stale_invite_reactivate");
674
705
  const canonicalMemberId = existingChannel.address;
675
706
  const canonicalCallerId = externalUserId
676
707
  ? canonicalizeInboundIdentity(sourceChannel as ChannelId, externalUserId)
@@ -682,38 +713,17 @@ export async function redeemInviteByCode(params: {
682
713
  ? existingContact.displayName
683
714
  : displayName;
684
715
 
685
- let reactivated: ReturnType<typeof upsertContactChannel> | undefined;
716
+ // Consume the assistant invite use BEFORE activating the member, so a
717
+ // concurrent revoke/exhaustion (recordInviteUse returns false) leaves no
718
+ // active member behind.
686
719
  try {
687
- getSqlite()
688
- .transaction(() => {
689
- reactivated = upsertContactChannel({
690
- sourceChannel,
691
- externalUserId,
692
- externalChatId,
693
- displayName: preservedDisplayName,
694
- username,
695
- role: "contact",
696
- status: "active",
697
- policy: "allow",
698
- inviteId: invite.id,
699
- verifiedAt: Date.now(),
700
- verifiedVia: "invite",
701
- contactId: invite.contactId,
702
- });
703
-
704
- const recorded = recordInviteUse({
705
- inviteId: invite.id,
706
- externalUserId,
707
- externalChatId,
708
- });
709
-
710
- if (!recorded) throw STALE_INVITE_REACTIVATE;
711
- })
712
- .immediate();
713
- } catch (err) {
714
- if (err === STALE_INVITE_REACTIVATE) {
720
+ if (
721
+ !recordInviteUse({ inviteId: invite.id, externalUserId, externalChatId })
722
+ ) {
723
+ // Invite revoked/expired between pre-validation and write.
715
724
  return { ok: false, reason: "invalid_token" };
716
725
  }
726
+ } catch (err) {
717
727
  // Rare: gateway claim succeeded but the assistant mutation failed — a
718
728
  // recoverable wasted gateway use; no cross-process rollback attempted.
719
729
  log.error(
@@ -723,10 +733,29 @@ export async function redeemInviteByCode(params: {
723
733
  throw err;
724
734
  }
725
735
 
736
+ // Gateway-first: activate the member channel on the authoritative gateway
737
+ // before the assistant DB; the local mirror is best-effort.
738
+ const reactivated = await activateMemberChannel({
739
+ sourceChannel,
740
+ externalUserId,
741
+ externalChatId,
742
+ displayName: preservedDisplayName,
743
+ username,
744
+ policy: "allow",
745
+ inviteId: invite.id,
746
+ verifiedAt: Date.now(),
747
+ verifiedVia: "invite",
748
+ contactId: invite.contactId,
749
+ });
750
+
751
+ if (reactivated.status === "refused") {
752
+ return { ok: false, reason: "invalid_token" };
753
+ }
754
+
726
755
  return {
727
756
  ok: true,
728
757
  type: "redeemed",
729
- memberId: reactivated!.channel.id,
758
+ memberId: reactivated.memberId,
730
759
  inviteId: invite.id,
731
760
  };
732
761
  }
@@ -743,39 +772,17 @@ export async function redeemInviteByCode(params: {
743
772
  }
744
773
  }
745
774
 
746
- const STALE_INVITE_FRESH = Symbol("stale_invite_fresh");
747
- let freshResult: ReturnType<typeof upsertContactChannel> | undefined;
775
+ // Consume the assistant invite use BEFORE activating the member, so a
776
+ // concurrent revoke/exhaustion (recordInviteUse returns false) leaves no
777
+ // active member behind.
748
778
  try {
749
- getSqlite()
750
- .transaction(() => {
751
- freshResult = upsertContactChannel({
752
- sourceChannel,
753
- externalUserId,
754
- externalChatId,
755
- displayName: freshDisplayName,
756
- username,
757
- role: "contact",
758
- status: "active",
759
- policy: "allow",
760
- inviteId: invite.id,
761
- verifiedAt: Date.now(),
762
- verifiedVia: "invite",
763
- contactId: invite.contactId,
764
- });
765
-
766
- const recorded = recordInviteUse({
767
- inviteId: invite.id,
768
- externalUserId,
769
- externalChatId,
770
- });
771
-
772
- if (!recorded) throw STALE_INVITE_FRESH;
773
- })
774
- .immediate();
775
- } catch (err) {
776
- if (err === STALE_INVITE_FRESH) {
779
+ if (
780
+ !recordInviteUse({ inviteId: invite.id, externalUserId, externalChatId })
781
+ ) {
782
+ // Invite revoked/expired between pre-validation and write.
777
783
  return { ok: false, reason: "invalid_token" };
778
784
  }
785
+ } catch (err) {
779
786
  // Rare: gateway claim succeeded but the assistant mutation failed — a
780
787
  // recoverable wasted gateway use; no cross-process rollback attempted.
781
788
  log.error(
@@ -785,10 +792,29 @@ export async function redeemInviteByCode(params: {
785
792
  throw err;
786
793
  }
787
794
 
795
+ // Gateway-first: activate the member channel on the authoritative gateway
796
+ // before the assistant DB; the local mirror is best-effort.
797
+ const freshResult = await activateMemberChannel({
798
+ sourceChannel,
799
+ externalUserId,
800
+ externalChatId,
801
+ displayName: freshDisplayName,
802
+ username,
803
+ policy: "allow",
804
+ inviteId: invite.id,
805
+ verifiedAt: Date.now(),
806
+ verifiedVia: "invite",
807
+ contactId: invite.contactId,
808
+ });
809
+
810
+ if (freshResult.status === "refused") {
811
+ return { ok: false, reason: "invalid_token" };
812
+ }
813
+
788
814
  return {
789
815
  ok: true,
790
816
  type: "redeemed",
791
- memberId: freshResult!.channel.id,
817
+ memberId: freshResult.memberId,
792
818
  inviteId: invite.id,
793
819
  };
794
820
  }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Tests for the local-actor-identity cache-warm path.
3
+ *
4
+ * The SSE eager-subscribe path resolves the local actor principal
5
+ * synchronously from the IO-free guardian-delivery cache snapshot. A cold
6
+ * cache returns undefined, so the daemon warms it at startup
7
+ * (`warmLocalGuardianPrincipalCache`) before clients register. These tests pin
8
+ * that the sync read is cold before the warm and resolves the gateway-owned
9
+ * principal after it.
10
+ */
11
+
12
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
13
+
14
+ import type { GuardianDelivery } from "@vellumai/gateway-client";
15
+
16
+ // ── Controllable IPC mock ──────────────────────────────────────────────────
17
+
18
+ type IpcHandler = (params?: Record<string, unknown>) => unknown;
19
+ const ipcHandlers = new Map<string, IpcHandler>();
20
+
21
+ mock.module("../ipc/gateway-client.js", () => ({
22
+ ipcCall: async (method: string, params?: Record<string, unknown>) => {
23
+ const handler = ipcHandlers.get(method);
24
+ return handler ? handler(params) : undefined;
25
+ },
26
+ ipcCallPersistent: async () => undefined,
27
+ resetPersistentClient: () => {},
28
+ }));
29
+
30
+ let httpAuthDisabled = false;
31
+ mock.module("../config/env.js", () => ({
32
+ isHttpAuthDisabled: () => httpAuthDisabled,
33
+ }));
34
+
35
+ import { __resetGuardianDeliveryCacheForTest } from "../contacts/guardian-delivery-reader.js";
36
+ import {
37
+ findLocalGuardianPrincipalIdFromStore,
38
+ resolveActorPrincipalIdForLocalGuardianSync,
39
+ warmLocalGuardianPrincipalCache,
40
+ } from "./local-actor-identity.js";
41
+
42
+ const METHOD = "resolve_guardian_delivery";
43
+
44
+ const vellumGuardian: GuardianDelivery = {
45
+ channelType: "vellum",
46
+ contactId: "contact-1",
47
+ address: "self",
48
+ status: "active",
49
+ principalId: "principal-abc",
50
+ };
51
+
52
+ describe("warmLocalGuardianPrincipalCache", () => {
53
+ beforeEach(() => {
54
+ __resetGuardianDeliveryCacheForTest();
55
+ ipcHandlers.clear();
56
+ httpAuthDisabled = false;
57
+ });
58
+
59
+ afterEach(() => {
60
+ __resetGuardianDeliveryCacheForTest();
61
+ });
62
+
63
+ test("sync read is cold before warming", () => {
64
+ ipcHandlers.set(METHOD, () => ({ guardians: [vellumGuardian] }));
65
+
66
+ // No warm yet — the cache snapshot is empty.
67
+ expect(findLocalGuardianPrincipalIdFromStore()).toBeUndefined();
68
+ });
69
+
70
+ test("warming populates the cache for the sync read", async () => {
71
+ ipcHandlers.set(METHOD, () => ({ guardians: [vellumGuardian] }));
72
+
73
+ await warmLocalGuardianPrincipalCache();
74
+
75
+ expect(findLocalGuardianPrincipalIdFromStore()).toBe("principal-abc");
76
+ });
77
+
78
+ test("cold-start SSE registration resolves the principal after warm", async () => {
79
+ httpAuthDisabled = true;
80
+ ipcHandlers.set(METHOD, () => ({ guardians: [vellumGuardian] }));
81
+
82
+ // Cold cache: dev-bypass header resolves to no principal.
83
+ expect(
84
+ resolveActorPrincipalIdForLocalGuardianSync("dev-bypass"),
85
+ ).toBeUndefined();
86
+
87
+ await warmLocalGuardianPrincipalCache();
88
+
89
+ // Warmed: the SSE sync path now resolves the gateway-owned principal.
90
+ expect(resolveActorPrincipalIdForLocalGuardianSync("dev-bypass")).toBe(
91
+ "principal-abc",
92
+ );
93
+ });
94
+
95
+ test("warm tolerates an unreachable gateway without caching a failure", async () => {
96
+ ipcHandlers.set(METHOD, () => {
97
+ throw new Error("gateway down");
98
+ });
99
+
100
+ await warmLocalGuardianPrincipalCache();
101
+
102
+ // Failure not cached; a later successful read warms the cache.
103
+ expect(findLocalGuardianPrincipalIdFromStore()).toBeUndefined();
104
+ ipcHandlers.set(METHOD, () => ({ guardians: [vellumGuardian] }));
105
+ await warmLocalGuardianPrincipalCache();
106
+ expect(findLocalGuardianPrincipalIdFromStore()).toBe("principal-abc");
107
+ });
108
+ });