@vellumai/assistant 0.10.4 → 0.10.5-dev.202607022329.d89bf2f

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 (866) hide show
  1. package/ARCHITECTURE.md +94 -91
  2. package/Dockerfile +5 -16
  3. package/README.md +7 -10
  4. package/bun.lock +0 -4
  5. package/docs/architecture/security.md +20 -20
  6. package/docs/stt-provider-onboarding.md +4 -5
  7. package/docs/trusted-contact-access.md +7 -7
  8. package/eslint-rules/cli-no-daemon-internals.js +4 -0
  9. package/eslint.config.mjs +7 -1
  10. package/knip.json +0 -1
  11. package/node_modules/@vellumai/gateway-client/src/__tests__/contact-read-contracts.test.ts +36 -0
  12. package/node_modules/@vellumai/gateway-client/src/__tests__/invite-contract.test.ts +233 -0
  13. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +40 -0
  14. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +20 -8
  15. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +10 -0
  16. package/node_modules/@vellumai/gateway-client/src/index.ts +33 -0
  17. package/node_modules/@vellumai/gateway-client/src/invite-contract.ts +216 -0
  18. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +91 -0
  19. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +39 -3
  20. package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +0 -8
  21. package/node_modules/@vellumai/service-contracts/src/__tests__/package-boundary.test.ts +1 -5
  22. package/node_modules/@vellumai/service-contracts/src/twilio-ingress.ts +3 -14
  23. package/openapi.yaml +1073 -225
  24. package/package.json +1 -4
  25. package/scripts/test.sh +35 -13
  26. package/src/__tests__/access-request-seed-content-blocks.test.ts +60 -4
  27. package/src/__tests__/acp-session.test.ts +4 -1
  28. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +0 -6
  29. package/src/__tests__/add-message-preassigned-id.test.ts +56 -0
  30. package/src/__tests__/agent-loop-set-system-prompt.test.ts +139 -0
  31. package/src/__tests__/app-conversation-ids-backfill.test.ts +3 -0
  32. package/src/__tests__/app-source-watcher.test.ts +11 -0
  33. package/src/__tests__/approval-interception-trust-gates.test.ts +148 -3
  34. package/src/__tests__/assistant-attachments.test.ts +62 -4
  35. package/src/__tests__/assistant-id-boundary-guard.test.ts +0 -1
  36. package/src/__tests__/attachments-store-heic-normalize.test.ts +230 -0
  37. package/src/__tests__/attachments-store.test.ts +3 -0
  38. package/src/__tests__/background-shell-bash.test.ts +34 -0
  39. package/src/__tests__/background-shell-host-bash.test.ts +12 -0
  40. package/src/__tests__/background-tool-registry.test.ts +97 -0
  41. package/src/__tests__/background-tool-routes.test.ts +72 -1
  42. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -1
  43. package/src/__tests__/btw-routes.test.ts +57 -3
  44. package/src/__tests__/call-controller.test.ts +474 -84
  45. package/src/__tests__/call-domain.test.ts +48 -1
  46. package/src/__tests__/call-pointer-messages.test.ts +57 -19
  47. package/src/__tests__/call-pointer-no-hardcoded-copy.guard.test.ts +2 -6
  48. package/src/__tests__/call-routes-http.test.ts +15 -14
  49. package/src/__tests__/call-setup-flow-invite.test.ts +568 -0
  50. package/src/__tests__/call-setup-flow-midcall-trust.test.ts +231 -0
  51. package/src/__tests__/call-setup-flow-name-capture.test.ts +754 -0
  52. package/src/__tests__/call-setup-flow-verification.test.ts +925 -0
  53. package/src/__tests__/call-setup-flow.test.ts +311 -0
  54. package/src/__tests__/cancel-resolves-conversation-key.test.ts +0 -1
  55. package/src/__tests__/channel-guardian.test.ts +4 -7
  56. package/src/__tests__/channel-readiness-service.test.ts +0 -6
  57. package/src/__tests__/channel-readiness-slack-remote.test.ts +0 -4
  58. package/src/__tests__/channel-retry-sweep.test.ts +76 -64
  59. package/src/__tests__/channel-setup-panel-ack.test.ts +184 -0
  60. package/src/__tests__/clear-all-lexical.test.ts +90 -0
  61. package/src/__tests__/compactor-media-strip.test.ts +177 -0
  62. package/src/__tests__/config-loader-backfill.test.ts +5 -5
  63. package/src/__tests__/config-schema.test.ts +17 -0
  64. package/src/__tests__/contact-store-interaction-info.test.ts +27 -70
  65. package/src/__tests__/contacts-relay-reads.test.ts +188 -23
  66. package/src/__tests__/contacts-tools.test.ts +42 -1
  67. package/src/__tests__/contacts-write.test.ts +181 -0
  68. package/src/__tests__/context-search-conversations-source.test.ts +220 -81
  69. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +1 -0
  70. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
  71. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
  72. package/src/__tests__/conversation-agent-loop.test.ts +39 -0
  73. package/src/__tests__/conversation-analysis-routes.test.ts +1 -1
  74. package/src/__tests__/conversation-app-control-instantiation.test.ts +2 -0
  75. package/src/__tests__/conversation-clear-safety.test.ts +0 -1
  76. package/src/__tests__/conversation-crud-enabled-plugins.test.ts +60 -0
  77. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -1
  78. package/src/__tests__/conversation-disk-view.test.ts +1 -0
  79. package/src/__tests__/conversation-enabledplugins-route.test.ts +184 -0
  80. package/src/__tests__/conversation-error.test.ts +0 -18
  81. package/src/__tests__/conversation-fork-crud.test.ts +67 -0
  82. package/src/__tests__/conversation-history-web-search.test.ts +7 -137
  83. package/src/__tests__/conversation-initial-prompt.test.ts +110 -0
  84. package/src/__tests__/conversation-loop-db-lock-resilience.test.ts +191 -0
  85. package/src/__tests__/conversation-process-callsite.test.ts +37 -0
  86. package/src/__tests__/conversation-queue.test.ts +149 -0
  87. package/src/__tests__/conversation-routes-enabled-plugins.test.ts +392 -0
  88. package/src/__tests__/conversation-routes-guardian-reply.test.ts +84 -0
  89. package/src/__tests__/conversation-routes-hidden-queue.test.ts +407 -0
  90. package/src/__tests__/conversation-runtime-assembly.test.ts +36 -16
  91. package/src/__tests__/conversation-skill-tools.test.ts +73 -0
  92. package/src/__tests__/conversation-wipe.test.ts +20 -5
  93. package/src/__tests__/credential-prompt-route.test.ts +69 -0
  94. package/src/__tests__/credential-security-invariants.test.ts +1 -3
  95. package/src/__tests__/db-drop-messages-fts.test.ts +115 -0
  96. package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -62
  97. package/src/__tests__/edit-propagation.test.ts +56 -2
  98. package/src/__tests__/empty-state-greeting-cache.test.ts +9 -0
  99. package/src/__tests__/gateway-only-enforcement.test.ts +15 -110
  100. package/src/__tests__/guardian-expiry-notifier.test.ts +1 -0
  101. package/src/__tests__/guardian-question-mode.test.ts +57 -0
  102. package/src/__tests__/guardian-verification-voice-binding.test.ts +1 -1
  103. package/src/__tests__/guardian-wait-controller.test.ts +619 -0
  104. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +33 -2
  105. package/src/__tests__/heic-fixture.ts +55 -0
  106. package/src/__tests__/helpers/seed-contact-channel.ts +0 -2
  107. package/src/__tests__/http-conversation-lineage.test.ts +6 -1
  108. package/src/__tests__/image-conversion.test.ts +170 -0
  109. package/src/__tests__/inbound-invite-redemption.test.ts +72 -341
  110. package/src/__tests__/introduction-card-resolver.test.ts +354 -0
  111. package/src/__tests__/invite-redeemed-ipc.test.ts +97 -0
  112. package/src/__tests__/invite-routes-http.test.ts +246 -420
  113. package/src/__tests__/job-handler-registration-guard.test.ts +104 -0
  114. package/src/__tests__/lexical-index-dual-write.test.ts +426 -0
  115. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +2 -2
  116. package/src/__tests__/list-messages-attachments.test.ts +159 -0
  117. package/src/__tests__/list-messages-background-tool-completion.test.ts +141 -0
  118. package/src/__tests__/list-messages-queued.test.ts +25 -0
  119. package/src/__tests__/managed-store.test.ts +129 -1
  120. package/src/__tests__/media-stream-audio-transcode.test.ts +113 -0
  121. package/src/__tests__/media-stream-output.test.ts +350 -19
  122. package/src/__tests__/media-stream-server-integration.test.ts +943 -169
  123. package/src/__tests__/media-stream-stt-session.test.ts +498 -1
  124. package/src/__tests__/memory-jobs-worker-lanes.test.ts +2 -2
  125. package/src/__tests__/model-intents.test.ts +1 -1
  126. package/src/__tests__/mtime-cache.test.ts +19 -0
  127. package/src/__tests__/non-member-access-request.test.ts +349 -3
  128. package/src/__tests__/notification-decision-fallback.test.ts +104 -8
  129. package/src/__tests__/notification-decision-strategy.test.ts +57 -8
  130. package/src/__tests__/onboarding-template-contract.test.ts +27 -43
  131. package/src/__tests__/openai-provider.test.ts +33 -0
  132. package/src/__tests__/openai-responses-provider.test.ts +40 -0
  133. package/src/__tests__/persistence-layering-guard.test.ts +29 -20
  134. package/src/__tests__/persona-resolver.test.ts +56 -2
  135. package/src/__tests__/personal-memory-auth-bypass.test.ts +62 -0
  136. package/src/__tests__/phone.test.ts +1 -15
  137. package/src/__tests__/plugin-bootstrap.test.ts +25 -196
  138. package/src/__tests__/plugin-disabled-state.test.ts +33 -0
  139. package/src/__tests__/plugin-effective-enabled-set.test.ts +227 -0
  140. package/src/__tests__/plugin-event-hub-facade.test.ts +323 -0
  141. package/src/__tests__/plugin-import-boundary-guard.test.ts +156 -42
  142. package/src/__tests__/plugin-injector-contribution.test.ts +4 -20
  143. package/src/__tests__/plugin-pipeline.test.ts +63 -1
  144. package/src/__tests__/plugin-route-contribution.test.ts +62 -115
  145. package/src/__tests__/plugin-tool-contribution.test.ts +0 -22
  146. package/src/__tests__/plugin-types.test.ts +0 -1
  147. package/src/__tests__/pointer-conversation-trust.test.ts +38 -6
  148. package/src/__tests__/pre-model-call-sanitize.test.ts +1 -1
  149. package/src/__tests__/prune-old-tool-invocations-job.test.ts +97 -0
  150. package/src/__tests__/public-ingress-urls.test.ts +0 -48
  151. package/src/__tests__/qdrant-collection-migration.test.ts +145 -2
  152. package/src/__tests__/reaction-intercept-member-verdict-warm.test.ts +0 -1
  153. package/src/__tests__/resolve-trust-class.test.ts +28 -10
  154. package/src/__tests__/scaffold-managed-skill-tool.test.ts +137 -0
  155. package/src/__tests__/schedule-retry.test.ts +38 -14
  156. package/src/__tests__/schedule-routes-workflow-validation.test.ts +3 -3
  157. package/src/__tests__/schedule-routes.test.ts +14 -6
  158. package/src/__tests__/schedule-store.test.ts +3 -1
  159. package/src/__tests__/scheduler-disk-pressure.test.ts +17 -6
  160. package/src/__tests__/scheduler-recurrence.test.ts +57 -107
  161. package/src/__tests__/scheduler-reuse-conversation.test.ts +51 -46
  162. package/src/__tests__/scheduler-wake.test.ts +13 -23
  163. package/src/__tests__/scheduler-worker-standdown.test.ts +220 -0
  164. package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
  165. package/src/__tests__/shell-observability.test.ts +1 -0
  166. package/src/__tests__/skill-boundary-guard.test.ts +3 -4
  167. package/src/__tests__/skill-load-plugin-scope.test.ts +238 -0
  168. package/src/__tests__/skill-projection-feature-flag.test.ts +2 -0
  169. package/src/__tests__/skill-script-runner-host.test.ts +53 -0
  170. package/src/__tests__/skill-script-runner-sandbox.test.ts +5 -2
  171. package/src/__tests__/skill-tool-factory.test.ts +73 -1
  172. package/src/__tests__/slack-app-setup-skill-regression.test.ts +37 -14
  173. package/src/__tests__/slack-inbound-verification.test.ts +36 -0
  174. package/src/__tests__/slack-notification-approval-card.test.ts +89 -2
  175. package/src/__tests__/slack-reaction-canonical-approval.test.ts +51 -1
  176. package/src/__tests__/stt-hints.test.ts +2 -256
  177. package/src/__tests__/subagent-allowlist-validation.test.ts +47 -0
  178. package/src/__tests__/subagent-call-site-routing.test.ts +72 -0
  179. package/src/__tests__/subagent-fork-notifications.test.ts +14 -2
  180. package/src/__tests__/subagent-persistence.test.ts +122 -0
  181. package/src/__tests__/subagent-spawn-and-await.test.ts +1 -0
  182. package/src/__tests__/subagent-terminal-message.test.ts +99 -0
  183. package/src/__tests__/subagent-tool-gate-mode.test.ts +78 -1
  184. package/src/__tests__/system-prompt.test.ts +17 -118
  185. package/src/__tests__/task-scheduler.test.ts +68 -89
  186. package/src/__tests__/telephony-credential-preflight.test.ts +312 -0
  187. package/src/__tests__/telephony-tts-guard.test.ts +538 -0
  188. package/src/__tests__/title-generate-hook.test.ts +16 -0
  189. package/src/__tests__/tool-approval-handler.test.ts +87 -1
  190. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +45 -20
  191. package/src/__tests__/tool-grant-request-escalation.test.ts +39 -0
  192. package/src/__tests__/trust-context-guards.test.ts +21 -10
  193. package/src/__tests__/trusted-contact-multichannel.test.ts +4 -0
  194. package/src/__tests__/twilio-provider.test.ts +17 -17
  195. package/src/__tests__/twilio-routes-twiml.test.ts +6 -543
  196. package/src/__tests__/twilio-routes.test.ts +257 -387
  197. package/src/__tests__/twilio-validation.test.ts +1 -1
  198. package/src/__tests__/unified-turn-context-client-os.test.ts +45 -0
  199. package/src/__tests__/usage-cache-backfill-migration.test.ts +5 -0
  200. package/src/__tests__/voice-approval-resolver-relay.test.ts +150 -0
  201. package/src/__tests__/voice-ingress-preflight.test.ts +61 -0
  202. package/src/__tests__/voice-quality.test.ts +2 -2
  203. package/src/__tests__/voice-scoped-grant-consumer.test.ts +1 -7
  204. package/src/__tests__/voice-session-bridge-processing-wait.test.ts +27 -0
  205. package/src/__tests__/voice-session-bridge.test.ts +1 -10
  206. package/src/__tests__/workspace-git-service.test.ts +1 -1
  207. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +1 -0
  208. package/src/__tests__/workspace-migration-118-seed-now-md.test.ts +120 -0
  209. package/src/__tests__/workspace-migration-120-revise-onboarding-threads.test.ts +175 -0
  210. package/src/__tests__/workspace-migration-121-seed-default-user-guardrails.test.ts +118 -0
  211. package/src/__tests__/workspace-migration-122-relocate-default-user-boundary.test.ts +119 -0
  212. package/src/__tests__/workspace-migration-123-swap-quality-profile-to-fable.test.ts +191 -0
  213. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +235 -241
  214. package/src/acp/__tests__/prepare-agent-env.test.ts +0 -97
  215. package/src/acp/__tests__/session-manager-persistence.test.ts +2 -0
  216. package/src/acp/__tests__/session-manager-resume.test.ts +8 -0
  217. package/src/acp/__tests__/session-manager-usage.test.ts +6 -0
  218. package/src/acp/__tests__/session-manager.test.ts +6 -0
  219. package/src/acp/agent-process.test.ts +116 -0
  220. package/src/acp/agent-process.ts +71 -0
  221. package/src/acp/auto-install.ts +12 -8
  222. package/src/acp/failure-error.test.ts +109 -0
  223. package/src/acp/failure-error.ts +111 -0
  224. package/src/acp/prepare-agent-env.ts +0 -12
  225. package/src/acp/resolve-agent.test.ts +6 -45
  226. package/src/acp/resolve-agent.ts +2 -4
  227. package/src/acp/session-manager.test.ts +312 -0
  228. package/src/acp/session-manager.ts +102 -46
  229. package/src/agent/ax-tree-compaction.test.ts +4 -1
  230. package/src/agent/image-optimize.ts +17 -127
  231. package/src/agent/loop.ts +45 -192
  232. package/src/api/events/background-tool-completed.test.ts +51 -0
  233. package/src/api/events/background-tool-completed.ts +29 -0
  234. package/src/api/events/background-tool-started.test.ts +36 -0
  235. package/src/api/events/background-tool-started.ts +28 -0
  236. package/src/api/events/conversation-error.ts +0 -1
  237. package/src/api/events/open-panel.ts +33 -0
  238. package/src/api/events/subagent-status-changed.ts +4 -1
  239. package/src/api/index.ts +19 -0
  240. package/src/api/responses/conversation-message.ts +34 -0
  241. package/src/approvals/guardian-decision-primitive.ts +74 -24
  242. package/src/approvals/guardian-request-resolvers.ts +450 -122
  243. package/src/apps/app-store.ts +1 -0
  244. package/src/apps/shared-app-links-store.ts +1 -0
  245. package/src/avatar/identity-avatar.ts +3 -7
  246. package/src/background-wake/wake-intent-hooks.test.ts +1 -1
  247. package/src/calls/__tests__/fish-audio-client.test.ts +73 -0
  248. package/src/calls/__tests__/gateway-invite-reader.test.ts +170 -0
  249. package/src/calls/__tests__/relay-setup-router.test.ts +126 -102
  250. package/src/calls/call-controller.ts +134 -48
  251. package/src/calls/call-domain.ts +18 -42
  252. package/src/calls/call-pointer-messages.ts +51 -67
  253. package/src/calls/call-setup-flow-types.ts +76 -0
  254. package/src/calls/call-setup-flow.ts +1743 -0
  255. package/src/calls/call-speech-output.ts +87 -23
  256. package/src/calls/call-store.ts +4 -0
  257. package/src/calls/call-transport.ts +22 -36
  258. package/src/calls/fish-audio-client.ts +3 -0
  259. package/src/calls/gateway-invite-reader.ts +98 -0
  260. package/src/calls/guardian-wait-controller.ts +451 -0
  261. package/src/calls/media-stream-audio-transcode.ts +82 -2
  262. package/src/calls/media-stream-output.ts +142 -36
  263. package/src/calls/media-stream-server.ts +297 -178
  264. package/src/calls/media-stream-stt-session.ts +393 -50
  265. package/src/calls/media-turn-detector.ts +4 -6
  266. package/src/calls/relay-access-wait.ts +3 -4
  267. package/src/calls/relay-setup-router.ts +22 -43
  268. package/src/calls/relay-verification.ts +17 -24
  269. package/src/calls/resolve-call-tts-provider.ts +96 -29
  270. package/src/calls/stt-hints.ts +24 -87
  271. package/src/calls/telephony-credential-preflight.ts +159 -0
  272. package/src/calls/telephony-tts-capability.ts +183 -0
  273. package/src/calls/tts-call-strategy.ts +18 -20
  274. package/src/calls/twilio-provider.ts +3 -2
  275. package/src/calls/twilio-routes.ts +47 -351
  276. package/src/calls/types.ts +5 -3
  277. package/src/calls/voice-ingress-preflight.ts +9 -0
  278. package/src/calls/voice-quality.ts +13 -18
  279. package/src/calls/voice-session-bridge.ts +25 -35
  280. package/src/channels/__tests__/types.test.ts +28 -0
  281. package/src/channels/channel-verification-sessions.ts +7 -2
  282. package/src/channels/config.ts +0 -42
  283. package/src/channels/slack-thread-store.ts +12 -0
  284. package/src/channels/types.ts +23 -0
  285. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -1
  286. package/src/cli/commands/__tests__/schedules.test.ts +83 -34
  287. package/src/cli/commands/__tests__/status.test.ts +42 -2
  288. package/src/cli/commands/contacts.ts +2 -2
  289. package/src/cli/commands/credentials.ts +17 -0
  290. package/src/cli/commands/monitoring.ts +191 -0
  291. package/src/cli/commands/schedules-worker.ts +175 -0
  292. package/src/cli/commands/schedules.ts +169 -43
  293. package/src/cli/commands/status.ts +12 -1
  294. package/src/cli/commands/tools.ts +5 -1
  295. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +1 -0
  296. package/src/cli/lib/__tests__/search-plugins.test.ts +29 -1
  297. package/src/cli/lib/__tests__/toggle-plugin.test.ts +7 -0
  298. package/src/cli/lib/search-plugins.ts +6 -0
  299. package/src/cli/lib/toggle-plugin.ts +21 -2
  300. package/src/cli/program.ts +3 -1
  301. package/src/config/__tests__/deployment-context-defaults.test.ts +23 -0
  302. package/src/config/acp-defaults.test.ts +0 -10
  303. package/src/config/acp-defaults.ts +0 -6
  304. package/src/config/bundled-skills/acp/SKILL.md +13 -45
  305. package/src/config/bundled-skills/acp/TOOLS.json +3 -3
  306. package/src/config/bundled-skills/app-builder/SKILL.md +3 -3
  307. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +9 -9
  308. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +1 -1
  309. package/src/config/bundled-skills/contacts/tools/contact-search.ts +1 -1
  310. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  311. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +9 -9
  312. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +10 -7
  313. package/src/config/bundled-skills/skill-management/TOOLS.json +10 -0
  314. package/src/config/call-site-defaults.ts +0 -2
  315. package/src/config/feature-flag-registry.json +16 -8
  316. package/src/config/loader.ts +11 -0
  317. package/src/config/schema.ts +9 -1
  318. package/src/config/schemas/__tests__/memory-v2.test.ts +2 -2
  319. package/src/config/schemas/__tests__/memory-v3.test.ts +64 -0
  320. package/src/config/schemas/call-site-catalog.ts +0 -12
  321. package/src/config/schemas/calls.ts +8 -0
  322. package/src/config/schemas/elevenlabs.ts +1 -1
  323. package/src/config/schemas/llm.ts +0 -2
  324. package/src/config/schemas/memory-v2.ts +1 -1
  325. package/src/config/schemas/memory-v3.ts +82 -0
  326. package/src/config/schemas/monitoring.ts +61 -0
  327. package/src/config/schemas/schedules.ts +35 -0
  328. package/src/config/seed-inference-profiles.ts +2 -2
  329. package/src/config/skills-plugin-filter.test.ts +96 -0
  330. package/src/config/skills.ts +26 -0
  331. package/src/contacts/__tests__/member-write-relay.test.ts +156 -4
  332. package/src/contacts/canonical-guardian-store.ts +5 -0
  333. package/src/contacts/contact-store.ts +81 -35
  334. package/src/contacts/contacts-write.ts +50 -25
  335. package/src/contacts/member-write-relay.ts +115 -31
  336. package/src/contacts/types.ts +0 -8
  337. package/src/context/compactor.ts +29 -15
  338. package/src/context/outbound-sanitize.ts +202 -0
  339. package/src/daemon/__tests__/abort-null-controller.test.ts +167 -0
  340. package/src/daemon/__tests__/conversation-tool-setup-plugin-scope.test.ts +122 -0
  341. package/src/daemon/__tests__/embedding-reconcile.test.ts +99 -16
  342. package/src/daemon/__tests__/wake-conversation-ops-completion.test.ts +107 -0
  343. package/src/daemon/abort-watchdog.ts +7 -0
  344. package/src/daemon/app-source-watcher.ts +9 -0
  345. package/src/daemon/assistant-attachments.ts +42 -0
  346. package/src/daemon/conversation-agent-loop-handlers.ts +256 -147
  347. package/src/daemon/conversation-agent-loop.ts +61 -68
  348. package/src/daemon/conversation-error.ts +1 -15
  349. package/src/daemon/conversation-history.ts +32 -236
  350. package/src/daemon/conversation-initial-prompt.ts +77 -0
  351. package/src/daemon/conversation-lifecycle.ts +25 -1
  352. package/src/daemon/conversation-messaging.ts +2 -2
  353. package/src/daemon/conversation-plugin-scope.ts +38 -0
  354. package/src/daemon/conversation-process.ts +50 -14
  355. package/src/daemon/conversation-runtime-assembly.ts +36 -6
  356. package/src/daemon/conversation-skill-tools.ts +19 -3
  357. package/src/daemon/conversation-store.ts +3 -3
  358. package/src/daemon/conversation-surfaces.ts +171 -0
  359. package/src/daemon/conversation-tool-setup.test.ts +95 -0
  360. package/src/daemon/conversation-tool-setup.ts +116 -1
  361. package/src/daemon/conversation-turn-finalize.ts +211 -0
  362. package/src/daemon/conversation.ts +92 -1
  363. package/src/daemon/disk-pressure-policy.ts +2 -1
  364. package/src/daemon/event-loop-watchdog.ts +22 -6
  365. package/src/daemon/external-plugins-bootstrap.ts +47 -134
  366. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +1 -1
  367. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +26 -14
  368. package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +27 -14
  369. package/src/daemon/handlers/__tests__/config-channels.test.ts +0 -5
  370. package/src/daemon/handlers/config-a2a.ts +7 -14
  371. package/src/daemon/handlers/config-channels.ts +2 -10
  372. package/src/daemon/handlers/conversation-history.ts +3 -1
  373. package/src/daemon/handlers/conversations.ts +0 -52
  374. package/src/daemon/handlers/shared.ts +4 -24
  375. package/src/daemon/handlers/skills.ts +4 -1
  376. package/src/daemon/host-cu-proxy.ts +1 -1
  377. package/src/daemon/lifecycle.ts +59 -472
  378. package/src/daemon/message-protocol.ts +3 -3
  379. package/src/daemon/message-provenance.ts +12 -14
  380. package/src/daemon/message-types/background-tools.ts +28 -0
  381. package/src/daemon/message-types/contacts.ts +2 -10
  382. package/src/daemon/message-types/conversations.ts +10 -0
  383. package/src/daemon/message-types/integrations.ts +2 -0
  384. package/src/daemon/message-types/surfaces.ts +1 -0
  385. package/src/daemon/message-types/sync.ts +1 -0
  386. package/src/daemon/pointer-conversation-trust.ts +23 -9
  387. package/src/daemon/pointer-turn-runner.ts +174 -0
  388. package/src/daemon/providers-setup.ts +16 -0
  389. package/src/daemon/shutdown-handlers.ts +139 -47
  390. package/src/daemon/skill-memory-refresh.ts +2 -2
  391. package/src/daemon/sqlite-corruption-watchdog.test.ts +217 -0
  392. package/src/daemon/sqlite-corruption-watchdog.ts +155 -0
  393. package/src/daemon/tool-setup-types.ts +8 -0
  394. package/src/daemon/trust-context.ts +21 -6
  395. package/src/daemon/turn-latency-tracker.test.ts +93 -0
  396. package/src/daemon/turn-latency-tracker.ts +38 -0
  397. package/src/daemon/wake-conversation-ops.ts +10 -8
  398. package/src/documents/document-comments-store.test.ts +7 -1
  399. package/src/documents/document-comments-store.ts +8 -0
  400. package/src/documents/document-store.ts +15 -0
  401. package/src/export/__tests__/transcript-formatter.test.ts +11 -0
  402. package/src/export/transcript-formatter.ts +49 -67
  403. package/src/home/__tests__/feed-writer.test.ts +247 -1
  404. package/src/home/conversation-starter-checkpoints.ts +1 -0
  405. package/src/home/feed-writer.ts +61 -0
  406. package/src/home/job-handlers/conversation-starters.ts +1 -0
  407. package/src/hooks/hook-loader.ts +12 -0
  408. package/src/hooks/registry.ts +32 -4
  409. package/src/inbound/public-ingress-urls.ts +0 -17
  410. package/src/ipc/__tests__/contacts-info-ipc.test.ts +285 -0
  411. package/src/ipc/assistant-server.ts +28 -7
  412. package/src/ipc/routes/__tests__/contacts-mirror-apply-atomicity.test.ts +62 -0
  413. package/src/ipc/routes/__tests__/contacts-mirror-apply.test.ts +272 -0
  414. package/src/ipc/routes/__tests__/contacts-mirror-ipc-routes.test.ts +322 -0
  415. package/src/ipc/routes/__tests__/invite-ipc-routes.test.ts +60 -4
  416. package/src/ipc/routes/contacts-info-ipc-routes.ts +239 -0
  417. package/src/ipc/routes/contacts-mirror-ipc-routes.ts +202 -0
  418. package/src/ipc/routes/db-proxy.ts +5 -2
  419. package/src/ipc/routes/invite-ipc-routes.ts +53 -39
  420. package/src/jobs/worker.ts +9 -2
  421. package/src/live-voice/__tests__/live-voice-archive.test.ts +10 -1
  422. package/src/live-voice/live-voice-archive.ts +5 -0
  423. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +22 -6
  424. package/src/messaging/providers/channel-transport.ts +6 -0
  425. package/src/messaging/providers/index.ts +3 -0
  426. package/src/messaging/providers/slack/api.test.ts +49 -1
  427. package/src/messaging/providers/slack/api.ts +110 -0
  428. package/src/messaging/providers/slack/send.test.ts +85 -0
  429. package/src/messaging/providers/slack/send.ts +100 -28
  430. package/src/messaging/providers/slack/transport.ts +7 -13
  431. package/src/messaging/providers/whatsapp/send.ts +29 -9
  432. package/src/monitoring/__tests__/control.test.ts +235 -0
  433. package/src/monitoring/__tests__/sample-ring-buffer.test.ts +96 -0
  434. package/src/monitoring/control.ts +105 -0
  435. package/src/monitoring/resource-sampler.ts +258 -0
  436. package/src/monitoring/sample-ring-buffer.ts +108 -0
  437. package/src/monitoring/worker.ts +94 -0
  438. package/src/notifications/README.md +1 -1
  439. package/src/notifications/access-request-copy.ts +94 -23
  440. package/src/notifications/adapters/slack.ts +89 -21
  441. package/src/notifications/approval-card-builder.ts +33 -18
  442. package/src/notifications/approval-card-data.ts +48 -15
  443. package/src/notifications/broadcaster.ts +25 -16
  444. package/src/notifications/decision-engine.ts +131 -32
  445. package/src/notifications/guardian-question-mode.ts +58 -10
  446. package/src/permissions/approval-policy.ts +8 -1
  447. package/src/permissions/checker.ts +2 -0
  448. package/src/permissions/gateway-threshold-reader.ts +7 -6
  449. package/src/permissions/ipc-risk-types.ts +1 -0
  450. package/src/permissions/secret-prompter.ts +11 -3
  451. package/src/permissions/types.ts +2 -2
  452. package/src/persistence/__tests__/conversation-queries-search.test.ts +397 -0
  453. package/src/persistence/__tests__/conversation-search-lexical.test.ts +103 -0
  454. package/src/persistence/__tests__/jobs-worker-memory-disabled.test.ts +95 -0
  455. package/src/persistence/__tests__/slow-query-log.test.ts +266 -0
  456. package/src/persistence/__tests__/slow-sync-log.test.ts +155 -0
  457. package/src/persistence/a2a-invite-store.ts +205 -0
  458. package/src/persistence/attachments-store.ts +94 -10
  459. package/src/persistence/checkpoints.ts +45 -0
  460. package/src/persistence/conversation-crud.ts +453 -260
  461. package/src/persistence/conversation-display-order-migration.ts +8 -2
  462. package/src/persistence/conversation-group-migration.test.ts +8 -0
  463. package/src/persistence/conversation-group-migration.ts +9 -0
  464. package/src/persistence/conversation-queries.ts +244 -125
  465. package/src/persistence/conversation-row-batch-delete.ts +9 -5
  466. package/src/persistence/conversation-search-lexical.ts +49 -0
  467. package/src/persistence/db-async-query.ts +42 -0
  468. package/src/persistence/db-connection.ts +13 -0
  469. package/src/persistence/delivery-channels.ts +0 -82
  470. package/src/persistence/delivery-crud.ts +27 -6
  471. package/src/persistence/embeddings/__tests__/embedding-identity.test.ts +18 -5
  472. package/src/persistence/embeddings/__tests__/messages-lexical-index.test.ts +334 -0
  473. package/src/persistence/embeddings/embedding-identity.ts +17 -5
  474. package/src/persistence/embeddings/embedding-runtime-manager.ts +31 -0
  475. package/src/persistence/embeddings/messages-lexical-index.ts +396 -0
  476. package/src/persistence/embeddings/qdrant-client.ts +81 -5
  477. package/src/persistence/group-crud.ts +13 -1
  478. package/src/persistence/job-handlers/__tests__/message-lexical-backfill.test.ts +506 -0
  479. package/src/persistence/job-handlers/__tests__/message-lexical-enqueue.test.ts +164 -0
  480. package/src/persistence/job-handlers/__tests__/message-lexical.test.ts +259 -0
  481. package/src/persistence/job-handlers/cleanup.ts +57 -9
  482. package/src/persistence/job-handlers/message-lexical-backfill.ts +187 -0
  483. package/src/persistence/job-handlers/message-lexical.ts +251 -0
  484. package/src/persistence/jobs-store.ts +102 -8
  485. package/src/persistence/jobs-worker.ts +68 -28
  486. package/src/persistence/llm-request-log-store.ts +35 -19
  487. package/src/persistence/llm-usage-store.ts +11 -0
  488. package/src/persistence/memory-lifecycle-hooks.test.ts +65 -0
  489. package/src/persistence/memory-lifecycle-hooks.ts +55 -1
  490. package/src/persistence/migrations/028-call-session-mode.ts +1 -1
  491. package/src/persistence/migrations/116-messages-fts.ts +8 -1
  492. package/src/persistence/migrations/311-create-subagents-table.ts +50 -0
  493. package/src/persistence/migrations/312-drop-inbox-conversation-state-table.ts +15 -0
  494. package/src/persistence/migrations/313-drop-messages-fts.ts +17 -0
  495. package/src/persistence/migrations/314-add-conversation-enabled-plugins.ts +25 -0
  496. package/src/persistence/migrations/315-create-a2a-invites.ts +58 -0
  497. package/src/persistence/migrations/316-drop-contact-channels-invite-id.ts +28 -0
  498. package/src/persistence/migrations/317-canonical-guardian-requester-signals.ts +24 -0
  499. package/src/persistence/migrations/318-drop-contact-channel-telemetry.ts +32 -0
  500. package/src/persistence/migrations/__tests__/315-create-a2a-invites.test.ts +161 -0
  501. package/src/persistence/migrations/__tests__/316-drop-contact-channels-invite-id.test.ts +156 -0
  502. package/src/persistence/migrations/__tests__/318-drop-contact-channel-telemetry.test.ts +134 -0
  503. package/src/persistence/migrations/__tests__/migration-prefix-guard.test.ts +99 -0
  504. package/src/persistence/raw-query.ts +50 -11
  505. package/src/persistence/schema/a2a.ts +14 -0
  506. package/src/persistence/schema/contacts.ts +0 -62
  507. package/src/persistence/schema/conversation-groups.ts +12 -0
  508. package/src/persistence/schema/conversations.ts +2 -0
  509. package/src/persistence/schema/documents.ts +65 -0
  510. package/src/persistence/schema/guardian.ts +4 -0
  511. package/src/persistence/schema/index.ts +4 -0
  512. package/src/persistence/schema/memory-injection.ts +62 -0
  513. package/src/persistence/schema/workflows.ts +55 -0
  514. package/src/persistence/slow-query-log.ts +437 -0
  515. package/src/persistence/slow-sync-log.ts +204 -0
  516. package/src/persistence/steps.ts +16 -0
  517. package/src/persistence/subagent-store.ts +125 -0
  518. package/src/persistence/worker-control.ts +45 -238
  519. package/src/plugin-api/event-hub-facade.ts +172 -0
  520. package/src/plugin-api/index.ts +9 -1
  521. package/src/plugin-api/types.ts +40 -9
  522. package/src/plugins/defaults/default-plugin-names.ts +60 -0
  523. package/src/plugins/defaults/index.ts +19 -29
  524. package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +46 -67
  525. package/src/plugins/defaults/memory/__tests__/db-async-query.test.ts +36 -1
  526. package/src/plugins/defaults/memory/__tests__/find-most-recent-retrospective-for.test.ts +2 -4
  527. package/src/plugins/defaults/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +2 -2
  528. package/src/plugins/defaults/memory/__tests__/jobs-worker-v2-schedule.test.ts +9 -0
  529. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +7 -4
  530. package/src/plugins/defaults/memory/config.ts +11 -0
  531. package/src/plugins/defaults/memory/context-search/agent-runner.ts +1 -1
  532. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +124 -113
  533. package/src/plugins/defaults/memory/embeddings.ts +77 -0
  534. package/src/plugins/defaults/memory/find-most-recent-retrospective-for.ts +66 -0
  535. package/src/plugins/defaults/memory/frontmatter.ts +50 -0
  536. package/src/plugins/defaults/memory/graph/bootstrap.test.ts +36 -10
  537. package/src/plugins/defaults/memory/graph/bootstrap.ts +6 -5
  538. package/src/plugins/defaults/memory/graph/consolidation.ts +3 -5
  539. package/src/plugins/defaults/memory/graph/extraction.ts +3 -6
  540. package/src/plugins/defaults/memory/graph/graph-search.ts +12 -23
  541. package/src/plugins/defaults/memory/graph/narrative.ts +3 -5
  542. package/src/plugins/defaults/memory/graph/pattern-scan.ts +3 -5
  543. package/src/plugins/defaults/memory/graph/retriever.test.ts +1 -6
  544. package/src/plugins/defaults/memory/graph/retriever.ts +5 -10
  545. package/src/plugins/defaults/memory/graph/tools.ts +1 -1
  546. package/src/plugins/defaults/memory/hooks/init.ts +31 -5
  547. package/src/plugins/defaults/memory/indexer.ts +2 -3
  548. package/src/plugins/defaults/memory/injectors.ts +2 -2
  549. package/src/plugins/defaults/memory/job-handler-registration.ts +96 -0
  550. package/src/plugins/defaults/memory/job-handlers/backfill.ts +4 -26
  551. package/src/plugins/defaults/memory/job-handlers/embedding.test.ts +6 -19
  552. package/src/plugins/defaults/memory/job-handlers/embedding.ts +9 -22
  553. package/src/plugins/defaults/memory/job-handlers/index-maintenance.ts +2 -3
  554. package/src/plugins/defaults/memory/job-handlers.ts +13 -12
  555. package/src/plugins/defaults/memory/jobs/embed-concept-page.ts +1 -1
  556. package/src/plugins/defaults/memory/llm-helpers.ts +76 -0
  557. package/src/plugins/defaults/memory/memory-retrospective-job.ts +8 -5
  558. package/src/plugins/defaults/memory/memory-retrospective-startup-cleanup.ts +2 -2
  559. package/src/plugins/defaults/memory/memory-v2-concept-frequency.ts +2 -0
  560. package/src/plugins/defaults/memory/persistence-hooks.ts +46 -1
  561. package/src/plugins/defaults/memory/pkb/pkb-index.ts +1 -5
  562. package/src/plugins/defaults/memory/pkb/pkb-search.ts +2 -2
  563. package/src/plugins/defaults/memory/routes/__tests__/memory-v2-simulate-route.test.ts +4 -6
  564. package/src/plugins/defaults/memory/routes/memory-item-routes.ts +1 -1
  565. package/src/plugins/defaults/memory/search/semantic.ts +2 -2
  566. package/src/plugins/defaults/memory/startup.ts +244 -0
  567. package/src/plugins/defaults/memory/task-memory-cleanup.ts +8 -0
  568. package/src/{daemon → plugins/defaults/memory/v2}/__tests__/memory-v2-startup.test.ts +6 -6
  569. package/src/plugins/defaults/memory/v2/__tests__/migration.test.ts +2 -13
  570. package/src/plugins/defaults/memory/v2/__tests__/router.test.ts +4 -5
  571. package/src/plugins/defaults/memory/v2/__tests__/sweep-job.test.ts +1 -8
  572. package/src/plugins/defaults/memory/v2/activation.ts +2 -4
  573. package/src/plugins/defaults/memory/v2/cli-command-store.ts +2 -4
  574. package/src/plugins/defaults/memory/v2/frontmatter-sweep.ts +1 -1
  575. package/src/{daemon → plugins/defaults/memory/v2}/memory-v2-startup.test.ts +8 -8
  576. package/src/{daemon → plugins/defaults/memory/v2}/memory-v2-startup.ts +20 -22
  577. package/src/plugins/defaults/memory/v2/migration.ts +2 -5
  578. package/src/plugins/defaults/memory/v2/page-store.ts +1 -1
  579. package/src/plugins/defaults/memory/v2/qdrant.ts +2 -3
  580. package/src/plugins/defaults/memory/v2/router.ts +5 -5
  581. package/src/plugins/defaults/memory/v2/sim.ts +2 -4
  582. package/src/plugins/defaults/memory/v2/skill-store.ts +2 -4
  583. package/src/plugins/defaults/memory/v2/sweep-job.ts +6 -6
  584. package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +11 -9
  585. package/src/plugins/defaults/memory/v3/__tests__/dense.test.ts +107 -2
  586. package/src/plugins/defaults/memory/v3/__tests__/gate-flag.test.ts +69 -0
  587. package/src/plugins/defaults/memory/v3/__tests__/gate.test.ts +181 -0
  588. package/src/plugins/defaults/memory/v3/__tests__/live-integration.test.ts +7 -3
  589. package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +345 -8
  590. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +1 -4
  591. package/src/plugins/defaults/memory/v3/__tests__/section-needle.test.ts +64 -0
  592. package/src/plugins/defaults/memory/v3/__tests__/shadow-integration.test.ts +18 -13
  593. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +62 -1
  594. package/src/plugins/defaults/memory/v3/card.ts +1 -4
  595. package/src/plugins/defaults/memory/v3/dense.ts +60 -25
  596. package/src/plugins/defaults/memory/v3/edge.ts +1 -1
  597. package/src/plugins/defaults/memory/v3/gate-flag.ts +20 -0
  598. package/src/plugins/defaults/memory/v3/gate.ts +158 -0
  599. package/src/plugins/defaults/memory/v3/maintain-job.ts +2 -4
  600. package/src/plugins/defaults/memory/v3/orchestrate.ts +186 -25
  601. package/src/plugins/defaults/memory/v3/pool-select.test.ts +7 -9
  602. package/src/plugins/defaults/memory/v3/pool-select.ts +6 -6
  603. package/src/plugins/defaults/memory/v3/prune.ts +2 -2
  604. package/src/plugins/defaults/memory/v3/section-dense-store.ts +8 -11
  605. package/src/plugins/defaults/memory/v3/section-needle.ts +27 -7
  606. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +10 -0
  607. package/src/plugins/defaults/memory/v3-eval/eval-packets.ts +1 -4
  608. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +5 -0
  609. package/src/plugins/defaults/turn-context/injectors.ts +1 -0
  610. package/src/plugins/defaults/turn-context/unified-turn-context.ts +12 -0
  611. package/src/plugins/injector-registry.ts +13 -2
  612. package/src/plugins/mtime-cache.ts +36 -47
  613. package/src/plugins/pipeline.ts +15 -1
  614. package/src/plugins/types.ts +14 -23
  615. package/src/prompts/__tests__/system-prompt.test.ts +170 -32
  616. package/src/prompts/persona-resolver.ts +13 -9
  617. package/src/prompts/system-prompt.ts +30 -9
  618. package/src/prompts/templates/BOOTSTRAP.md +16 -119
  619. package/src/prompts/templates/SOUL.md +43 -13
  620. package/src/prompts/templates/system-sections.ts +32 -27
  621. package/src/prompts/templates/users/default.md +19 -0
  622. package/src/providers/__tests__/placeholder-sentinels.test.ts +56 -0
  623. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +96 -1
  624. package/src/providers/model-intents.ts +2 -2
  625. package/src/providers/openai/chat-completions-provider.ts +14 -1
  626. package/src/providers/openai/responses-provider.ts +18 -0
  627. package/src/providers/placeholder-sentinels.ts +18 -8
  628. package/src/providers/retry.ts +1 -0
  629. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +0 -64
  630. package/src/providers/speech-to-text/__tests__/resolve.test.ts +92 -79
  631. package/src/providers/speech-to-text/deepgram-realtime.test.ts +98 -0
  632. package/src/providers/speech-to-text/deepgram-realtime.ts +62 -3
  633. package/src/providers/speech-to-text/provider-catalog.ts +0 -89
  634. package/src/providers/speech-to-text/resolve.ts +55 -0
  635. package/src/providers/tool-progress-events.test.ts +50 -0
  636. package/src/providers/tool-progress-events.ts +99 -0
  637. package/src/providers/types.ts +1 -1
  638. package/src/providers/vellum-model-routing.test.ts +51 -0
  639. package/src/providers/vellum-model-routing.ts +84 -0
  640. package/src/runtime/AGENTS.md +5 -4
  641. package/src/runtime/__tests__/agent-wake.test.ts +158 -1
  642. package/src/runtime/__tests__/channel-verification-service.test.ts +2 -2
  643. package/src/runtime/__tests__/trust-verdict-consumer.test.ts +37 -8
  644. package/src/runtime/access-request-helper.ts +154 -3
  645. package/src/runtime/actor-trust-resolver.ts +7 -21
  646. package/src/runtime/agent-wake.ts +41 -1
  647. package/src/runtime/assistant-event.ts +86 -12
  648. package/src/runtime/auth/__tests__/route-policy.test.ts +31 -0
  649. package/src/runtime/channel-approval-types.ts +52 -2
  650. package/src/runtime/channel-approvals.ts +23 -8
  651. package/src/runtime/channel-invite-transport.ts +2 -2
  652. package/src/runtime/channel-invite-transports/voice.ts +1 -1
  653. package/src/runtime/channel-readiness-service.ts +3 -5
  654. package/src/runtime/channel-retry-sweep.ts +18 -24
  655. package/src/runtime/channel-verification-service.ts +9 -11
  656. package/src/runtime/finalize-event-delivery.ts +30 -16
  657. package/src/runtime/guardian-action-service.ts +10 -11
  658. package/src/runtime/guardian-reply-router.ts +122 -21
  659. package/src/runtime/http-server.ts +44 -101
  660. package/src/runtime/introduction-policy.test.ts +214 -0
  661. package/src/runtime/introduction-policy.ts +284 -0
  662. package/src/runtime/invite-service.ts +104 -361
  663. package/src/runtime/local-actor-identity.test.ts +38 -0
  664. package/src/runtime/local-actor-identity.ts +14 -0
  665. package/src/runtime/middleware/__tests__/rate-limiter.test.ts +22 -0
  666. package/src/runtime/middleware/rate-limiter.ts +30 -0
  667. package/src/runtime/middleware/twilio-validation.ts +5 -7
  668. package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +28 -0
  669. package/src/runtime/migrations/origin-mode.ts +6 -6
  670. package/src/runtime/pre-first-message-gate.ts +1 -0
  671. package/src/runtime/routes/__tests__/channel-verification-revoke.test.ts +6 -35
  672. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +24 -3
  673. package/src/runtime/routes/__tests__/contact-routes.test.ts +80 -15
  674. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +7 -1
  675. package/src/runtime/routes/__tests__/global-search-routes.test.ts +0 -3
  676. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +142 -1
  677. package/src/runtime/routes/__tests__/invite-relay-routes.test.ts +119 -30
  678. package/src/runtime/routes/__tests__/migration-import-allowed-hosts-env.test.ts +70 -0
  679. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +194 -0
  680. package/src/runtime/routes/__tests__/plugins-routes.test.ts +755 -29
  681. package/src/runtime/routes/__tests__/retrospective-routes.test.ts +4 -0
  682. package/src/runtime/routes/__tests__/schedule-routes-create.test.ts +70 -0
  683. package/src/runtime/routes/__tests__/schedule-worker-routes.test.ts +161 -0
  684. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +1 -1
  685. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +1 -1
  686. package/src/runtime/routes/acp-routes.ts +15 -3
  687. package/src/runtime/routes/app-management-routes.ts +4 -7
  688. package/src/runtime/routes/approval-routes.ts +4 -0
  689. package/src/runtime/routes/attachment-routes.ts +54 -3
  690. package/src/runtime/routes/avatar-routes.ts +0 -4
  691. package/src/runtime/routes/background-tool-routes.ts +34 -5
  692. package/src/runtime/routes/btw-routes.ts +32 -4
  693. package/src/runtime/routes/channel-route-shared.ts +18 -14
  694. package/src/runtime/routes/channel-verification-routes.ts +0 -3
  695. package/src/runtime/routes/contact-routes.ts +298 -117
  696. package/src/runtime/routes/conversation-list-routes.ts +6 -0
  697. package/src/runtime/routes/conversation-management-routes.ts +74 -37
  698. package/src/runtime/routes/conversation-query-routes.ts +5 -2
  699. package/src/runtime/routes/conversation-routes.ts +222 -82
  700. package/src/runtime/routes/conversations-import-routes.ts +8 -1
  701. package/src/runtime/routes/credential-prompt-routes.ts +20 -5
  702. package/src/runtime/routes/debug-routes.ts +1 -0
  703. package/src/runtime/routes/documents-routes.ts +5 -2
  704. package/src/runtime/routes/empty-state-greeting-cache.ts +37 -10
  705. package/src/runtime/routes/global-search-routes.ts +2 -1
  706. package/src/runtime/routes/guardian-approval-interception.ts +51 -0
  707. package/src/runtime/routes/home-feed-routes.ts +60 -1
  708. package/src/runtime/routes/identity-routes.ts +4 -87
  709. package/src/runtime/routes/inbound-message-handler.ts +82 -41
  710. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +430 -57
  711. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +302 -572
  712. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +76 -270
  713. package/src/runtime/routes/inbound-stages/background-dispatch.ts +21 -116
  714. package/src/runtime/routes/inbound-stages/edit-intercept.ts +9 -1
  715. package/src/runtime/routes/inbound-stages/escalation-intercept.test.ts +158 -0
  716. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +22 -1
  717. package/src/runtime/routes/index.ts +6 -0
  718. package/src/runtime/routes/inference-provider-connection-routes.ts +22 -16
  719. package/src/runtime/routes/internal-twilio-routes.ts +0 -15
  720. package/src/runtime/routes/messages-lexical-routes.ts +75 -0
  721. package/src/runtime/routes/migration-routes.ts +69 -6
  722. package/src/runtime/routes/monitoring-routes.ts +237 -0
  723. package/src/runtime/routes/plugins-routes.ts +329 -8
  724. package/src/runtime/routes/schedule-routes.ts +49 -8
  725. package/src/runtime/routes/schedule-worker-routes.ts +210 -0
  726. package/src/runtime/routes/secret-routes.ts +1 -1
  727. package/src/runtime/routes/surface-conversation-resolver.ts +1 -0
  728. package/src/runtime/services/__tests__/analyze-conversation.test.ts +54 -9
  729. package/src/runtime/services/__tests__/auto-analysis-enqueue.test.ts +30 -6
  730. package/src/runtime/services/__tests__/conversation-analyze-job.test.ts +43 -8
  731. package/src/runtime/services/__tests__/conversation-serializer.test.ts +20 -0
  732. package/src/runtime/services/analyze-conversation.ts +14 -4
  733. package/src/runtime/services/conversation-serializer.ts +5 -0
  734. package/src/runtime/slack-no-response.ts +47 -0
  735. package/src/runtime/slack-reply-session.test.ts +633 -0
  736. package/src/runtime/slack-reply-session.ts +382 -0
  737. package/src/runtime/slack-task-progress.ts +95 -0
  738. package/src/runtime/sync/resource-sync-events.ts +25 -0
  739. package/src/runtime/tool-grant-request-helper.ts +17 -1
  740. package/src/runtime/trust-class.test.ts +141 -0
  741. package/src/runtime/trust-class.ts +121 -0
  742. package/src/runtime/trust-verdict-consumer.ts +7 -6
  743. package/src/schedule/__tests__/run-script.test.ts +21 -0
  744. package/src/schedule/__tests__/worker-control.test.ts +241 -0
  745. package/src/schedule/run-script.ts +13 -6
  746. package/src/schedule/schedule-usage-store.ts +1 -0
  747. package/src/schedule/scheduler.ts +178 -127
  748. package/src/schedule/worker-control.ts +118 -0
  749. package/src/schedule/worker.ts +131 -0
  750. package/src/skills/__tests__/categories-cache.test.ts +38 -0
  751. package/src/skills/categories-cache.ts +40 -5
  752. package/src/skills/managed-store.ts +41 -6
  753. package/src/skills/validate-input.ts +47 -0
  754. package/src/stt/types.ts +5 -9
  755. package/src/subagent/manager.ts +267 -39
  756. package/src/subagent/types.ts +6 -2
  757. package/src/subagent/validate-allowlists.ts +65 -0
  758. package/src/telemetry/watchdog-direct-emit.test.ts +109 -0
  759. package/src/telemetry/watchdog-direct-emit.ts +89 -0
  760. package/src/tools/acp/list-agents.test.ts +0 -6
  761. package/src/tools/background-tool-registry.ts +61 -0
  762. package/src/tools/document/document-comment-tool.test.ts +4 -0
  763. package/src/tools/host-terminal/host-shell.test.ts +397 -0
  764. package/src/tools/host-terminal/host-shell.ts +203 -14
  765. package/src/tools/skills/find-similar-skills.test.ts +152 -1
  766. package/src/tools/skills/find-similar-skills.ts +30 -4
  767. package/src/tools/skills/load.ts +36 -0
  768. package/src/tools/skills/scaffold-managed.ts +66 -34
  769. package/src/tools/skills/skill-script-runner.ts +17 -0
  770. package/src/tools/skills/skill-tool-factory.ts +5 -1
  771. package/src/tools/terminal/safe-env.ts +3 -0
  772. package/src/tools/terminal/shell.test.ts +177 -0
  773. package/src/tools/terminal/shell.ts +98 -4
  774. package/src/tools/tool-approval-handler.ts +106 -29
  775. package/{node_modules/@vellumai/skill-host-contracts/src → src/tools}/tool-types.ts +16 -17
  776. package/src/tools/types.ts +29 -17
  777. package/src/tools/ui-surface/definitions.ts +3 -1
  778. package/src/tts/__tests__/provider-adapters.test.ts +37 -0
  779. package/src/tts/__tests__/provider-catalog.test.ts +21 -0
  780. package/src/tts/provider-catalog.ts +40 -9
  781. package/src/tts/providers/fish-audio-provider.ts +19 -10
  782. package/src/tts/types.ts +3 -4
  783. package/src/util/__tests__/process-tree.test.ts +10 -2
  784. package/src/util/__tests__/text-spacing.test.ts +53 -0
  785. package/src/util/cgroup-memory.ts +142 -0
  786. package/src/util/image-conversion.ts +281 -0
  787. package/src/util/phone.ts +1 -8
  788. package/src/util/platform.ts +33 -0
  789. package/src/util/process-tree.ts +10 -2
  790. package/src/util/text-spacing.ts +53 -0
  791. package/src/util/worker-process.ts +271 -0
  792. package/src/workflows/journal-store.ts +16 -1
  793. package/src/workflows/library.test.ts +35 -20
  794. package/src/workflows/library.ts +105 -66
  795. package/src/workspace/git-service.ts +1 -0
  796. package/src/workspace/migrations/011-backfill-installation-id.ts +83 -31
  797. package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +41 -22
  798. package/src/workspace/migrations/070-memory-v2-summary-schema-rebuild.ts +1 -1
  799. package/src/workspace/migrations/118-seed-now-md.ts +64 -0
  800. package/src/workspace/migrations/119-strip-persisted-memory-v3-tuning-defaults.ts +169 -0
  801. package/src/workspace/migrations/120-revise-onboarding-threads.ts +47 -0
  802. package/src/workspace/migrations/121-seed-default-user-guardrails.ts +82 -0
  803. package/src/workspace/migrations/122-relocate-default-user-boundary.ts +111 -0
  804. package/src/workspace/migrations/123-swap-quality-profile-to-fable.ts +85 -0
  805. package/src/workspace/migrations/__tests__/119-strip-persisted-memory-v3-tuning-defaults.test.ts +181 -0
  806. package/src/workspace/migrations/registry.ts +12 -0
  807. package/drizzle/0000_dizzy_maggott.sql +0 -301
  808. package/drizzle/meta/0000_snapshot.json +0 -1933
  809. package/drizzle/meta/_journal.json +0 -13
  810. package/node_modules/@vellumai/skill-host-contracts/__tests__/client.test.ts +0 -887
  811. package/node_modules/@vellumai/skill-host-contracts/bun.lock +0 -24
  812. package/node_modules/@vellumai/skill-host-contracts/package.json +0 -18
  813. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -97
  814. package/node_modules/@vellumai/skill-host-contracts/src/client.ts +0 -1349
  815. package/node_modules/@vellumai/skill-host-contracts/src/index.ts +0 -6
  816. package/node_modules/@vellumai/skill-host-contracts/src/runtime-mode.ts +0 -11
  817. package/node_modules/@vellumai/skill-host-contracts/src/server-message.ts +0 -32
  818. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +0 -333
  819. package/node_modules/@vellumai/skill-host-contracts/tsconfig.json +0 -20
  820. package/node_modules/@vellumai/skill-host-contracts/tsconfig.test.json +0 -12
  821. package/src/__tests__/invite-redemption-service.test.ts +0 -1180
  822. package/src/__tests__/invite-service-ipc.test.ts +0 -350
  823. package/src/__tests__/job-handler-registry-guard.test.ts +0 -125
  824. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +0 -203
  825. package/src/__tests__/relay-server.test.ts +0 -6153
  826. package/src/__tests__/telephony-stt-routing.test.ts +0 -329
  827. package/src/__tests__/voice-invite-redemption.test.ts +0 -702
  828. package/src/calls/relay-server.ts +0 -2463
  829. package/src/calls/telephony-stt-routing.ts +0 -145
  830. package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +0 -128
  831. package/src/daemon/__tests__/daemon-skill-host.test.ts +0 -273
  832. package/src/daemon/daemon-skill-host.ts +0 -262
  833. package/src/daemon/message-types/meet.ts +0 -143
  834. package/src/daemon/shutdown-registry.ts +0 -40
  835. package/src/ipc/__tests__/skill-server-bidirectional.test.ts +0 -253
  836. package/src/ipc/__tests__/skill-server.test.ts +0 -182
  837. package/src/ipc/skill-ipc-types.ts +0 -54
  838. package/src/ipc/skill-routes/__tests__/config.test.ts +0 -146
  839. package/src/ipc/skill-routes/__tests__/events-ipc.test.ts +0 -448
  840. package/src/ipc/skill-routes/__tests__/identity.test.ts +0 -62
  841. package/src/ipc/skill-routes/__tests__/log.test.ts +0 -133
  842. package/src/ipc/skill-routes/__tests__/memory.test.ts +0 -203
  843. package/src/ipc/skill-routes/__tests__/platform.test.ts +0 -111
  844. package/src/ipc/skill-routes/__tests__/providers.test.ts +0 -265
  845. package/src/ipc/skill-routes/__tests__/registries.test.ts +0 -400
  846. package/src/ipc/skill-routes/config.ts +0 -47
  847. package/src/ipc/skill-routes/events.ts +0 -147
  848. package/src/ipc/skill-routes/identity.ts +0 -21
  849. package/src/ipc/skill-routes/index.ts +0 -37
  850. package/src/ipc/skill-routes/log.ts +0 -40
  851. package/src/ipc/skill-routes/memory.ts +0 -92
  852. package/src/ipc/skill-routes/platform.ts +0 -39
  853. package/src/ipc/skill-routes/providers.ts +0 -162
  854. package/src/ipc/skill-routes/registries.ts +0 -338
  855. package/src/ipc/skill-server.ts +0 -831
  856. package/src/ipc/skill-socket-path.ts +0 -20
  857. package/src/jobs/register-job-handlers.ts +0 -74
  858. package/src/persistence/invite-store.ts +0 -481
  859. package/src/plugins/job-handler-registry.ts +0 -94
  860. package/src/providers/cache-control.ts +0 -26
  861. package/src/runtime/__tests__/runtime-mode.test.ts +0 -62
  862. package/src/runtime/invite-redemption-service.ts +0 -828
  863. package/src/runtime/invite-redemption-templates.ts +0 -46
  864. package/src/runtime/runtime-mode.ts +0 -40
  865. package/src/runtime/slack-dm-text-delivery.ts +0 -177
  866. package/src/util/voice-code.ts +0 -31
