@vellumai/assistant 0.10.2-dev.202606250318.5e7cfb0 → 0.10.2

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 (430) hide show
  1. package/bun.lock +0 -20
  2. package/docs/workspace-tools.md +33 -42
  3. package/eslint-rules/cli-no-daemon-internals.js +0 -6
  4. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +0 -31
  5. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +0 -44
  6. package/node_modules/@vellumai/gateway-client/src/index.ts +0 -14
  7. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +0 -17
  8. package/node_modules/@vellumai/service-contracts/package.json +0 -1
  9. package/node_modules/@vellumai/service-contracts/src/index.ts +0 -1
  10. package/openapi.yaml +0 -155
  11. package/package.json +1 -4
  12. package/scripts/test.sh +15 -36
  13. package/src/__tests__/actor-token-service.test.ts +14 -36
  14. package/src/__tests__/agent-loop-override-profile.test.ts +0 -1
  15. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +0 -2
  16. package/src/__tests__/agent-wake-override-profile.test.ts +0 -2
  17. package/src/__tests__/annotate-activity-metadata.test.ts +0 -2
  18. package/src/__tests__/annotate-risk-options.test.ts +0 -2
  19. package/src/__tests__/approval-cascade.test.ts +0 -2
  20. package/src/__tests__/assistant-attachments.test.ts +0 -42
  21. package/src/__tests__/background-workers-disk-pressure.test.ts +0 -2
  22. package/src/__tests__/btw-routes.test.ts +0 -2
  23. package/src/__tests__/build-persisted-content.test.ts +0 -2
  24. package/src/__tests__/call-controller.test.ts +0 -19
  25. package/src/__tests__/channel-guardian.test.ts +58 -94
  26. package/src/__tests__/channel-reply-delivery.test.ts +0 -2
  27. package/src/__tests__/compaction-events.test.ts +0 -2
  28. package/src/__tests__/compaction.benchmark.test.ts +0 -2
  29. package/src/__tests__/compactor-call-site-logging.test.ts +0 -2
  30. package/src/__tests__/compactor-low-watermark-cut.test.ts +0 -2
  31. package/src/__tests__/compactor-preserved-tail-count.test.ts +0 -2
  32. package/src/__tests__/compactor-summary-call-truncation.test.ts +0 -2
  33. package/src/__tests__/compactor-web-search-strip.test.ts +0 -2
  34. package/src/__tests__/config-loader-backfill.test.ts +10 -123
  35. package/src/__tests__/config-schema.test.ts +0 -1
  36. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +29 -31
  37. package/src/__tests__/contacts-relay-reads.test.ts +15 -13
  38. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -2
  39. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +0 -2
  40. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +0 -2
  41. package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -2
  42. package/src/__tests__/conversation-agent-loop.test.ts +0 -134
  43. package/src/__tests__/conversation-analysis-routes.test.ts +0 -2
  44. package/src/__tests__/conversation-app-control-lifecycle.test.ts +0 -2
  45. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -2
  46. package/src/__tests__/conversation-history-web-search.test.ts +0 -2
  47. package/src/__tests__/conversation-load-history-repair.test.ts +0 -2
  48. package/src/__tests__/conversation-load-history-stripped.test.ts +0 -2
  49. package/src/__tests__/conversation-pairing.test.ts +0 -2
  50. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +0 -2
  51. package/src/__tests__/conversation-process-callsite.test.ts +0 -2
  52. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -2
  53. package/src/__tests__/conversation-queue.test.ts +0 -91
  54. package/src/__tests__/conversation-routes-guardian-reply.test.ts +0 -14
  55. package/src/__tests__/conversation-routes-slash-commands.test.ts +0 -14
  56. package/src/__tests__/conversation-slash-queue.test.ts +0 -2
  57. package/src/__tests__/conversation-slash-unknown.test.ts +0 -2
  58. package/src/__tests__/conversation-speed-override.test.ts +0 -2
  59. package/src/__tests__/conversation-surfaces-task-progress.test.ts +0 -29
  60. package/src/__tests__/conversation-title-service.test.ts +0 -2
  61. package/src/__tests__/conversation-tool-setup-attribution.test.ts +0 -47
  62. package/src/__tests__/conversation-usage.test.ts +0 -2
  63. package/src/__tests__/conversation-workspace-cache-state.test.ts +0 -2
  64. package/src/__tests__/conversation-workspace-injection.test.ts +0 -2
  65. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -2
  66. package/src/__tests__/credential-security-invariants.test.ts +1 -1
  67. package/src/__tests__/db-migration-rollback.test.ts +171 -205
  68. package/src/__tests__/db-test-helpers.ts +4 -5
  69. package/src/__tests__/deterministic-verification-control-plane.test.ts +2 -4
  70. package/src/__tests__/disk-pressure-guard.test.ts +0 -41
  71. package/src/__tests__/dm-persistence.test.ts +0 -2
  72. package/src/__tests__/emit-signal-routing-intent.test.ts +5 -10
  73. package/src/__tests__/events-dev-bypass-actor.test.ts +1 -7
  74. package/src/__tests__/exploration-drift-hook.test.ts +2 -3
  75. package/src/__tests__/filing-service.test.ts +0 -2
  76. package/src/__tests__/guardian-binding-drift-heal.test.ts +10 -75
  77. package/src/__tests__/guardian-dispatch.test.ts +1 -95
  78. package/src/__tests__/guardian-outbound-http.test.ts +0 -13
  79. package/src/__tests__/heartbeat-disk-pressure.test.ts +0 -2
  80. package/src/__tests__/heartbeat-service.test.ts +0 -2
  81. package/src/__tests__/helpers/channel-test-adapter.ts +7 -1
  82. package/src/__tests__/host-app-control-routes.test.ts +30 -24
  83. package/src/__tests__/host-bash-routes.test.ts +41 -31
  84. package/src/__tests__/host-browser-routes.test.ts +32 -26
  85. package/src/__tests__/host-cu-routes-targeted.test.ts +33 -25
  86. package/src/__tests__/host-file-routes-targeted.test.ts +52 -40
  87. package/src/__tests__/host-transfer-routes-targeted.test.ts +43 -31
  88. package/src/__tests__/http-user-message-parity.test.ts +8 -290
  89. package/src/__tests__/inbound-invite-redemption.test.ts +0 -28
  90. package/src/__tests__/inbound-slack-persistence.test.ts +0 -2
  91. package/src/__tests__/invite-redemption-service.test.ts +0 -198
  92. package/src/__tests__/llm-context-normalization.test.ts +0 -105
  93. package/src/__tests__/llm-request-log-error-payload.test.ts +9 -71
  94. package/src/__tests__/llm-usage-store.test.ts +0 -25
  95. package/src/__tests__/mcp-health-check.test.ts +1 -2
  96. package/src/__tests__/media-stream-server-integration.test.ts +0 -127
  97. package/src/__tests__/memory-retrieval-hook.test.ts +0 -2
  98. package/src/__tests__/messaging-send-tool.test.ts +0 -2
  99. package/src/__tests__/migration-import-from-url.test.ts +2 -2
  100. package/src/__tests__/mtime-cache.test.ts +5 -146
  101. package/src/__tests__/native-web-search.test.ts +0 -2
  102. package/src/__tests__/non-member-access-request.test.ts +17 -189
  103. package/src/__tests__/notification-broadcaster.test.ts +0 -4
  104. package/src/__tests__/notification-decision-recipient-context.test.ts +32 -33
  105. package/src/__tests__/notification-deep-link.test.ts +0 -6
  106. package/src/__tests__/notification-guardian-path.test.ts +0 -19
  107. package/src/__tests__/openai-provider.test.ts +12 -22
  108. package/src/__tests__/openai-responses-provider.test.ts +2 -12
  109. package/src/__tests__/outbound-slack-persistence.test.ts +0 -2
  110. package/src/__tests__/pending-interactions-resolved-event.test.ts +4 -7
  111. package/src/__tests__/persistence-secret-redaction.test.ts +0 -2
  112. package/src/__tests__/plugin-bootstrap.test.ts +73 -3
  113. package/src/__tests__/plugin-route-contribution.test.ts +17 -4
  114. package/src/__tests__/plugin-tool-contribution.test.ts +18 -3
  115. package/src/__tests__/plugin-types.test.ts +2 -0
  116. package/src/__tests__/process-message-background-slack.test.ts +0 -2
  117. package/src/__tests__/process-message-display-content.test.ts +0 -2
  118. package/src/__tests__/provider-error-scenarios.test.ts +4 -5
  119. package/src/__tests__/provider-usage-tracking.test.ts +0 -39
  120. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +0 -2
  121. package/src/__tests__/registry.test.ts +1 -4
  122. package/src/__tests__/relay-server.test.ts +25 -694
  123. package/src/__tests__/runtime-attachment-metadata.test.ts +1 -0
  124. package/src/__tests__/secret-ingress-http.test.ts +0 -14
  125. package/src/__tests__/send-endpoint-busy.test.ts +8 -30
  126. package/src/__tests__/skills.test.ts +0 -44
  127. package/src/__tests__/slack-inbound-verification.test.ts +2 -47
  128. package/src/__tests__/stt-hints.test.ts +13 -44
  129. package/src/__tests__/subagent-detail.test.ts +0 -27
  130. package/src/__tests__/subagent-disposal.test.ts +0 -65
  131. package/src/__tests__/subagent-notify-parent.test.ts +0 -2
  132. package/src/__tests__/subagent-role-registry.test.ts +2 -7
  133. package/src/__tests__/subagent-spawn-tool-fork.test.ts +0 -2
  134. package/src/__tests__/subagent-tools.test.ts +0 -2
  135. package/src/__tests__/suggestion-routes.test.ts +0 -2
  136. package/src/__tests__/title-generate-hook.test.ts +0 -2
  137. package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -2
  138. package/src/__tests__/tool-executor.test.ts +11 -16
  139. package/src/__tests__/tool-preview-lifecycle.test.ts +0 -2
  140. package/src/__tests__/tool-result-metadata-plumbing.test.ts +0 -2
  141. package/src/__tests__/tool-start-timestamp.test.ts +0 -2
  142. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +10 -10
  143. package/src/__tests__/twilio-routes.test.ts +0 -96
  144. package/src/__tests__/ui-file-upload-surface.test.ts +0 -86
  145. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  146. package/src/__tests__/voice-invite-redemption.test.ts +0 -33
  147. package/src/__tests__/web-search-backend-failure.test.ts +0 -2
  148. package/src/__tests__/workspace-migration-remove-hooks.test.ts +35 -14
  149. package/src/__tests__/workspace-tool-loader.test.ts +2 -195
  150. package/src/__tests__/workspace-tools-watcher-flag.test.ts +70 -0
  151. package/src/agent/loop.ts +0 -56
  152. package/src/api/index.ts +1 -19
  153. package/src/api/responses/llm-request-log-entry.ts +0 -29
  154. package/src/api/responses/subagent-detail.ts +0 -17
  155. package/src/api/surfaces.ts +3 -39
  156. package/src/approvals/guardian-request-resolvers.ts +11 -1
  157. package/src/calls/__tests__/relay-setup-router.test.ts +4 -262
  158. package/src/calls/call-domain.ts +3 -3
  159. package/src/calls/guardian-dispatch.ts +8 -10
  160. package/src/calls/inbound-trust-reader.ts +1 -17
  161. package/src/calls/media-stream-server.ts +0 -21
  162. package/src/calls/relay-server.ts +50 -167
  163. package/src/calls/relay-setup-router.ts +7 -37
  164. package/src/calls/relay-verification.ts +4 -4
  165. package/src/calls/stt-hints.ts +12 -9
  166. package/src/calls/twilio-routes.ts +4 -14
  167. package/src/channels/types.ts +20 -10
  168. package/src/cli/commands/__tests__/cache.test.ts +1 -8
  169. package/src/cli/commands/cache.ts +181 -194
  170. package/src/cli/commands/db/__tests__/repair.test.ts +5 -6
  171. package/src/cli/commands/db/status.ts +1 -37
  172. package/src/cli/commands/mcp.ts +218 -252
  173. package/src/cli/commands/memory/index.ts +0 -2
  174. package/src/cli/commands/plugins.ts +3 -75
  175. package/src/cli/lib/__tests__/install-from-github.test.ts +0 -102
  176. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +1 -160
  177. package/src/cli/lib/list-installed-plugins.ts +1 -179
  178. package/src/config/__tests__/sync-gated-profiles.test.ts +3 -11
  179. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +17 -27
  180. package/src/config/bundled-skills/contacts/tools/contact-search.ts +3 -13
  181. package/src/config/bundled-skills/subagent/SKILL.md +1 -1
  182. package/src/config/bundled-skills/subagent/TOOLS.json +1 -1
  183. package/src/config/feature-flag-registry.json +13 -5
  184. package/src/config/loader.ts +5 -38
  185. package/src/config/schemas/__tests__/memory-v3.test.ts +0 -1
  186. package/src/config/schemas/memory-lifecycle.ts +0 -12
  187. package/src/config/schemas/memory-v3.ts +0 -7
  188. package/src/config/schemas/memory.ts +0 -4
  189. package/src/config/schemas/timeouts.ts +0 -8
  190. package/src/config/seed-inference-profiles.ts +11 -21
  191. package/src/config/skills.ts +5 -27
  192. package/src/config/sync-gated-profiles.ts +13 -12
  193. package/src/contacts/contacts-write.ts +0 -3
  194. package/src/daemon/assistant-attachments.ts +4 -27
  195. package/src/daemon/conversation-agent-loop.ts +0 -28
  196. package/src/daemon/conversation-process.ts +16 -35
  197. package/src/daemon/conversation-surfaces.ts +38 -111
  198. package/src/daemon/conversation-tool-setup.ts +16 -50
  199. package/src/daemon/conversation.ts +1 -13
  200. package/src/daemon/disk-pressure-guard.ts +2 -12
  201. package/src/daemon/event-loop-watchdog.ts +1 -28
  202. package/src/daemon/external-plugins-bootstrap.ts +34 -4
  203. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -25
  204. package/src/daemon/handlers/config-a2a.ts +14 -6
  205. package/src/daemon/handlers/config-channels.ts +22 -78
  206. package/src/daemon/handlers/conversations.ts +0 -77
  207. package/src/daemon/lifecycle.ts +0 -4
  208. package/src/daemon/mcp-reload-service.ts +0 -10
  209. package/src/daemon/memory-v2-startup.test.ts +0 -72
  210. package/src/daemon/memory-v2-startup.ts +19 -87
  211. package/src/daemon/message-types/conversations.ts +0 -2
  212. package/src/daemon/message-types/surfaces.ts +12 -12
  213. package/src/daemon/server.ts +4 -0
  214. package/src/daemon/shutdown-handlers.ts +0 -20
  215. package/src/daemon/tool-setup-types.ts +0 -9
  216. package/src/daemon/workspace-tools-watcher.ts +328 -0
  217. package/src/ipc/__tests__/clients-list-ipc.test.ts +1 -1
  218. package/src/ipc/assistant-server.ts +2 -2
  219. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
  220. package/src/mcp/client.ts +1 -15
  221. package/src/mcp/mcp-auth-orchestrator.ts +1 -6
  222. package/src/mcp/mcp-oauth-provider.ts +8 -19
  223. package/src/memory/__tests__/memory-retrospective-job.test.ts +0 -8
  224. package/src/memory/conversation-crud.ts +0 -38
  225. package/src/memory/db-connection.ts +3 -22
  226. package/src/memory/db-init.ts +502 -36
  227. package/src/memory/db-singleton.ts +4 -6
  228. package/src/memory/jobs-worker.ts +0 -58
  229. package/src/memory/llm-request-log-store.ts +1 -26
  230. package/src/memory/llm-usage-store.ts +20 -48
  231. package/src/memory/memory-retrospective-job.ts +8 -9
  232. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +56 -130
  233. package/src/memory/migrations/__tests__/run-migrations.test.ts +2 -2
  234. package/src/memory/migrations/registry.ts +573 -0
  235. package/src/memory/migrations/run-migrations.ts +6 -90
  236. package/src/memory/migrations/validate-migration-state.ts +66 -101
  237. package/src/memory/schema/conversations.ts +0 -9
  238. package/src/memory/schema/infrastructure.ts +0 -20
  239. package/src/memory/v2/__tests__/cli-command-store.test.ts +0 -25
  240. package/src/memory/v2/__tests__/skill-store.test.ts +0 -80
  241. package/src/memory/v2/cli-command-store.ts +38 -75
  242. package/src/memory/v2/prompts/consolidation.ts +82 -13
  243. package/src/memory/v2/prompts/router.ts +93 -21
  244. package/src/memory/v2/skill-store.ts +31 -68
  245. package/src/notifications/__tests__/broadcaster.test.ts +8 -16
  246. package/src/notifications/__tests__/decision-engine.test.ts +9 -78
  247. package/src/notifications/broadcaster.ts +1 -8
  248. package/src/notifications/decision-engine.ts +7 -15
  249. package/src/notifications/destination-resolver.ts +24 -68
  250. package/src/notifications/emit-signal.ts +14 -39
  251. package/src/permissions/question-prompter.test.ts +1 -1
  252. package/src/permissions/question-prompter.ts +4 -7
  253. package/src/plugin-api/index.ts +6 -6
  254. package/src/plugin-api/types.ts +5 -3
  255. package/src/plugin-api/vision-support.test.ts +4 -28
  256. package/src/plugin-api/vision-support.ts +31 -66
  257. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +0 -161
  258. package/src/plugins/defaults/advisor/consult.ts +6 -110
  259. package/src/plugins/defaults/advisor/steering.ts +2 -14
  260. package/src/plugins/defaults/advisor/tools/advisor.ts +5 -32
  261. package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +1 -2
  262. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +7 -47
  263. package/src/plugins/defaults/image-fallback/hooks/post-tool-use.ts +11 -10
  264. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +20 -12
  265. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +11 -42
  266. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +3 -33
  267. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +4 -48
  268. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +8 -4
  269. package/src/plugins/defaults/memory-v3-shadow/injector.ts +15 -43
  270. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +2 -11
  271. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +13 -77
  272. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +11 -12
  273. package/src/plugins/mtime-cache.ts +291 -76
  274. package/src/plugins/pipeline.ts +13 -111
  275. package/src/plugins/types.ts +2 -0
  276. package/src/providers/anthropic/client.ts +0 -5
  277. package/src/providers/call-site-routing.ts +0 -4
  278. package/src/providers/model-catalog.ts +0 -16
  279. package/src/providers/openai/__tests__/api-error-detail.test.ts +120 -0
  280. package/src/providers/openai/chat-completions-provider.ts +83 -37
  281. package/src/providers/openai/responses-provider.ts +46 -50
  282. package/src/providers/openrouter/client.ts +0 -5
  283. package/src/providers/provider-send-message.ts +0 -4
  284. package/src/providers/ratelimit.ts +0 -4
  285. package/src/providers/retry.ts +0 -4
  286. package/src/providers/types.ts +0 -9
  287. package/src/providers/usage-tracking.ts +0 -4
  288. package/src/runtime/__tests__/trust-verdict-consumer.test.ts +3 -335
  289. package/src/runtime/access-request-helper.ts +39 -19
  290. package/src/runtime/actor-trust-resolver.ts +2 -2
  291. package/src/runtime/assistant-event-hub.ts +1 -1
  292. package/src/runtime/assistant-stream-state.ts +2 -9
  293. package/src/runtime/auth/require-bound-guardian.ts +11 -21
  294. package/src/runtime/channel-verification-service.ts +31 -56
  295. package/src/runtime/confirmation-request-guardian-bridge.ts +3 -3
  296. package/src/runtime/guardian-vellum-migration.ts +7 -66
  297. package/src/runtime/invite-redemption-service.ts +187 -198
  298. package/src/runtime/local-actor-identity.ts +11 -76
  299. package/src/runtime/pending-interactions.ts +1 -11
  300. package/src/runtime/routes/__tests__/channel-verification-revoke.test.ts +5 -56
  301. package/src/runtime/routes/__tests__/channel-verification-routes.test.ts +1 -1
  302. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +0 -187
  303. package/src/runtime/routes/browser-routes.ts +1 -1
  304. package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +5 -13
  305. package/src/runtime/routes/channel-verification-routes.ts +3 -3
  306. package/src/runtime/routes/contact-routes.ts +32 -8
  307. package/src/runtime/routes/conversation-cli-routes.ts +5 -4
  308. package/src/runtime/routes/conversation-list-routes.ts +7 -4
  309. package/src/runtime/routes/conversation-query-routes.ts +0 -72
  310. package/src/runtime/routes/conversation-routes.ts +85 -84
  311. package/src/runtime/routes/events-routes.ts +2 -2
  312. package/src/runtime/routes/global-search-routes.ts +1 -3
  313. package/src/runtime/routes/guardian-action-routes.ts +5 -4
  314. package/src/runtime/routes/host-app-control-routes.ts +4 -5
  315. package/src/runtime/routes/host-bash-routes.ts +4 -5
  316. package/src/runtime/routes/host-browser-routes.ts +11 -9
  317. package/src/runtime/routes/host-cu-routes.ts +4 -5
  318. package/src/runtime/routes/host-file-routes.ts +4 -5
  319. package/src/runtime/routes/host-transfer-routes.ts +6 -6
  320. package/src/runtime/routes/http-adapter.ts +1 -1
  321. package/src/runtime/routes/identity-routes.ts +2 -3
  322. package/src/runtime/routes/inbound-message-handler.ts +5 -5
  323. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +5 -97
  324. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +49 -61
  325. package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -16
  326. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +7 -7
  327. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +8 -21
  328. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +3 -14
  329. package/src/runtime/routes/index.ts +0 -2
  330. package/src/runtime/routes/llm-context-normalization.ts +0 -83
  331. package/src/runtime/routes/mcp-auth-routes.ts +19 -171
  332. package/src/runtime/routes/migration-rollback-routes.ts +3 -4
  333. package/src/runtime/routes/migration-routes.ts +1 -4
  334. package/src/runtime/routes/subagents-routes.ts +0 -5
  335. package/src/runtime/routes/surface-action-routes.ts +56 -42
  336. package/src/runtime/services/__tests__/conversation-serializer.test.ts +0 -1
  337. package/src/runtime/services/conversation-serializer.ts +9 -7
  338. package/src/runtime/tool-grant-request-helper.ts +3 -3
  339. package/src/runtime/trust-verdict-consumer.ts +9 -85
  340. package/src/runtime/verification-outbound-actions.ts +18 -18
  341. package/src/signals/user-message.ts +0 -16
  342. package/src/subagent/manager.ts +0 -9
  343. package/src/subagent/types.ts +3 -3
  344. package/src/telemetry/types.ts +1 -34
  345. package/src/telemetry/usage-telemetry-reporter.test.ts +2 -3
  346. package/src/telemetry/usage-telemetry-reporter.ts +3 -87
  347. package/src/tools/ask-question/ask-question-tool.test.ts +0 -29
  348. package/src/tools/ask-question/ask-question-tool.ts +0 -13
  349. package/src/tools/executor.ts +4 -4
  350. package/src/tools/registry.ts +0 -18
  351. package/src/tools/shared/filesystem/path-policy.ts +5 -12
  352. package/src/tools/tool-approval-handler.ts +1 -1
  353. package/src/tools/tool-defaults.ts +2 -9
  354. package/src/tools/tool-manifest.ts +0 -3
  355. package/src/tools/types.ts +2 -17
  356. package/src/tools/workspace-tools/loader.ts +244 -348
  357. package/src/util/errors.ts +1 -26
  358. package/src/util/platform.ts +0 -5
  359. package/src/workflows/library.test.ts +0 -140
  360. package/src/workflows/library.ts +28 -82
  361. package/src/workspace/migrations/017-seed-persona-dirs.ts +34 -3
  362. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +24 -3
  363. package/src/workspace/migrations/048-remove-workspace-hooks.ts +66 -14
  364. package/src/workspace/migrations/registry.ts +0 -2
  365. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-delivery-contract.test.ts +0 -91
  366. package/node_modules/@vellumai/gateway-client/src/guardian-delivery-contract.ts +0 -48
  367. package/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +0 -28
  368. package/node_modules/@vellumai/service-contracts/src/channels.ts +0 -41
  369. package/src/__tests__/code-search-tool.test.ts +0 -585
  370. package/src/__tests__/guardian-expiry-notifier.test.ts +0 -282
  371. package/src/__tests__/mcp-config-secret-boundary.test.ts +0 -390
  372. package/src/__tests__/plugin-pipeline.test.ts +0 -96
  373. package/src/__tests__/sse-actor-principal-guardian-source.test.ts +0 -102
  374. package/src/__tests__/steer-on-enqueue-question.test.ts +0 -181
  375. package/src/__tests__/workspace-migration-111-prune-seeded-callsite-defaults.test.ts +0 -208
  376. package/src/agent/loop-exclusive-tool.test.ts +0 -150
  377. package/src/api/constants/sse-replay.ts +0 -41
  378. package/src/api/events/conversation-notice.ts +0 -26
  379. package/src/approvals/guardian-channel-delivery.ts +0 -30
  380. package/src/approvals/guardian-expiry-notifier.ts +0 -148
  381. package/src/cli/commands/memory/__tests__/worker.test.ts +0 -302
  382. package/src/cli/commands/memory/worker.ts +0 -175
  383. package/src/config/__tests__/loader-callsite-strip-fallback.test.ts +0 -143
  384. package/src/config/prune-seeded-callsite-defaults.ts +0 -110
  385. package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +0 -129
  386. package/src/contacts/__tests__/guardian-delivery-reader.test.ts +0 -312
  387. package/src/contacts/__tests__/member-write-relay.test.ts +0 -202
  388. package/src/contacts/guardian-delivery-reader.ts +0 -223
  389. package/src/contacts/member-write-relay.ts +0 -189
  390. package/src/daemon/conversation-notices.ts +0 -60
  391. package/src/daemon/handlers/__tests__/config-channels.test.ts +0 -225
  392. package/src/hooks/hook-loader.ts +0 -341
  393. package/src/mcp/mcp-header-store.ts +0 -134
  394. package/src/memory/__tests__/301-create-watchdog-events.test.ts +0 -110
  395. package/src/memory/__tests__/prompt-override.test.ts +0 -192
  396. package/src/memory/__tests__/watchdog-events-store.test.ts +0 -161
  397. package/src/memory/migrations/300-add-processing-started-at.ts +0 -30
  398. package/src/memory/migrations/301-create-watchdog-events.ts +0 -45
  399. package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +0 -224
  400. package/src/memory/prompt-override.ts +0 -129
  401. package/src/memory/steps.ts +0 -573
  402. package/src/memory/watchdog-events-store.ts +0 -87
  403. package/src/memory/worker-control.ts +0 -118
  404. package/src/memory/worker-process.ts +0 -72
  405. package/src/notifications/__tests__/connected-channels.test.ts +0 -114
  406. package/src/notifications/__tests__/destination-resolver.test.ts +0 -256
  407. package/src/onboarding/checkin-event.test.ts +0 -222
  408. package/src/onboarding/checkin-event.ts +0 -321
  409. package/src/onboarding/schedule-checkin.ts +0 -190
  410. package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +0 -106
  411. package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +0 -60
  412. package/src/plugins/defaults/advisor/context-pack.ts +0 -288
  413. package/src/plugins/defaults/memory-v3-shadow/pool-select.test.ts +0 -146
  414. package/src/plugins/surface-import.ts +0 -121
  415. package/src/providers/openai/__tests__/api-error-normalization.test.ts +0 -321
  416. package/src/providers/openai/api-error-normalization.ts +0 -270
  417. package/src/runtime/__tests__/channel-verification-service.test.ts +0 -133
  418. package/src/runtime/__tests__/guardian-vellum-migration.test.ts +0 -181
  419. package/src/runtime/__tests__/is-guardian-bound-for-channel.test.ts +0 -66
  420. package/src/runtime/__tests__/local-principal-trust.test.ts +0 -164
  421. package/src/runtime/anchored-guardian.test.ts +0 -156
  422. package/src/runtime/anchored-guardian.ts +0 -135
  423. package/src/runtime/auth/__tests__/require-bound-guardian.test.ts +0 -99
  424. package/src/runtime/local-principal-trust.ts +0 -52
  425. package/src/runtime/routes/__tests__/contact-routes.test.ts +0 -212
  426. package/src/runtime/routes/__tests__/global-search-routes.test.ts +0 -93
  427. package/src/runtime/routes/onboarding-checkin-routes.ts +0 -86
  428. package/src/tools/filesystem/search.ts +0 -543
  429. package/src/util/telemetry-db-path.ts +0 -24
  430. package/src/workspace/migrations/111-prune-seeded-callsite-defaults.ts +0 -134