@@ -1,31 +1,27 @@
1
1
  /**
2
2
  * Ingress ACL enforcement stage: resolves the inbound actor to a member
3
- * record, enforces allow/deny/escalate policies, handles invite token
4
- * intercepts, and notifies the guardian of denied access requests.
3
+ * record, enforces allow/deny/escalate policies, and notifies the guardian
4
+ * of denied access requests.
5
+ *
6
+ * Invite code/token redemption is intercepted at gateway ingress; redeemed
7
+ * messages never reach this stage.
5
8
  */
6
9
  import type { AdmissionPolicy, SourceMetadata } from "@vellumai/gateway-client";
10
+ import { isTrustClass } from "@vellumai/gateway-client";
7
11
 
8
- import { isInviteCodeRedemptionEnabled } from "../../../channels/config.js";
9
12
  import type { ChannelId } from "../../../channels/types.js";
10
13
  import { getGuardianDelivery } from "../../../contacts/guardian-delivery-reader.js";
11
14
  import { channelStatusToMemberStatus } from "../../../contacts/member-status.js";
12
15
  import { MESSAGE_PREVIEW_MAX_LENGTH } from "../../../notifications/notification-utils.js";
13
- import {
14
- deleteInbound,
15
- recordInbound,
16
- } from "../../../persistence/delivery-crud.js";
17
- import { markProcessed } from "../../../persistence/delivery-status.js";
18
- import {
19
- findByInviteCodeHash,
20
- findByInviteCodeHashAnyChannel,
21
- } from "../../../persistence/invite-store.js";
22
16
  import { resolveGuardianName } from "../../../prompts/user-reference.js";