@@ -7,15 +7,11 @@
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";
14
10
  import type { ChannelId } from "../channels/types.js";
15
11
  import { findContactChannel, getContact } from "../contacts/contact-store.js";
16
- import { activateMemberChannel } from "../contacts/member-write-relay.js";
17
- import type { ChannelStatus } from "../contacts/types.js";
12
+ import { upsertContactChannel } from "../contacts/contacts-write.js";
18
13
  import { ipcCallPersistent } from "../ipc/gateway-client.js";
14
+ import { getSqlite } from "../memory/db-connection.js";
19
15
  import {
20
16
  findActiveVoiceInvites,
21
17
  findByInviteCodeHash,
@@ -27,27 +23,9 @@ import {
27
23
  import { canonicalizeInboundIdentity } from "../util/canonicalize-identity.js";
28
24
  import { getLogger } from "../util/logger.js";
29
25
  import { hashVoiceCode } from "../util/voice-code.js";
30
- import { verdictMemberFromVerdict } from "./trust-verdict-consumer.js";
31
26
 
32
27
  const log = getLogger("invite-redemption-service");
33
28
 
34
- /**
35
- * Resolve the sender's existing member status for the already_member/blocked
36
- * gate from the gateway trust verdict. Falls back to the local channel status
37
- * when the verdict is absent or carries no resolvable member status (e.g. an
38
- * externalChatId-only match or a resolutionFailed verdict), so a locally
39
- * blocked contact can't bypass the gate.
40
- */
41
- export async function resolveMemberGateStatus(
42
- verdict: Awaited<ReturnType<typeof getInboundTrustVerdict>>,
43
- localChannelStatus: ChannelStatus | null,
44
- ): Promise<ChannelStatus | null> {
45
- const memberStatus = verdict
46
- ? verdictMemberFromVerdict(verdict)?.status
47
- : null;
48
- return memberStatus ?? localChannelStatus;
49
- }
50
-
51
29
  // ---------------------------------------------------------------------------
52
30
  // Gateway lifecycle bridge (shared by all redemption paths)
53
31
  // ---------------------------------------------------------------------------
@@ -240,22 +218,18 @@ export async function redeemInvite(params: {
240
218
  const targetMismatch =
241
219
  existingContact && existingContact.id !== invite.contactId;
242
220
 
243
- const gateStatus = await resolveMemberGateStatus(
244
- await getInboundTrustVerdict({
245
- channelType: sourceChannel as ChannelId,
246
- actorExternalId: canonicalUserId,
247
- }),
248
- existingChannel?.status ?? null,
249
- );
250
-
251
- if (existingChannel && gateStatus === "active" && !targetMismatch) {
221
+ if (
222
+ existingChannel &&
223
+ existingChannel.status === "active" &&
224
+ !targetMismatch
225
+ ) {
252
226
  return { ok: true, type: "already_member", memberId: existingChannel.id };
253
227
  }
254
228
 
255
229
  // Blocked members cannot bypass the guardian's explicit block via invite
256
230
  // links. Return the same generic failure as an invalid token to avoid
257
231
  // leaking membership status to the caller.
258
- if (existingChannel && gateStatus === "blocked") {
232
+ if (existingChannel && existingChannel.status === "blocked") {
259
233
  return { ok: false, reason: "invalid_token" };
260
234
  }
261
235
 
@@ -273,10 +247,14 @@ export async function redeemInvite(params: {
273
247
  return { ok: false, reason: "invalid_token" };
274
248
  }
275
249
 
276
- // Inactive member reactivation: when the user already has a member record in
277
- // a non-active state (revoked/pending), reactivate it gateway-first then
278
- // consume an invite use. The fresh-member path below mirrors this.
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.
279
254
  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");
280
258
  const canonicalMemberId = existingChannel.address;
281
259
  const canonicalCallerId = externalUserId
282
260
  ? canonicalizeInboundIdentity(sourceChannel as ChannelId, externalUserId)
@@ -288,17 +266,42 @@ export async function redeemInvite(params: {
288
266
  ? existingContact.displayName
289
267
  : displayName;
290
268
 
291
- // Consume the assistant invite use BEFORE activating the member, so a
292
- // concurrent revoke/exhaustion (recordInviteUse returns false) leaves no
293
- // active member behind.
269
+ let reactivated: ReturnType<typeof upsertContactChannel> | undefined;
294
270
  try {
295
- if (
296
- !recordInviteUse({ inviteId: invite.id, externalUserId, externalChatId })
297
- ) {
298
- // Invite revoked/expired between pre-validation and write.
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) {
299
303
  return { ok: false, reason: "invalid_token" };
300
304
  }
301
- } catch (err) {
302
305
  // Rare: the gateway claim already consumed the row but the assistant
303
306
  // mutation failed — a recoverable wasted gateway use; no cross-process
304
307
  // rollback is attempted.
@@ -309,30 +312,10 @@ export async function redeemInvite(params: {
309
312
  throw err;
310
313
  }
311
314
 
312
- // Gateway-first: activate the member channel on the authoritative gateway
313
- // before the assistant DB; the local mirror is best-effort.
314
- const reactivated = await activateMemberChannel({
315
- sourceChannel,
316
- externalUserId,
317
- externalChatId,
318
- // Reactivation should not overwrite a guardian-managed nickname.
319
- displayName: preservedDisplayName,
320
- username,
321
- policy: "allow",
322
- inviteId: invite.id,
323
- verifiedAt: Date.now(),
324
- verifiedVia: "invite",
325
- contactId: invite.contactId,
326
- });
327
-
328
- if (reactivated.status === "refused") {
329
- return { ok: false, reason: "invalid_token" };
330
- }
331
-
332
315
  return {
333
316
  ok: true,
334
317
  type: "redeemed",
335
- memberId: reactivated.memberId,
318
+ memberId: reactivated!.channel.id,
336
319
  inviteId: invite.id,
337
320
  };
338
321
  }
@@ -349,17 +332,39 @@ export async function redeemInvite(params: {
349
332
  }
350
333
  }
351
334
 
352
- // Consume the assistant invite use BEFORE activating the member, so a
353
- // concurrent revoke/exhaustion (recordInviteUse returns false) leaves no
354
- // active member behind.
335
+ const STALE_INVITE_FRESH = Symbol("stale_invite_fresh");
336
+ let freshResult: ReturnType<typeof upsertContactChannel> | undefined;
355
337
  try {
356
- if (
357
- !recordInviteUse({ inviteId: invite.id, externalUserId, externalChatId })
358
- ) {
359
- // Invite revoked/expired between pre-validation and write.
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) {
360
366
  return { ok: false, reason: "invalid_token" };
361
367
  }
362
- } catch (err) {
363
368
  // Rare: gateway claim succeeded but the assistant mutation failed — a
364
369
  // recoverable wasted gateway use; no cross-process rollback attempted.
365
370
  log.error(
@@ -369,29 +374,10 @@ export async function redeemInvite(params: {
369
374
  throw err;
370
375
  }
371
376
 
372
- // Gateway-first: activate the member channel on the authoritative gateway
373
- // before the assistant DB; the local mirror is best-effort.
374
- const freshResult = await activateMemberChannel({
375
- sourceChannel,
376
- externalUserId,
377
- externalChatId,
378
- displayName: freshDisplayName,
379
- username,
380
- policy: "allow",
381
- inviteId: invite.id,
382
- verifiedAt: Date.now(),
383
- verifiedVia: "invite",
384
- contactId: invite.contactId,
385
- });
386
-
387
- if (freshResult.status === "refused") {
388
- return { ok: false, reason: "invalid_token" };
389
- }
390
-
391
377
  return {
392
378
  ok: true,
393
379
  type: "redeemed",
394
- memberId: freshResult.memberId,
380
+ memberId: freshResult!.channel.id,
395
381
  inviteId: invite.id,
396
382
  };
397
383
  }
@@ -479,12 +465,11 @@ export async function redeemVoiceInviteCode(params: {
479
465
  // should bind the sender's identity to the target contact, not the existing one.
480
466
  const targetMismatch = voiceContact && voiceContact.id !== invite.contactId;
481
467
 
482
- const gateStatus = await resolveMemberGateStatus(
483
- await getPhoneCallerVerdict(canonicalCallerId),
484
- existingVoiceChannel?.status ?? null,
485
- );
486
-
487
- if (existingVoiceChannel && gateStatus === "active" && !targetMismatch) {
468
+ if (
469
+ existingVoiceChannel &&
470
+ existingVoiceChannel.status === "active" &&
471
+ !targetMismatch
472
+ ) {
488
473
  return {
489
474
  ok: true,
490
475
  type: "already_member",
@@ -493,7 +478,7 @@ export async function redeemVoiceInviteCode(params: {
493
478
  }
494
479
 
495
480
  // Blocked members cannot bypass the guardian's explicit block
496
- if (existingVoiceChannel && gateStatus === "blocked") {
481
+ if (existingVoiceChannel && existingVoiceChannel.status === "blocked") {
497
482
  return { ok: false, reason: "invalid_or_expired" };
498
483
  }
499
484
 
@@ -511,6 +496,10 @@ export async function redeemVoiceInviteCode(params: {
511
496
  return { ok: false, reason: "invalid_or_expired" };
512
497
  }
513
498
 
499
+ // Atomic redemption: upsert member + consume invite use in a transaction
500
+ const STALE_INVITE = Symbol("stale_invite");
501
+ let memberId: string | undefined;
502
+
514
503
  // When the invite targets a specific contact, preserve the target contact's
515
504
  // guardian-assigned display name if it has one.
516
505
  let preservedDisplayName = voiceContact?.displayName?.trim().length
@@ -523,20 +512,36 @@ export async function redeemVoiceInviteCode(params: {
523
512
  }
524
513
  }
525
514
 
526
- // Consume the assistant invite use BEFORE activating the member, so a
527
- // concurrent revoke/exhaustion (recordInviteUse returns false) leaves no
528
- // active member behind.
529
515
  try {
530
- if (
531
- !recordInviteUse({
532
- inviteId: invite.id,
533
- externalUserId: callerExternalUserId,
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;
534
539
  })
535
- ) {
536
- // Invite revoked/expired between pre-validation and write.
540
+ .immediate();
541
+ } catch (err) {
542
+ if (err === STALE_INVITE) {
537
543
  return { ok: false, reason: "invalid_or_expired" };
538
544
  }
539
- } catch (err) {
540
545
  // Rare: gateway claim succeeded but the assistant mutation failed — a
541
546
  // recoverable wasted gateway use; no cross-process rollback attempted.
542
547
  log.error(
@@ -546,28 +551,10 @@ export async function redeemVoiceInviteCode(params: {
546
551
  throw err;
547
552
  }
548
553
 
549
- // Gateway-first: activate the member channel on the authoritative gateway
550
- // before the assistant DB; the local mirror is best-effort.
551
- const writeResult = await activateMemberChannel({
552
- sourceChannel: "phone",
553
- externalUserId: callerExternalUserId,
554
- externalChatId: callerExternalUserId,
555
- displayName: preservedDisplayName,
556
- policy: "allow",
557
- inviteId: invite.id,
558
- verifiedAt: Date.now(),
559
- verifiedVia: "invite",
560
- contactId: invite.contactId,
561
- });
562
-
563
- if (writeResult.status === "refused") {
564
- return { ok: false, reason: "invalid_or_expired" };
565
- }
566
-
567
554
  return {
568
555
  ok: true,
569
556
  type: "redeemed",
570
- memberId: writeResult.memberId,
557
+ memberId: memberId!,
571
558
  inviteId: invite.id,
572
559
  };
573
560
  }
@@ -652,22 +639,18 @@ export async function redeemInviteByCode(params: {
652
639
  const targetMismatch =
653
640
  existingContact && existingContact.id !== invite.contactId;
654
641
 
655
- const gateStatus = await resolveMemberGateStatus(
656
- await getInboundTrustVerdict({
657
- channelType: sourceChannel as ChannelId,
658
- actorExternalId: canonicalUserId,
659
- }),
660
- existingChannel?.status ?? null,
661
- );
662
-
663
- if (existingChannel && gateStatus === "active" && !targetMismatch) {
642
+ if (
643
+ existingChannel &&
644
+ existingChannel.status === "active" &&
645
+ !targetMismatch
646
+ ) {
664
647
  return { ok: true, type: "already_member", memberId: existingChannel.id };
665
648
  }
666
649
 
667
650
  // Blocked members cannot bypass the guardian's explicit block via invite
668
651
  // codes. Return the same generic failure as an invalid token to avoid
669
652
  // leaking membership status to the caller.
670
- if (existingChannel && gateStatus === "blocked") {
653
+ if (existingChannel && existingChannel.status === "blocked") {
671
654
  return { ok: false, reason: "invalid_token" };
672
655
  }
673
656
 
@@ -684,9 +667,10 @@ export async function redeemInviteByCode(params: {
684
667
  return { ok: false, reason: "invalid_token" };
685
668
  }
686
669
 
687
- // Inactive member reactivation: reactivate gateway-first, then consume an
688
- // invite use.
670
+ // Inactive member reactivation: reactivate via upsertContactChannel and consume
671
+ // an invite use atomically.
689
672
  if (existingChannel && !targetMismatch) {
673
+ const STALE_INVITE_REACTIVATE = Symbol("stale_invite_reactivate");
690
674
  const canonicalMemberId = existingChannel.address;
691
675
  const canonicalCallerId = externalUserId
692
676
  ? canonicalizeInboundIdentity(sourceChannel as ChannelId, externalUserId)
@@ -698,17 +682,38 @@ export async function redeemInviteByCode(params: {
698
682
  ? existingContact.displayName
699
683
  : displayName;
700
684
 
701
- // Consume the assistant invite use BEFORE activating the member, so a
702
- // concurrent revoke/exhaustion (recordInviteUse returns false) leaves no
703
- // active member behind.
685
+ let reactivated: ReturnType<typeof upsertContactChannel> | undefined;
704
686
  try {
705
- if (
706
- !recordInviteUse({ inviteId: invite.id, externalUserId, externalChatId })
707
- ) {
708
- // Invite revoked/expired between pre-validation and write.
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) {
709
715
  return { ok: false, reason: "invalid_token" };
710
716
  }
711
- } catch (err) {
712
717
  // Rare: gateway claim succeeded but the assistant mutation failed — a
713
718
  // recoverable wasted gateway use; no cross-process rollback attempted.
714
719
  log.error(
@@ -718,29 +723,10 @@ export async function redeemInviteByCode(params: {
718
723
  throw err;
719
724
  }
720
725
 
721
- // Gateway-first: activate the member channel on the authoritative gateway
722
- // before the assistant DB; the local mirror is best-effort.
723
- const reactivated = await activateMemberChannel({
724
- sourceChannel,
725
- externalUserId,
726
- externalChatId,
727
- displayName: preservedDisplayName,
728
- username,
729
- policy: "allow",
730
- inviteId: invite.id,
731
- verifiedAt: Date.now(),
732
- verifiedVia: "invite",
733
- contactId: invite.contactId,
734
- });
735
-
736
- if (reactivated.status === "refused") {
737
- return { ok: false, reason: "invalid_token" };
738
- }
739
-
740
726
  return {
741
727
  ok: true,
742
728
  type: "redeemed",
743
- memberId: reactivated.memberId,
729
+ memberId: reactivated!.channel.id,
744
730
  inviteId: invite.id,
745
731
  };
746
732
  }
@@ -757,17 +743,39 @@ export async function redeemInviteByCode(params: {
757
743
  }
758
744
  }
759
745
 
760
- // Consume the assistant invite use BEFORE activating the member, so a
761
- // concurrent revoke/exhaustion (recordInviteUse returns false) leaves no
762
- // active member behind.
746
+ const STALE_INVITE_FRESH = Symbol("stale_invite_fresh");
747
+ let freshResult: ReturnType<typeof upsertContactChannel> | undefined;
763
748
  try {
764
- if (
765
- !recordInviteUse({ inviteId: invite.id, externalUserId, externalChatId })
766
- ) {
767
- // Invite revoked/expired between pre-validation and write.
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) {
768
777
  return { ok: false, reason: "invalid_token" };
769
778
  }
770
- } catch (err) {
771
779
  // Rare: gateway claim succeeded but the assistant mutation failed — a
772
780
  // recoverable wasted gateway use; no cross-process rollback attempted.
773
781
  log.error(
@@ -777,29 +785,10 @@ export async function redeemInviteByCode(params: {
777
785
  throw err;
778
786
  }
779
787
 
780
- // Gateway-first: activate the member channel on the authoritative gateway
781
- // before the assistant DB; the local mirror is best-effort.
782
- const freshResult = await activateMemberChannel({
783
- sourceChannel,
784
- externalUserId,
785
- externalChatId,
786
- displayName: freshDisplayName,
787
- username,
788
- policy: "allow",
789
- inviteId: invite.id,
790
- verifiedAt: Date.now(),
791
- verifiedVia: "invite",
792
- contactId: invite.contactId,
793
- });
794
-
795
- if (freshResult.status === "refused") {
796
- return { ok: false, reason: "invalid_token" };
797
- }
798
-
799
788
  return {
800
789
  ok: true,
801
790
  type: "redeemed",
802
- memberId: freshResult.memberId,
791
+ memberId: freshResult!.channel.id,
803
792
  inviteId: invite.id,
804
793
  };
805
794
  }