23
17
  import { getLogger } from "../../../util/logger.js";
24
18
  import { truncate } from "../../../util/truncate.js";
25
- import { hashVoiceCode } from "../../../util/voice-code.js";
26
- import { notifyGuardianOfAccessRequest } from "../../access-request-helper.js";
19
+ import {
20
+ isAccessRequestDenied,
21
+ isApprovalHandshakeInProgress,
22
+ notifyGuardianOfAccessRequest,
23
+ } from "../../access-request-helper.js";
27
24
  import { resolveAnchoredGuardian } from "../../anchored-guardian.js";
28
- import { getInviteAdapterRegistry } from "../../channel-invite-transport.js";
29
25
  import {
30
26
  createOutboundSession,
31
27
  findActiveSession,
@@ -33,11 +29,6 @@ import {
33
29
  resolveBootstrapToken,
34
30
  } from "../../channel-verification-service.js";
35
31
  import { deliverChannelReply } from "../../gateway-client.js";
36
- import {
37
- redeemInvite,
38
- redeemInviteByCode,
39
- } from "../../invite-redemption-service.js";
40
- import { getInviteRedemptionReply } from "../../invite-redemption-templates.js";
41
32
  import type { VerdictMember } from "../../trust-verdict-consumer.js";
42
33
  import { verdictMemberFromVerdict } from "../../trust-verdict-consumer.js";
43
34
 
@@ -63,6 +54,32 @@ async function resolveGuardianLabel(sourceChannel: ChannelId): Promise<string> {
63
54
  return resolveGuardianName(anchored?.displayName);
64
55
  }
65
56
 
57
+ /**
58
+ * Compose the requester-facing reply for a denied inbound, keyed on the
59
+ * access-request outcome. Single source for this copy — the not_a_member,
60
+ * inactive-member, and admission-floor deny lanes all route through it.
61
+ *
62
+ * - Handshake in progress: the guardian already approved and a verification
63
+ * code is live; tell the sender their next step. The code is DM'd directly
64
+ * to the requester on Slack but relayed via the guardian elsewhere, so the
65
+ * copy covers both.
66
+ * - Guardian notified: the standard "I'll let <guardian> know" copy.
67
+ * - Otherwise: the plain not-approved copy.
68
+ */
69
+ export async function composeAccessDenialReply(params: {
70
+ sourceChannel: ChannelId;
71
+ guardianNotified: boolean;
72
+ handshakeInProgress: boolean;
73
+ }): Promise<string> {
74
+ if (params.handshakeInProgress) {
75
+ return `Your access request was approved! Reply here with the 6-digit verification code to finish connecting — if you don't have it, ask ${await resolveGuardianLabel(params.sourceChannel)} for it.`;
76
+ }
77
+ if (params.guardianNotified) {
78
+ return `Hmm looks like you don't have access to talk to me. I'll let ${await resolveGuardianLabel(params.sourceChannel)} know you tried talking to me and get back to you.`;
79
+ }
80
+ return "Sorry, you haven't been approved to message this assistant.";
81
+ }
82
+
66
83
  // ---------------------------------------------------------------------------
67
84
  // Public API
68
85
  // ---------------------------------------------------------------------------
@@ -80,7 +97,6 @@ export interface AclEnforcementParams {
80
97
  actorUsername: string | undefined;
81
98
  replyCallbackUrl: string | undefined;
82
99
  assistantId: string;
83
- externalMessageId: string;
84
100
  /**
85
101
  * Effective admission policy for this request (gateway floor resolved with
86
102
  * any per-conversation override). When set, ACL skips its hard-deny paths
@@ -93,6 +109,35 @@ export interface AclEnforcementParams {
93
109
  * replies for senders who will be admitted by the floor stage anyway.
94
110
  */
95
111
  effectiveAdmissionPolicy?: AdmissionPolicy;
112
+ /**
113
+ * True when the inbound event is an interaction callback (e.g. a Slack
114
+ * Block Kit button press or a message_deleted sentinel) rather than a
115
+ * message the sender composed. Callbacks are decision attempts / lifecycle
116
+ * events, not access attempts: a denied callback must never mint a
117
+ * verification challenge or create an access request — a stale button
118
+ * press from an unrecognized sender would otherwise spawn a fresh
119
+ * Approve/Reject card the guardian already dealt with (LUM-2673). The
120
+ * deny itself (and its canned reply) still applies. Required so every
121
+ * caller makes the classification explicitly.
122
+ */
123
+ isCallbackInteraction: boolean;
124
+ }
125
+
126
+ /**
127
+ * Fail-closed / fail-safe deny result: soft-deny with NO stranger-lane side
128
+ * effects (no access-request card, no verification challenge, no canned
129
+ * reply). Used for unresolvable verdicts, where the sender must not be
130
+ * treated as a stranger.
131
+ */
132
+ function failClosedDeny(): AclResult {
133
+ return {
134
+ resolvedMember: null,
135
+ earlyResponse: {
136
+ accepted: true,
137
+ denied: true,
138
+ reason: "not_a_member",
139
+ },
140
+ };
96
141
  }
97
142
 
98
143
  export interface AclResult {
@@ -109,8 +154,8 @@ export interface AclResult {
109
154
 
110
155
  /**
111
156
  * Enforce ingress ACL rules: member lookup, non-member/inactive denial,
112
- * policy enforcement (allow/deny/escalate bypass), invite token intercepts,
113
- * and guardian notification for denied access.
157
+ * policy enforcement (allow/deny/escalate bypass), and guardian
158
+ * notification for denied access.
114
159
  */
115
160
  export async function enforceIngressAcl(
116
161
  params: AclEnforcementParams,
@@ -128,13 +173,15 @@ export async function enforceIngressAcl(
128
173
  actorUsername,
129
174
  replyCallbackUrl,
130
175
  assistantId,
131
- externalMessageId,
132
176
  effectiveAdmissionPolicy,
177
+ isCallbackInteraction,
133
178
  } = params;
134
179
 
135
180
  let isValidatedBootstrap = false;
136
181
 
137
- // Trust signals from Slack users.info, forwarded via sourceMetadata.
182
+ // Identity signals forwarded via sourceMetadata: bot flag (Slack users.info
183
+ // / Telegram is_bot) plus Slack workspace trust signals.
184
+ const isBot = sourceMetadata?.isBot ?? undefined;
138
185
  const isStranger = sourceMetadata?.isStranger ?? undefined;
139
186
  const isRestricted = sourceMetadata?.isRestricted ?? undefined;
140
187
 
@@ -143,21 +190,14 @@ export async function enforceIngressAcl(
143
190
 
144
191
  // Absent verdict = gateway could not vouch for this actor → fail-closed deny.
145
192
  // A PRESENT verdict with no member (stranger) still flows through the
146
- // intercepts below; only a missing verdict short-circuits here.
193
+ // stranger lane below; only a missing verdict short-circuits here.
147
194
  const verdict = sourceMetadata?.trustVerdict;
148
195
  if (verdict == null) {
149
196
  log.info(
150
197
  { sourceChannel, externalUserId: canonicalSenderId },
151
198
  "Ingress ACL: absent trust verdict, denying fail-closed",
152
199
  );
153
- return {
154
- resolvedMember: null,
155
- earlyResponse: {
156
- accepted: true,
157
- denied: true,
158
- reason: "not_a_member",
159
- },
160
- };
200
+ return failClosedDeny();
161
201
  }
162
202
 
163
203
  // Gateway attempted resolution but failed (DB error) → fail-closed deny,
@@ -168,18 +208,11 @@ export async function enforceIngressAcl(
168
208
  { sourceChannel, externalUserId: canonicalSenderId },
169
209
  "Ingress ACL: gateway trust resolution failed, denying fail-closed",
170
210
  );
171
- return {
172
- resolvedMember: null,
173
- earlyResponse: {
174
- accepted: true,
175
- denied: true,
176
- reason: "not_a_member",
177
- },
178
- };
211
+ return failClosedDeny();
179
212
  }
180
213
 
181
214
  // Member resolved from the gateway verdict (ACL + identity only); null for a
182
- // stranger verdict, which falls through to the non-member intercepts.
215
+ // stranger verdict, which falls through to the non-member deny lane.
183
216
  const resolvedMember: VerdictMember | null =
184
217
  verdictMemberFromVerdict(verdict);
185
218
 
@@ -190,14 +223,92 @@ export async function enforceIngressAcl(
190
223
  { sourceChannel, externalUserId: canonicalSenderId },
191
224
  "Ingress ACL: member verdict with unresolvable ACL, denying fail-closed",
192
225
  );
193
- return {
194
- resolvedMember: null,
195
- earlyResponse: {
196
- accepted: true,
197
- denied: true,
198
- reason: "not_a_member",
226
+ return failClosedDeny();
227
+ }
228
+
229
+ // An unrecognized trust class is an unresolvable verdict (version skew,
230
+ // malformed payload), not a stranger. Fail safe: soft-deny with no
231
+ // stranger-lane side effects (no access-request card, no verification
232
+ // challenge, no canned reply) — never fail-stranger.
233
+ if (!isTrustClass(verdict.trustClass)) {
234
+ log.warn(
235
+ {
236
+ sourceChannel,
237
+ externalUserId: canonicalSenderId,
238
+ trustClass: verdict.trustClass,
199
239
  },
200
- };
240
+ "Ingress ACL: unrecognized trust class on verdict, denying fail-safe",
241
+ );
242
+ return failClosedDeny();
243
+ }
244
+
245
+ // ── Guardian short-circuit ──
246
+ // A verdict classified `guardian` is admitted even when its same-channel
247
+ // member row is inactive (e.g. pending): the gateway classifies guardians
248
+ // by principal, so a guardian speaking on a channel where they hold no
249
+ // active guardian binding must not fall through the member-vs-stranger
250
+ // gates below — those would misroute the guardian into the stranger lane
251
+ // and fire an access request at the guardian themselves.
252
+ if (verdict.trustClass === "guardian") {
253
+ // The gateway proves guardian identity via a same-channel member row
254
+ // (the active binding address, or a row belonging to the guardian
255
+ // contact), so every guardian verdict carries a resolvable member row.
256
+ // A guardian verdict WITHOUT one is contradictory — cross-channel
257
+ // address collisions are not identity proofs and must never confer
258
+ // guardian capabilities. Fail safe: soft-deny with no stranger-lane
259
+ // side effects.
260
+ if (!resolvedMember) {
261
+ log.warn(
262
+ { sourceChannel, externalUserId: canonicalSenderId },
263
+ "Ingress ACL: guardian verdict without a member row, denying fail-safe",
264
+ );
265
+ return failClosedDeny();
266
+ }
267
+
268
+ // The gateway never classifies a blocked/revoked same-channel row as
269
+ // guardian (explicit per-channel governance wins over the principal
270
+ // check), so a verdict claiming both is contradictory. Fail safe:
271
+ // soft-deny with no stranger-lane side effects.
272
+ if (
273
+ resolvedMember.status === "blocked" ||
274
+ resolvedMember.status === "revoked"
275
+ ) {
276
+ log.warn(
277
+ {
278
+ sourceChannel,
279
+ externalUserId: canonicalSenderId,
280
+ status: resolvedMember.status,
281
+ },
282
+ "Ingress ACL: contradictory guardian verdict with blocked/revoked member row, denying fail-safe",
283
+ );
284
+ return failClosedDeny();
285
+ }
286
+
287
+ // An explicit per-channel `policy: "deny"` on the guardian's own row is
288
+ // honored like blocked/revoked: explicit governance wins over
289
+ // classification. Deny with the accurate policy_deny reason but none of
290
+ // the stranger-lane side effects — the canned "ask the guardian" reply
291
+ // would be addressed at the guardian themselves.
292
+ if (resolvedMember.policy === "deny") {
293
+ log.info(
294
+ { sourceChannel, externalUserId: canonicalSenderId },
295
+ "Ingress ACL: guardian member row carries policy deny, denying",
296
+ );
297
+ return {
298
+ resolvedMember,
299
+ earlyResponse: {
300
+ accepted: true,
301
+ denied: true,
302
+ reason: "policy_deny",
303
+ },
304
+ };
305
+ }
306
+
307
+ log.info(
308
+ { sourceChannel, externalUserId: canonicalSenderId },
309
+ "Ingress ACL: guardian admitted via trust verdict",
310
+ );
311
+ return { resolvedMember };
201
312
  }
202
313
 
203
314
  // /start gv_<token> bootstrap commands must also bypass ACL — the user
@@ -207,12 +318,6 @@ export async function enforceIngressAcl(
207
318
  commandIntentForAcl?.type === "start" &&
208
319
  typeof commandIntentForAcl.payload === "string" &&
209
320
  commandIntentForAcl.payload.startsWith("gv_");
210
- const inviteAdapter = getInviteAdapterRegistry().get(sourceChannel);
211
- const inviteToken = inviteAdapter?.extractInboundToken?.({
212
- commandIntent: commandIntentForAcl,
213
- content: trimmedContent,
214
- sourceMetadata,
215
- });
216
321
 
217
322
  if (canonicalSenderId || hasSenderIdentityClaim) {
218
323
  if (!resolvedMember) {
@@ -243,58 +348,6 @@ export async function enforceIngressAcl(
243
348
  }
244
349
  }
245
350
 
246
- // ── Invite token intercept (non-member) ──
247
- // /start invite deep links grant access without guardian approval.
248
- // Runs BEFORE the policy-aware bypass so a valid /start iv_<token>
249
- // always redeems and creates a member record — even when the
250
- // admission policy is `strangers` (which would otherwise admit the
251
- // sender as a non-member before the token is consumed).
252
- if (inviteToken && denyNonMember) {
253
- const inviteResult = await handleInviteTokenIntercept({
254
- rawToken: inviteToken,
255
- sourceChannel,
256
- externalChatId: conversationExternalId,
257
- externalMessageId,
258
- senderExternalUserId: canonicalSenderId ?? rawSenderId,
259
- senderName: actorDisplayName,
260
- senderUsername: actorUsername,
261
- replyCallbackUrl,
262
- assistantId,
263
- canonicalAssistantId,
264
- });
265
- if (inviteResult)
266
- return {
267
- resolvedMember: null,
268
- earlyResponse: inviteResult,
269
- };
270
- }
271
-
272
- // ── 6-digit invite code intercept (non-member) ──
273
- // On channels with codeRedemptionEnabled, a bare 6-digit message may be
274
- // an invite code. Attempt redemption; on failure (no matching code) fall
275
- // through to normal processing — the number may be a regular message.
276
- // Runs before the policy-aware bypass for the same reason as the token
277
- // intercept above.
278
- if (denyNonMember && /^\d{6}$/.test(trimmedContent)) {
279
- const codeInterceptResult = await handleInviteCodeIntercept({
280
- code: trimmedContent,
281
- sourceChannel,
282
- externalChatId: conversationExternalId,
283
- externalMessageId,
284
- senderExternalUserId: canonicalSenderId ?? rawSenderId,
285
- senderName: actorDisplayName,
286
- senderUsername: actorUsername,
287
- replyCallbackUrl,
288
- assistantId,
289
- canonicalAssistantId,
290
- });
291
- if (codeInterceptResult)
292
- return {
293
- resolvedMember: null,
294
- earlyResponse: codeInterceptResult,
295
- };
296
- }
297
-
298
351
  // ── Policy-aware non-member bypass ──
299
352
  // Skip the ACL deny gate so the admission floor stage emits the final
300
353
  // verdict instead of the ACL prematurely firing guardian notifications,
@@ -308,7 +361,6 @@ export async function enforceIngressAcl(
308
361
  // stranger there still can't reach the floor, but suppressing its
309
362
  // self-verify challenge is a default-onboarding behavior change left
310
363
  // for a separate §8.2 decision.
311
- // Runs AFTER invite intercepts so valid tokens redeem first.
312
364
  if (
313
365
  denyNonMember &&
314
366
  (effectiveAdmissionPolicy === "strangers" ||
@@ -323,10 +375,32 @@ export async function enforceIngressAcl(
323
375
  "Ingress ACL: no member record, denying",
324
376
  );
325
377
 
378
+ // Terminal deny: if the guardian already rejected this sender, skip all
379
+ // re-engagement (self-verify challenge + guardian notify) and deliver
380
+ // only the canned reply. Otherwise a denied sender whose first
381
+ // verification session expired would be handed a fresh, unusable
382
+ // challenge the guardian was never told about.
383
+ const nonMemberSenderId = canonicalSenderId ?? rawSenderId;
384
+ const terminallyDenied =
385
+ !!nonMemberSenderId &&
386
+ isAccessRequestDenied({
387
+ canonicalAssistantId,
388
+ sourceChannel,
389
+ actorExternalId: nonMemberSenderId,
390
+ });
391
+
326
392
  // Slack-specific: send a verification challenge directly to the
327
393
  // user's DM instead of requiring guardian-mediated approval. The
328
- // user can reply with the code in the DM to self-verify.
329
- if (sourceChannel === "slack" && (canonicalSenderId ?? rawSenderId)) {
394
+ // user can reply with the code in the DM to self-verify. Bots are
395
+ // excluded a bot cannot return a code, so it goes straight to the
396
+ // guardian-notify lane and its introduction card offers direct trust.
397
+ if (
398
+ sourceChannel === "slack" &&
399
+ isBot !== true &&
400
+ (canonicalSenderId ?? rawSenderId) &&
401
+ !terminallyDenied &&
402
+ !isCallbackInteraction
403
+ ) {
330
404
  const slackVerifyResult = initiateSlackVerificationChallenge({
331
405
  sourceChannel,
332
406
  senderUserId: (canonicalSenderId ?? rawSenderId)!,
@@ -346,6 +420,7 @@ export async function enforceIngressAcl(
346
420
  trimmedContent,
347
421
  MESSAGE_PREVIEW_MAX_LENGTH,
348
422
  ),
423
+ isBot,
349
424
  isStranger,
350
425
  isRestricted,
351
426
  messageTs,
@@ -402,7 +477,12 @@ export async function enforceIngressAcl(
402
477
  // pipeline. Unlike Slack, we cannot DM the requester directly — the
403
478
  // verification code is delivered to the guardian, who decides whether
404
479
  // to share it with the email sender out-of-band.
405
- if (sourceChannel === "email" && (canonicalSenderId ?? rawSenderId)) {
480
+ if (
481
+ sourceChannel === "email" &&
482
+ (canonicalSenderId ?? rawSenderId) &&
483
+ !terminallyDenied &&
484
+ !isCallbackInteraction
485
+ ) {
406
486
  const emailVerifyResult = initiateEmailVerificationChallenge({
407
487
  sourceChannel,
408
488
  senderUserId: (canonicalSenderId ?? rawSenderId)!,
@@ -421,6 +501,7 @@ export async function enforceIngressAcl(
421
501
  trimmedContent,
422
502
  MESSAGE_PREVIEW_MAX_LENGTH,
423
503
  ),
504
+ isBot,
424
505
  isStranger,
425
506
  isRestricted,
426
507
  messageTs,
@@ -447,34 +528,52 @@ export async function enforceIngressAcl(
447
528
  // Notify the guardian about the access request so they can approve/deny.
448
529
  // Uses the shared helper which handles guardian binding lookup,
449
530
  // deduplication, canonical request creation, and notification emission.
531
+ // Skipped for callback interactions — a button press must not create
532
+ // an access request — but the handshake window is still probed so the
533
+ // reply doesn't tell a just-approved sender they lack access.
450
534
  let guardianNotified = false;
451
- try {
452
- const accessResult = await notifyGuardianOfAccessRequest({
535
+ let handshakeInProgress = false;
536
+ if (isCallbackInteraction) {
537
+ handshakeInProgress = isApprovalHandshakeInProgress({
453
538
  canonicalAssistantId,
454
539
  sourceChannel,
455
- conversationExternalId,
456
- actorExternalId: canonicalSenderId ?? rawSenderId,
457
- actorDisplayName,
458
- actorUsername,
459
- messagePreview: truncate(
460
- trimmedContent,
461
- MESSAGE_PREVIEW_MAX_LENGTH,
462
- ),
463
- isStranger,
464
- isRestricted,
465
- messageTs,
540
+ actorExternalId: (canonicalSenderId ?? rawSenderId)!,
466
541
  });
467
- guardianNotified = accessResult.notified;
468
- } catch (err) {
469
- log.error(
470
- { err, sourceChannel, conversationExternalId },
471
- "Failed to notify guardian of access request",
472
- );
542
+ } else {
543
+ try {
544
+ const accessResult = await notifyGuardianOfAccessRequest({
545
+ canonicalAssistantId,
546
+ sourceChannel,
547
+ conversationExternalId,
548
+ actorExternalId: canonicalSenderId ?? rawSenderId,
549
+ actorDisplayName,
550
+ actorUsername,
551
+ messagePreview: truncate(
552
+ trimmedContent,
553
+ MESSAGE_PREVIEW_MAX_LENGTH,
554
+ ),
555
+ isBot,
556
+ isStranger,
557
+ isRestricted,
558
+ messageTs,
559
+ });
560
+ guardianNotified = accessResult.notified;
561
+ handshakeInProgress =
562
+ !accessResult.notified &&
563
+ accessResult.reason === "approval_pending_verification";
564
+ } catch (err) {
565
+ log.error(
566
+ { err, sourceChannel, conversationExternalId },
567
+ "Failed to notify guardian of access request",
568
+ );
569
+ }
473
570
  }
474
571
 
475
- const replyText = guardianNotified
476
- ? `Hmm looks like you don't have access to talk to me. I'll let ${await resolveGuardianLabel(sourceChannel)} know you tried talking to me and get back to you.`
477
- : "Sorry, you haven't been approved to message this assistant.";
572
+ const replyText = await composeAccessDenialReply({
573
+ sourceChannel,
574
+ guardianNotified,
575
+ handshakeInProgress,
576
+ });
478
577
  let replyDelivered = false;
479
578
  if (replyCallbackUrl) {
480
579
  const replyPayload: Parameters<typeof deliverChannelReply>[1] = {
@@ -543,63 +642,6 @@ export async function enforceIngressAcl(
543
642
  }
544
643
  }
545
644
 
546
- // ── Invite token intercept (inactive member) ──
547
- // Invite tokens can reactivate revoked/pending members without
548
- // requiring guardian approval, but blocked members are excluded so
549
- // they are short-circuited at the ACL layer rather than entering the
550
- // redemption path. Runs BEFORE the policy-aware bypass so a valid
551
- // invite always redeems and reactivates the member record, rather
552
- // than the bypass admitting the sender in their inactive state.
553
- if (!isBlockedMember && inviteToken && denyInactiveMember) {
554
- const inviteResult = await handleInviteTokenIntercept({
555
- rawToken: inviteToken,
556
- sourceChannel,
557
- externalChatId: conversationExternalId,
558
- externalMessageId,
559
- senderExternalUserId: canonicalSenderId ?? rawSenderId,
560
- senderName: actorDisplayName,
561
- senderUsername: actorUsername,
562
- replyCallbackUrl,
563
- assistantId,
564
- canonicalAssistantId,
565
- });
566
- if (inviteResult)
567
- return {
568
- resolvedMember: null,
569
- earlyResponse: inviteResult,
570
- };
571
- }
572
-
573
- // ── 6-digit invite code intercept (inactive member) ──
574
- // Codes can reactivate revoked/pending members; non-matching codes
575
- // fall through. Blocked members are excluded here for consistency —
576
- // the redemption service would reject them anyway, but early exit
577
- // avoids unnecessary work. Runs before the policy-aware bypass for
578
- // the same reason as the token intercept above.
579
- if (
580
- !isBlockedMember &&
581
- denyInactiveMember &&
582
- /^\d{6}$/.test(trimmedContent)
583
- ) {
584
- const codeInterceptResult = await handleInviteCodeIntercept({
585
- code: trimmedContent,
586
- sourceChannel,
587
- externalChatId: conversationExternalId,
588
- externalMessageId,
589
- senderExternalUserId: canonicalSenderId ?? rawSenderId,
590
- senderName: actorDisplayName,
591
- senderUsername: actorUsername,
592
- replyCallbackUrl,
593
- assistantId,
594
- canonicalAssistantId,
595
- });
596
- if (codeInterceptResult)
597
- return {
598
- resolvedMember: null,
599
- earlyResponse: codeInterceptResult,
600
- };
601
- }
602
-
603
645
  // ── Policy-aware inactive-member bypass ──
604
646
  // `strangers` (floor 1): admit non-blocked, non-revoked senders
605
647
  // (pending/unverified bypass the inactive-member deny gate).
@@ -616,7 +658,12 @@ export async function enforceIngressAcl(
616
658
  // there, verifying reaches `trusted_contact` (rank 3 ≥ floor 3), so
617
659
  // the challenge legitimately upgrades the sender into access.
618
660
  // In every case skip the deny gate so the admission stage decides.
619
- // Runs AFTER invite intercepts so valid tokens redeem first.
661
+ //
662
+ // A guardian-denied sender is persisted as an unverified contact and is
663
+ // admitted here on the same rank-vs-floor terms as any other unverified
664
+ // contact (admitted under `any_contact`/`strangers`, denied under
665
+ // stricter floors). The deny suppresses re-prompting, not admission;
666
+ // holding a denied contact out of every floor is the block action's job.
620
667
  if (!isBlockedMember && denyInactiveMember) {
621
668
  if (
622
669
  (effectiveAdmissionPolicy === "strangers" &&
@@ -640,13 +687,31 @@ export async function enforceIngressAcl(
640
687
  "Ingress ACL: member not active, denying",
641
688
  );
642
689
 
690
+ // Terminal deny: a sender the guardian already rejected must not be
691
+ // handed a fresh self-verify challenge on re-contact (the guardian is
692
+ // no longer notified, so the code would go nowhere). Skip the
693
+ // challenge and fall through to the canned reply.
694
+ const inactiveSenderId = canonicalSenderId ?? rawSenderId;
695
+ const terminallyDenied =
696
+ !isBlockedMember &&
697
+ !!inactiveSenderId &&
698
+ isAccessRequestDenied({
699
+ canonicalAssistantId,
700
+ sourceChannel,
701
+ actorExternalId: inactiveSenderId,
702
+ });
703
+
643
704
  // Slack-specific: re-verify inactive members via DM challenge
644
705
  // (same as non-member path). Blocked members are excluded —
645
- // the guardian made an explicit decision to block them.
706
+ // the guardian made an explicit decision to block them. Bots are
707
+ // excluded — a bot cannot return a code.
646
708
  if (
647
709
  sourceChannel === "slack" &&
710
+ isBot !== true &&
648
711
  resolvedMember.status !== "blocked" &&
649
- (canonicalSenderId ?? rawSenderId)
712
+ (canonicalSenderId ?? rawSenderId) &&
713
+ !terminallyDenied &&
714
+ !isCallbackInteraction
650
715
  ) {
651
716
  const slackVerifyResult = initiateSlackVerificationChallenge({
652
717
  sourceChannel,
@@ -669,6 +734,7 @@ export async function enforceIngressAcl(
669
734
  trimmedContent,
670
735
  MESSAGE_PREVIEW_MAX_LENGTH,
671
736
  ),
737
+ isBot,
672
738
  isStranger,
673
739
  isRestricted,
674
740
  messageTs,
@@ -720,39 +786,56 @@ export async function enforceIngressAcl(
720
786
  // For revoked/pending members, notify the guardian so they can
721
787
  // re-approve. Blocked members are intentionally excluded — the
722
788
  // guardian already made an explicit decision to block them.
789
+ // Callback interactions never create an access request; the
790
+ // handshake window is still probed for reply copy.
723
791
  let guardianNotified = false;
792
+ let handshakeInProgress = false;
724
793
  if (resolvedMember.status !== "blocked") {
725
- try {
726
- const accessResult = await notifyGuardianOfAccessRequest({
794
+ if (isCallbackInteraction) {
795
+ handshakeInProgress = isApprovalHandshakeInProgress({
727
796
  canonicalAssistantId,
728
797
  sourceChannel,
729
- conversationExternalId,
730
- actorExternalId: canonicalSenderId ?? rawSenderId,
731
- actorDisplayName,
732
- actorUsername,
733
- previousMemberStatus: channelStatusToMemberStatus(
734
- resolvedMember.status,
735
- ),
736
- messagePreview: truncate(
737
- trimmedContent,
738
- MESSAGE_PREVIEW_MAX_LENGTH,
739
- ),
740
- isStranger,
741
- isRestricted,
742
- messageTs,
798
+ actorExternalId: (canonicalSenderId ?? rawSenderId)!,
743
799
  });
744
- guardianNotified = accessResult.notified;
745
- } catch (err) {
746
- log.error(
747
- { err, sourceChannel, conversationExternalId },
748
- "Failed to notify guardian of access request",
749
- );
800
+ } else {
801
+ try {
802
+ const accessResult = await notifyGuardianOfAccessRequest({
803
+ canonicalAssistantId,
804
+ sourceChannel,
805
+ conversationExternalId,
806
+ actorExternalId: canonicalSenderId ?? rawSenderId,
807
+ actorDisplayName,
808
+ actorUsername,
809
+ previousMemberStatus: channelStatusToMemberStatus(
810
+ resolvedMember.status,
811
+ ),
812
+ messagePreview: truncate(
813
+ trimmedContent,
814
+ MESSAGE_PREVIEW_MAX_LENGTH,
815
+ ),
816
+ isBot,
817
+ isStranger,
818
+ isRestricted,
819
+ messageTs,
820
+ });
821
+ guardianNotified = accessResult.notified;
822
+ handshakeInProgress =
823
+ !accessResult.notified &&
824
+ accessResult.reason === "approval_pending_verification";
825
+ } catch (err) {
826
+ log.error(
827
+ { err, sourceChannel, conversationExternalId },
828
+ "Failed to notify guardian of access request",
829
+ );
830
+ }
750
831
  }
751
832
  }
752
833
 
753
- const inactiveReplyText = guardianNotified
754
- ? `Hmm looks like you don't have access to talk to me. I'll let ${await resolveGuardianLabel(sourceChannel)} know you tried talking to me and get back to you.`
755
- : "Sorry, you haven't been approved to message this assistant.";
834
+ const inactiveReplyText = await composeAccessDenialReply({
835
+ sourceChannel,
836
+ guardianNotified,
837
+ handshakeInProgress,
838
+ });
756
839
  let inactiveReplyDelivered = false;
757
840
  if (replyCallbackUrl) {
758
841
  const inactiveReplyPayload: Parameters<
@@ -839,359 +922,6 @@ export async function enforceIngressAcl(
839
922
  };
840
923
  }
841
924
 
842
- // ---------------------------------------------------------------------------
843
- // Invite token intercept
844
- // ---------------------------------------------------------------------------
845
-
846
- /**
847
- * Handle an inbound invite token for a non-member or inactive member.
848
- *
849
- * Redeems the invite, delivers a deterministic reply, and returns a Response
850
- * to short-circuit the handler. Returns `null` when the intercept should not
851
- * fire (e.g. already_member outcome — let normal flow handle it).
852
- */
853
- async function handleInviteTokenIntercept(params: {
854
- rawToken: string;
855
- sourceChannel: ChannelId;
856
- externalChatId: string;
857
- externalMessageId: string;
858
- senderExternalUserId?: string;
859
- senderName?: string;
860
- senderUsername?: string;
861
- replyCallbackUrl?: string;
862
- assistantId?: string;
863
- canonicalAssistantId: string;
864
- }): Promise<Record<string, unknown> | null> {
865
- const {
866
- rawToken,
867
- sourceChannel,
868
- externalChatId,
869
- externalMessageId,
870
- senderExternalUserId,
871
- senderName,
872
- senderUsername,
873
- replyCallbackUrl,
874
- assistantId,
875
- canonicalAssistantId,
876
- } = params;
877
-
878
- // Record the inbound event for dedup tracking BEFORE performing redemption.
879
- // Without this, duplicate webhook deliveries (common with Telegram) would
880
- // not be tracked: the first delivery redeems the invite and returns early,
881
- // then the retry finds an active member, passes ACL, and the raw
882
- // /start iv_<token> message leaks into the agent pipeline.
883
- const dedupResult = recordInbound(
884
- sourceChannel,
885
- externalChatId,
886
- externalMessageId,
887
- { assistantId: canonicalAssistantId },
888
- );
889
-
890
- if (dedupResult.duplicate) {
891
- return {
892
- accepted: true,
893
- duplicate: true,
894
- eventId: dedupResult.eventId,
895
- };
896
- }
897
-
898
- const outcome = await redeemInvite({
899
- rawToken,
900
- sourceChannel,
901
- externalUserId: senderExternalUserId,
902
- externalChatId,
903
- displayName: senderName,
904
- username: senderUsername,
905
- assistantId: canonicalAssistantId,
906
- });
907
-
908
- log.info(
909
- {
910
- sourceChannel,
911
- externalChatId: params.externalChatId,
912
- ok: outcome.ok,
913
- type: outcome.ok ? outcome.type : undefined,
914
- reason: !outcome.ok ? outcome.reason : undefined,
915
- },
916
- "Invite token intercept: redemption result",
917
- );
918
-
919
- // already_member means the user has an active record — let the normal
920
- // flow handle them (they passed ACL or the member is active).
921
- if (outcome.ok && outcome.type === "already_member") {
922
- // Deliver a quick acknowledgement and short-circuit so the user
923
- // does not trigger the deny gate or a duplicate agent loop.
924
- const replyText = getInviteRedemptionReply(outcome);
925
- if (replyCallbackUrl) {
926
- try {
927
- await deliverChannelReply(replyCallbackUrl, {
928
- chatId: externalChatId,
929
- text: replyText,
930
- assistantId,
931
- });
932
- } catch (err) {
933
- log.error(
934
- { err, externalChatId },
935
- "Failed to deliver invite already-member reply",
936
- );
937
- }
938
- }
939
- markProcessed(dedupResult.eventId);
940
- return {
941
- accepted: true,
942
- eventId: dedupResult.eventId,
943
- inviteRedemption: "already_member",
944
- };
945
- }
946
-
947
- const replyText = getInviteRedemptionReply(outcome);
948
-
949
- if (replyCallbackUrl) {
950
- try {
951
- await deliverChannelReply(replyCallbackUrl, {
952
- chatId: externalChatId,
953
- text: replyText,
954
- assistantId,
955
- });
956
- } catch (err) {
957
- log.error(
958
- { err, externalChatId },
959
- "Failed to deliver invite redemption reply",
960
- );
961
- }
962
- }
963
-
964
- if (outcome.ok && outcome.type === "redeemed") {
965
- markProcessed(dedupResult.eventId);
966
- return {
967
- accepted: true,
968
- eventId: dedupResult.eventId,
969
- inviteRedemption: "redeemed",
970
- memberId: outcome.memberId,
971
- };
972
- }
973
-
974
- // Failed redemption — inform the user and deny
975
- markProcessed(dedupResult.eventId);
976
- return {
977
- accepted: true,
978
- eventId: dedupResult.eventId,
979
- denied: true,
980
- inviteRedemption: outcome.reason,
981
- };
982
- }
983
-
984
- // ---------------------------------------------------------------------------
985
- // 6-digit invite code intercept
986
- // ---------------------------------------------------------------------------
987
-
988
- /**
989
- * Handle a bare 6-digit message as a potential invite code redemption.
990
- *
991
- * Checks channel policy (codeRedemptionEnabled), attempts redemption via
992
- * `redeemInviteByCode`, and returns a Response to short-circuit the handler
993
- * on success. Returns `null` when the code does not match any active invite,
994
- * allowing the message to fall through to normal processing.
995
- */
996
- async function handleInviteCodeIntercept(params: {
997
- code: string;
998
- sourceChannel: ChannelId;
999
- externalChatId: string;
1000
- externalMessageId: string;
1001
- senderExternalUserId?: string;
1002
- senderName?: string;
1003
- senderUsername?: string;
1004
- replyCallbackUrl?: string;
1005
- assistantId?: string;
1006
- canonicalAssistantId: string;
1007
- }): Promise<Record<string, unknown> | null> {
1008
- const {
1009
- code,
1010
- sourceChannel,
1011
- externalChatId,
1012
- externalMessageId,
1013
- senderExternalUserId,
1014
- senderName,
1015
- senderUsername,
1016
- replyCallbackUrl,
1017
- assistantId,
1018
- canonicalAssistantId,
1019
- } = params;
1020
-
1021
- // Skip channels that don't support code redemption
1022
- if (!isInviteCodeRedemptionEnabled(sourceChannel)) {
1023
- return null;
1024
- }
1025
-
1026
- // Pre-check: verify a matching invite exists before committing to handle
1027
- // this message. A bare 6-digit number may be a regular message, so we
1028
- // must not record inbound dedup until we know the code maps to an invite.
1029
- const codeHash = hashVoiceCode(code);
1030
- const candidateInvite = findByInviteCodeHash(codeHash, sourceChannel);
1031
- if (!candidateInvite) {
1032
- // The code doesn't match any invite on this channel. Before falling
1033
- // through to normal processing, check if it matches on a different
1034
- // channel — if so, inform the user instead of silently ignoring it.
1035
- const crossChannelInvite = findByInviteCodeHashAnyChannel(codeHash);
1036
- if (crossChannelInvite) {
1037
- // Record inbound for dedup tracking — without this, duplicate webhook
1038
- // deliveries would re-enter ACL and send the mismatch reply again.
1039
- const dedupResult = recordInbound(
1040
- sourceChannel,
1041
- externalChatId,
1042
- externalMessageId,
1043
- { assistantId: canonicalAssistantId },
1044
- );
1045
-
1046
- if (dedupResult.duplicate) {
1047
- return {
1048
- accepted: true,
1049
- duplicate: true,
1050
- eventId: dedupResult.eventId,
1051
- };
1052
- }
1053
-
1054
- const mismatchReply = "This invite is not valid for this channel.";
1055
- if (replyCallbackUrl) {
1056
- try {
1057
- await deliverChannelReply(replyCallbackUrl, {
1058
- chatId: externalChatId,
1059
- text: mismatchReply,
1060
- assistantId,
1061
- });
1062
- } catch (err) {
1063
- log.error(
1064
- { err, externalChatId },
1065
- "Failed to deliver invite code channel-mismatch reply",
1066
- );
1067
- }
1068
- }
1069
- markProcessed(dedupResult.eventId);
1070
- return {
1071
- accepted: true,
1072
- eventId: dedupResult.eventId,
1073
- denied: true,
1074
- inviteRedemption: "channel_mismatch",
1075
- };
1076
- }
1077
- return null;
1078
- }
1079
-
1080
- // Record the inbound event for dedup tracking BEFORE performing redemption,
1081
- // matching the token intercept path. Without this, duplicate webhook
1082
- // deliveries could slip through: the first delivery redeems the invite and
1083
- // activates membership, then a retry finds an active member, passes ACL,
1084
- // and the raw 6-digit message leaks into the agent pipeline.
1085
- const dedupResult = recordInbound(
1086
- sourceChannel,
1087
- externalChatId,
1088
- externalMessageId,
1089
- { assistantId: canonicalAssistantId },
1090
- );
1091
-
1092
- if (dedupResult.duplicate) {
1093
- return {
1094
- accepted: true,
1095
- duplicate: true,
1096
- eventId: dedupResult.eventId,
1097
- };
1098
- }
1099
-
1100
- let outcome: Awaited<ReturnType<typeof redeemInviteByCode>>;
1101
- try {
1102
- outcome = await redeemInviteByCode({
1103
- code,
1104
- sourceChannel,
1105
- externalUserId: senderExternalUserId,
1106
- externalChatId,
1107
- displayName: senderName,
1108
- username: senderUsername,
1109
- assistantId: canonicalAssistantId,
1110
- });
1111
- } catch (err) {
1112
- // Redemption threw — roll back the dedup record so webhook retries
1113
- // can re-attempt instead of short-circuiting as duplicates.
1114
- log.error(
1115
- { err, sourceChannel, externalChatId },
1116
- "Invite code intercept: redemption threw, rolling back dedup record",
1117
- );
1118
- deleteInbound(dedupResult.eventId);
1119
- throw err;
1120
- }
1121
-
1122
- log.info(
1123
- {
1124
- sourceChannel,
1125
- externalChatId,
1126
- ok: outcome.ok,
1127
- type: outcome.ok ? outcome.type : undefined,
1128
- reason: !outcome.ok ? outcome.reason : undefined,
1129
- },
1130
- "Invite code intercept: redemption result",
1131
- );
1132
-
1133
- // already_member: deliver acknowledgement and short-circuit
1134
- if (outcome.ok && outcome.type === "already_member") {
1135
- const replyText = getInviteRedemptionReply(outcome);
1136
- if (replyCallbackUrl) {
1137
- try {
1138
- await deliverChannelReply(replyCallbackUrl, {
1139
- chatId: externalChatId,
1140
- text: replyText,
1141
- assistantId,
1142
- });
1143
- } catch (err) {
1144
- log.error(
1145
- { err, externalChatId },
1146
- "Failed to deliver invite code already-member reply",
1147
- );
1148
- }
1149
- }
1150
- markProcessed(dedupResult.eventId);
1151
- return {
1152
- accepted: true,
1153
- eventId: dedupResult.eventId,
1154
- inviteRedemption: "already_member",
1155
- };
1156
- }
1157
-
1158
- const replyText = getInviteRedemptionReply(outcome);
1159
-
1160
- if (replyCallbackUrl) {
1161
- try {
1162
- await deliverChannelReply(replyCallbackUrl, {
1163
- chatId: externalChatId,
1164
- text: replyText,
1165
- assistantId,
1166
- });
1167
- } catch (err) {
1168
- log.error(
1169
- { err, externalChatId },
1170
- "Failed to deliver invite code redemption reply",
1171
- );
1172
- }
1173
- }
1174
-
1175
- if (outcome.ok && outcome.type === "redeemed") {
1176
- markProcessed(dedupResult.eventId);
1177
- return {
1178
- accepted: true,
1179
- eventId: dedupResult.eventId,
1180
- inviteRedemption: "redeemed",
1181
- memberId: outcome.memberId,
1182
- };
1183
- }
1184
-
1185
- // Failed redemption (expired, revoked, etc.) — inform and deny
1186
- markProcessed(dedupResult.eventId);
1187
- return {
1188
- accepted: true,
1189
- eventId: dedupResult.eventId,
1190
- denied: true,
1191
- inviteRedemption: !outcome.ok ? outcome.reason : undefined,
1192
- };
1193
- }
1194
-
1195
925
  // ---------------------------------------------------------------------------
1196
926
  // Channel verification challenges
1197
927
  // ---------------------------------------------------------------------------