@vellumai/assistant 0.4.49 → 0.4.51

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 (353) hide show
  1. package/ARCHITECTURE.md +24 -33
  2. package/README.md +3 -3
  3. package/docs/architecture/integrations.md +2 -2
  4. package/docs/architecture/keychain-broker.md +6 -6
  5. package/docs/architecture/memory.md +180 -119
  6. package/knip.json +32 -0
  7. package/package.json +3 -2
  8. package/src/__tests__/agent-loop.test.ts +3 -1
  9. package/src/__tests__/anthropic-provider.test.ts +114 -23
  10. package/src/__tests__/approval-cascade.test.ts +1 -15
  11. package/src/__tests__/approval-routes-http.test.ts +2 -0
  12. package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
  13. package/src/__tests__/btw-routes.test.ts +61 -5
  14. package/src/__tests__/canonical-guardian-store.test.ts +95 -0
  15. package/src/__tests__/checker.test.ts +13 -0
  16. package/src/__tests__/config-schema.test.ts +1 -68
  17. package/src/__tests__/config-watcher.test.ts +8 -0
  18. package/src/__tests__/context-memory-e2e.test.ts +11 -100
  19. package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
  20. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  21. package/src/__tests__/credential-security-e2e.test.ts +1 -0
  22. package/src/__tests__/credential-security-invariants.test.ts +8 -7
  23. package/src/__tests__/credential-vault-unit.test.ts +23 -18
  24. package/src/__tests__/credential-vault.test.ts +30 -18
  25. package/src/__tests__/credentials-cli.test.ts +257 -82
  26. package/src/__tests__/cu-unified-flow.test.ts +532 -0
  27. package/src/__tests__/date-context.test.ts +93 -77
  28. package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
  29. package/src/__tests__/guardian-routing-invariants.test.ts +93 -0
  30. package/src/__tests__/history-repair.test.ts +245 -0
  31. package/src/__tests__/host-cu-proxy.test.ts +165 -3
  32. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  33. package/src/__tests__/inbound-invite-redemption.test.ts +36 -7
  34. package/src/__tests__/integration-status.test.ts +31 -30
  35. package/src/__tests__/invite-redemption-service.test.ts +166 -13
  36. package/src/__tests__/invite-routes-http.test.ts +166 -5
  37. package/src/__tests__/keychain-broker-client.test.ts +4 -4
  38. package/src/__tests__/list-messages-attachments.test.ts +193 -0
  39. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
  40. package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
  41. package/src/__tests__/memory-recall-quality.test.ts +244 -407
  42. package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
  43. package/src/__tests__/memory-regressions.test.ts +477 -2841
  44. package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
  45. package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
  46. package/src/__tests__/mime-builder.test.ts +28 -0
  47. package/src/__tests__/native-web-search.test.ts +1 -0
  48. package/src/__tests__/oauth-cli.test.ts +824 -31
  49. package/src/__tests__/oauth-provider-profiles.test.ts +1 -1
  50. package/src/__tests__/oauth-store.test.ts +363 -17
  51. package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
  52. package/src/__tests__/registry.test.ts +0 -1
  53. package/src/__tests__/relay-server.test.ts +55 -1
  54. package/src/__tests__/schedule-tools.test.ts +32 -0
  55. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  56. package/src/__tests__/secret-onetime-send.test.ts +1 -0
  57. package/src/__tests__/secret-routes-managed-proxy.test.ts +183 -0
  58. package/src/__tests__/secure-keys.test.ts +78 -18
  59. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  60. package/src/__tests__/server-history-render.test.ts +2 -2
  61. package/src/__tests__/session-abort-tool-results.test.ts +1 -14
  62. package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
  63. package/src/__tests__/session-agent-loop.test.ts +19 -15
  64. package/src/__tests__/session-confirmation-signals.test.ts +1 -15
  65. package/src/__tests__/session-error.test.ts +124 -2
  66. package/src/__tests__/session-history-web-search.test.ts +918 -0
  67. package/src/__tests__/session-pre-run-repair.test.ts +1 -14
  68. package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
  69. package/src/__tests__/session-queue.test.ts +37 -27
  70. package/src/__tests__/session-runtime-assembly.test.ts +54 -0
  71. package/src/__tests__/session-slash-known.test.ts +1 -15
  72. package/src/__tests__/session-slash-queue.test.ts +1 -15
  73. package/src/__tests__/session-slash-unknown.test.ts +1 -15
  74. package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
  75. package/src/__tests__/session-workspace-injection.test.ts +3 -37
  76. package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
  77. package/src/__tests__/skills-install-extract.test.ts +93 -0
  78. package/src/__tests__/skills.test.ts +2 -2
  79. package/src/__tests__/skillssh-registry.test.ts +451 -0
  80. package/src/__tests__/slack-channel-config.test.ts +10 -8
  81. package/src/__tests__/trust-store.test.ts +15 -0
  82. package/src/__tests__/twilio-config.test.ts +11 -10
  83. package/src/__tests__/twilio-provider.test.ts +9 -4
  84. package/src/__tests__/voice-invite-redemption.test.ts +85 -5
  85. package/src/agent/ax-tree-compaction.test.ts +51 -0
  86. package/src/agent/loop.ts +39 -12
  87. package/src/approvals/AGENTS.md +1 -1
  88. package/src/approvals/guardian-request-resolvers.ts +14 -2
  89. package/src/bundler/compiler-tools.ts +66 -2
  90. package/src/calls/call-domain.ts +134 -3
  91. package/src/calls/call-store.ts +6 -0
  92. package/src/calls/relay-server.ts +44 -6
  93. package/src/calls/relay-setup-router.ts +17 -1
  94. package/src/calls/twilio-config.ts +5 -4
  95. package/src/calls/twilio-provider.ts +14 -9
  96. package/src/calls/twilio-rest.ts +10 -7
  97. package/src/calls/types.ts +3 -1
  98. package/src/cli/commands/config.ts +14 -9
  99. package/src/cli/commands/contacts.ts +3 -0
  100. package/src/cli/commands/credentials.ts +170 -174
  101. package/src/cli/commands/doctor.ts +11 -8
  102. package/src/cli/commands/keys.ts +9 -9
  103. package/src/cli/commands/mcp.ts +46 -59
  104. package/src/cli/commands/memory.ts +16 -165
  105. package/src/cli/commands/oauth/apps.ts +68 -10
  106. package/src/cli/commands/oauth/connections.ts +475 -105
  107. package/src/cli/commands/oauth/index.ts +3 -3
  108. package/src/cli/commands/oauth/providers.ts +18 -4
  109. package/src/cli/commands/sessions.ts +5 -2
  110. package/src/cli/commands/skills.ts +173 -1
  111. package/src/cli/http-client.ts +0 -20
  112. package/src/cli/main-screen.tsx +2 -2
  113. package/src/cli/program.ts +5 -6
  114. package/src/cli.ts +20 -22
  115. package/src/config/__tests__/feature-flag-registry-bundled.test.ts +39 -0
  116. package/src/config/bundled-skills/computer-use/TOOLS.json +1 -1
  117. package/src/config/bundled-skills/computer-use/tools/computer-use-observe.ts +12 -0
  118. package/src/config/bundled-skills/contacts/SKILL.md +35 -11
  119. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
  120. package/src/config/bundled-skills/gmail/SKILL.md +1 -1
  121. package/src/config/bundled-skills/gmail/TOOLS.json +52 -0
  122. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +13 -3
  123. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +9 -2
  124. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +5 -1
  125. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +5 -1
  126. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +5 -1
  127. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +5 -1
  128. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +9 -2
  129. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +5 -1
  130. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +5 -1
  131. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +5 -1
  132. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +5 -1
  133. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +5 -1
  134. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +5 -1
  135. package/src/config/bundled-skills/google-calendar/TOOLS.json +20 -0
  136. package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +2 -1
  137. package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +2 -1
  138. package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +2 -1
  139. package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +2 -1
  140. package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +2 -1
  141. package/src/config/bundled-skills/google-calendar/tools/shared.ts +8 -2
  142. package/src/config/bundled-skills/messaging/SKILL.md +1 -1
  143. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
  144. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
  145. package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +2 -2
  146. package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +2 -2
  147. package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +2 -2
  148. package/src/config/bundled-skills/messaging/tools/messaging-read.ts +2 -2
  149. package/src/config/bundled-skills/messaging/tools/messaging-search.ts +2 -2
  150. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +2 -2
  151. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +2 -2
  152. package/src/config/bundled-skills/messaging/tools/shared.ts +7 -5
  153. package/src/config/bundled-skills/slack/tools/shared.ts +1 -1
  154. package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +1 -1
  155. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +1 -1
  156. package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +1 -1
  157. package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +1 -1
  158. package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +1 -1
  159. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +1 -1
  160. package/src/config/bundled-tool-registry.ts +2 -5
  161. package/src/config/loader.ts +6 -42
  162. package/src/config/schema.ts +1 -12
  163. package/src/config/schemas/memory-lifecycle.ts +0 -9
  164. package/src/config/schemas/memory-processing.ts +0 -180
  165. package/src/config/schemas/memory-retrieval.ts +32 -104
  166. package/src/config/schemas/memory.ts +0 -10
  167. package/src/config/types.ts +0 -4
  168. package/src/contacts/contact-store.ts +39 -2
  169. package/src/contacts/contacts-write.ts +9 -0
  170. package/src/context/window-manager.ts +4 -1
  171. package/src/daemon/config-watcher.ts +55 -2
  172. package/src/daemon/daemon-control.ts +1 -1
  173. package/src/daemon/date-context.ts +114 -31
  174. package/src/daemon/handlers/config-ingress.ts +2 -2
  175. package/src/daemon/handlers/config-slack-channel.ts +59 -39
  176. package/src/daemon/handlers/config-telegram.ts +23 -14
  177. package/src/daemon/handlers/session-history.ts +1 -358
  178. package/src/daemon/handlers/sessions.ts +18 -13
  179. package/src/daemon/handlers/shared.ts +3 -17
  180. package/src/daemon/handlers/skills.ts +20 -1
  181. package/src/daemon/history-repair.ts +72 -8
  182. package/src/daemon/host-cu-proxy.ts +55 -26
  183. package/src/daemon/lifecycle.ts +39 -4
  184. package/src/daemon/mcp-reload-service.ts +2 -2
  185. package/src/daemon/message-types/computer-use.ts +1 -12
  186. package/src/daemon/message-types/memory.ts +4 -16
  187. package/src/daemon/message-types/messages.ts +1 -0
  188. package/src/daemon/message-types/sessions.ts +4 -42
  189. package/src/daemon/server.ts +6 -1
  190. package/src/daemon/session-agent-loop-handlers.ts +38 -0
  191. package/src/daemon/session-agent-loop.ts +334 -48
  192. package/src/daemon/session-error.ts +89 -6
  193. package/src/daemon/session-history.ts +17 -7
  194. package/src/daemon/session-media-retry.ts +6 -2
  195. package/src/daemon/session-memory.ts +69 -149
  196. package/src/daemon/session-process.ts +10 -1
  197. package/src/daemon/session-runtime-assembly.ts +49 -19
  198. package/src/daemon/session-slash.ts +3 -5
  199. package/src/daemon/session-surfaces.ts +4 -1
  200. package/src/daemon/session-tool-setup.ts +7 -1
  201. package/src/daemon/session.ts +12 -2
  202. package/src/email/providers/index.ts +2 -2
  203. package/src/instrument.ts +61 -1
  204. package/src/media/avatar-router.ts +1 -1
  205. package/src/memory/admin.ts +2 -191
  206. package/src/memory/canonical-guardian-store.ts +38 -2
  207. package/src/memory/conversation-crud.ts +0 -33
  208. package/src/memory/conversation-queries.ts +25 -83
  209. package/src/memory/db-init.ts +32 -0
  210. package/src/memory/embedding-backend.ts +84 -8
  211. package/src/memory/embedding-types.ts +9 -1
  212. package/src/memory/indexer.ts +7 -46
  213. package/src/memory/invite-store.ts +19 -0
  214. package/src/memory/items-extractor.ts +274 -76
  215. package/src/memory/job-handlers/backfill.ts +2 -127
  216. package/src/memory/job-handlers/cleanup.ts +2 -16
  217. package/src/memory/job-handlers/extraction.ts +2 -138
  218. package/src/memory/job-handlers/index-maintenance.ts +1 -6
  219. package/src/memory/job-handlers/summarization.ts +3 -148
  220. package/src/memory/job-utils.ts +21 -59
  221. package/src/memory/jobs-store.ts +1 -159
  222. package/src/memory/jobs-worker.ts +9 -52
  223. package/src/memory/migrations/104-core-indexes.ts +3 -3
  224. package/src/memory/migrations/149-oauth-tables.ts +2 -0
  225. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
  226. package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
  227. package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
  228. package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
  229. package/src/memory/migrations/154-drop-fts.ts +20 -0
  230. package/src/memory/migrations/155-drop-conflicts.ts +7 -0
  231. package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
  232. package/src/memory/migrations/157-invite-contact-id.ts +104 -0
  233. package/src/memory/migrations/index.ts +8 -0
  234. package/src/memory/migrations/registry.ts +6 -0
  235. package/src/memory/qdrant-client.ts +148 -51
  236. package/src/memory/raw-query.ts +1 -1
  237. package/src/memory/retriever.test.ts +294 -273
  238. package/src/memory/retriever.ts +421 -645
  239. package/src/memory/schema/calls.ts +2 -0
  240. package/src/memory/schema/contacts.ts +1 -0
  241. package/src/memory/schema/memory-core.ts +3 -48
  242. package/src/memory/schema/oauth.ts +2 -0
  243. package/src/memory/search/formatting.ts +263 -176
  244. package/src/memory/search/lexical.ts +1 -254
  245. package/src/memory/search/ranking.ts +0 -455
  246. package/src/memory/search/semantic.ts +100 -14
  247. package/src/memory/search/staleness.ts +47 -0
  248. package/src/memory/search/tier-classifier.ts +21 -0
  249. package/src/memory/search/types.ts +15 -77
  250. package/src/memory/task-memory-cleanup.ts +4 -6
  251. package/src/messaging/provider.ts +1 -1
  252. package/src/messaging/providers/gmail/adapter.ts +1 -1
  253. package/src/messaging/providers/gmail/mime-builder.ts +17 -7
  254. package/src/messaging/providers/telegram-bot/adapter.ts +17 -8
  255. package/src/messaging/providers/whatsapp/adapter.ts +13 -9
  256. package/src/messaging/registry.ts +9 -5
  257. package/src/oauth/byo-connection.test.ts +40 -25
  258. package/src/oauth/connect-orchestrator.ts +4 -10
  259. package/src/oauth/connection-resolver.ts +20 -6
  260. package/src/oauth/manual-token-connection.ts +5 -5
  261. package/src/oauth/oauth-store.ts +183 -31
  262. package/src/oauth/platform-connection.test.ts +1 -1
  263. package/src/oauth/provider-behaviors.ts +503 -4
  264. package/src/oauth/seed-providers.ts +214 -8
  265. package/src/oauth/token-persistence.ts +31 -16
  266. package/src/permissions/defaults.ts +1 -0
  267. package/src/permissions/trust-store.ts +23 -1
  268. package/src/playbooks/playbook-compiler.ts +1 -1
  269. package/src/prompts/system-prompt.ts +18 -2
  270. package/src/providers/anthropic/client.ts +56 -126
  271. package/src/providers/types.ts +7 -1
  272. package/src/runtime/AGENTS.md +9 -0
  273. package/src/runtime/auth/route-policy.ts +6 -3
  274. package/src/runtime/channel-readiness-service.ts +48 -40
  275. package/src/runtime/guardian-reply-router.ts +24 -22
  276. package/src/runtime/http-server.ts +2 -2
  277. package/src/runtime/http-types.ts +2 -0
  278. package/src/runtime/invite-redemption-service.ts +72 -12
  279. package/src/runtime/invite-service.ts +43 -0
  280. package/src/runtime/middleware/twilio-validation.ts +1 -1
  281. package/src/runtime/pending-interactions.ts +2 -2
  282. package/src/runtime/routes/brain-graph-routes.ts +10 -90
  283. package/src/runtime/routes/btw-routes.ts +10 -5
  284. package/src/runtime/routes/conversation-routes.ts +56 -11
  285. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
  286. package/src/runtime/routes/integrations/slack/channel.ts +2 -2
  287. package/src/runtime/routes/integrations/telegram.ts +2 -2
  288. package/src/runtime/routes/integrations/twilio.ts +17 -17
  289. package/src/runtime/routes/invite-routes.ts +29 -4
  290. package/src/runtime/routes/memory-item-routes.test.ts +754 -0
  291. package/src/runtime/routes/memory-item-routes.ts +503 -0
  292. package/src/runtime/routes/secret-routes.ts +17 -0
  293. package/src/runtime/routes/session-management-routes.ts +3 -3
  294. package/src/runtime/routes/settings-routes.ts +3 -3
  295. package/src/runtime/routes/trust-rules-routes.ts +14 -0
  296. package/src/runtime/routes/workspace-routes.ts +9 -4
  297. package/src/runtime/routes/workspace-utils.ts +8 -2
  298. package/src/schedule/integration-status.ts +26 -19
  299. package/src/security/keychain-broker-client.ts +17 -4
  300. package/src/security/oauth2.ts +6 -7
  301. package/src/security/secure-keys.ts +44 -19
  302. package/src/security/token-manager.ts +46 -39
  303. package/src/services/vercel-deploy.ts +0 -24
  304. package/src/signals/confirm.ts +78 -0
  305. package/src/signals/mcp-reload.ts +18 -0
  306. package/src/skills/catalog-install.ts +74 -18
  307. package/src/skills/skillssh-registry.ts +503 -0
  308. package/src/tools/assets/search.ts +5 -1
  309. package/src/tools/computer-use/definitions.ts +0 -10
  310. package/src/tools/computer-use/registry.ts +1 -1
  311. package/src/tools/credentials/vault.ts +22 -7
  312. package/src/tools/memory/definitions.ts +4 -13
  313. package/src/tools/memory/handlers.test.ts +83 -103
  314. package/src/tools/memory/handlers.ts +50 -85
  315. package/src/tools/network/script-proxy/session-manager.ts +8 -8
  316. package/src/tools/schedule/create.ts +10 -3
  317. package/src/tools/schedule/update.ts +8 -1
  318. package/src/tools/skills/load.ts +25 -2
  319. package/src/watcher/provider-types.ts +1 -1
  320. package/src/watcher/providers/github.ts +1 -1
  321. package/src/watcher/providers/gmail.ts +3 -3
  322. package/src/watcher/providers/google-calendar.ts +3 -3
  323. package/src/watcher/providers/linear.ts +1 -1
  324. package/src/__tests__/clarification-resolver.test.ts +0 -193
  325. package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
  326. package/src/__tests__/conflict-policy.test.ts +0 -269
  327. package/src/__tests__/conflict-store.test.ts +0 -372
  328. package/src/__tests__/contradiction-checker.test.ts +0 -361
  329. package/src/__tests__/entity-extractor.test.ts +0 -211
  330. package/src/__tests__/entity-search.test.ts +0 -1117
  331. package/src/__tests__/profile-compiler.test.ts +0 -392
  332. package/src/__tests__/session-conflict-gate.test.ts +0 -1228
  333. package/src/__tests__/session-profile-injection.test.ts +0 -557
  334. package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
  335. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
  336. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
  337. package/src/daemon/session-conflict-gate.ts +0 -167
  338. package/src/daemon/session-dynamic-profile.ts +0 -77
  339. package/src/memory/clarification-resolver.ts +0 -417
  340. package/src/memory/conflict-intent.ts +0 -205
  341. package/src/memory/conflict-policy.ts +0 -127
  342. package/src/memory/conflict-store.ts +0 -410
  343. package/src/memory/contradiction-checker.ts +0 -508
  344. package/src/memory/entity-extractor.ts +0 -535
  345. package/src/memory/format-recall.ts +0 -47
  346. package/src/memory/fts-reconciler.ts +0 -165
  347. package/src/memory/job-handlers/conflict.ts +0 -200
  348. package/src/memory/profile-compiler.ts +0 -195
  349. package/src/memory/recall-cache.ts +0 -117
  350. package/src/memory/search/entity.ts +0 -535
  351. package/src/memory/search/query-expansion.test.ts +0 -70
  352. package/src/memory/search/query-expansion.ts +0 -118
  353. package/src/runtime/routes/mcp-routes.ts +0 -20
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import type { ChannelId } from "../channels/types.js";
11
- import { findContactChannel } from "../contacts/contact-store.js";
11
+ import { findContactChannel, getContact } from "../contacts/contact-store.js";
12
12
  import { upsertContactChannel } from "../contacts/contacts-write.js";
13
13
  import { getSqlite } from "../memory/db.js";
14
14
  import {
@@ -135,7 +135,17 @@ export function redeemInvite(params: {
135
135
  const existingChannel = contactResult?.channel ?? null;
136
136
  const existingContact = contactResult?.contact ?? null;
137
137
 
138
- if (existingChannel && existingChannel.status === "active") {
138
+ // If the invite targets a specific contact and the sender's existing channel
139
+ // belongs to a different contact, ignore the existing match — the invite
140
+ // should bind the sender's identity to the target contact, not the existing one.
141
+ const targetMismatch =
142
+ existingContact && existingContact.id !== invite.contactId;
143
+
144
+ if (
145
+ existingChannel &&
146
+ existingChannel.status === "active" &&
147
+ !targetMismatch
148
+ ) {
139
149
  return { ok: true, type: "already_member", memberId: existingChannel.id };
140
150
  }
141
151
 
@@ -150,7 +160,7 @@ export function redeemInvite(params: {
150
160
  // in a non-active state (revoked/pending), reactivate it via upsertContactChannel
151
161
  // and consume an invite use atomically. The fresh-member path below also
152
162
  // uses upsertContactChannel to keep contacts in sync.
153
- if (existingChannel) {
163
+ if (existingChannel && !targetMismatch) {
154
164
  // Sentinel error used to trigger a transaction rollback when the invite
155
165
  // was concurrently revoked/expired between pre-validation and write time.
156
166
  const STALE_INVITE = Symbol("stale_invite");
@@ -189,6 +199,7 @@ export function redeemInvite(params: {
189
199
  inviteId: invite.id,
190
200
  verifiedAt: Date.now(),
191
201
  verifiedVia: "invite",
202
+ contactId: invite.contactId,
192
203
  });
193
204
 
194
205
  const recorded = recordInviteUse({
@@ -220,6 +231,16 @@ export function redeemInvite(params: {
220
231
 
221
232
  // Fresh member creation: upsert into contacts tables and consume an invite
222
233
  // use atomically, mirroring the reactivation path above.
234
+ // When the invite targets a specific contact (targetMismatch path), preserve
235
+ // the target contact's guardian-assigned display name if it has one.
236
+ let freshDisplayName = displayName;
237
+ if (invite.contactId) {
238
+ const targetContact = getContact(invite.contactId);
239
+ if (targetContact?.displayName?.trim().length) {
240
+ freshDisplayName = targetContact.displayName;
241
+ }
242
+ }
243
+
223
244
  const STALE_INVITE_FRESH = Symbol("stale_invite_fresh");
224
245
  let freshResult: ReturnType<typeof upsertContactChannel> | undefined;
225
246
  try {
@@ -229,13 +250,14 @@ export function redeemInvite(params: {
229
250
  sourceChannel,
230
251
  externalUserId,
231
252
  externalChatId,
232
- displayName,
253
+ displayName: freshDisplayName,
233
254
  username,
234
255
  status: "active",
235
256
  policy: "allow",
236
257
  inviteId: invite.id,
237
258
  verifiedAt: Date.now(),
238
259
  verifiedVia: "invite",
260
+ contactId: invite.contactId,
239
261
  });
240
262
 
241
263
  const recorded = recordInviteUse({
@@ -338,8 +360,18 @@ export function redeemVoiceInviteCode(params: {
338
360
  externalUserId: canonicalCallerId,
339
361
  });
340
362
  const existingVoiceChannel = voiceContactResult?.channel ?? null;
363
+ const voiceContact = voiceContactResult?.contact ?? null;
364
+
365
+ // If the invite targets a specific contact and the sender's existing channel
366
+ // belongs to a different contact, ignore the existing match — the invite
367
+ // should bind the sender's identity to the target contact, not the existing one.
368
+ const targetMismatch = voiceContact && voiceContact.id !== invite.contactId;
341
369
 
342
- if (existingVoiceChannel && existingVoiceChannel.status === "active") {
370
+ if (
371
+ existingVoiceChannel &&
372
+ existingVoiceChannel.status === "active" &&
373
+ !targetMismatch
374
+ ) {
343
375
  return {
344
376
  ok: true,
345
377
  type: "already_member",
@@ -356,12 +388,17 @@ export function redeemVoiceInviteCode(params: {
356
388
  const STALE_INVITE = Symbol("stale_invite");
357
389
  let memberId: string | undefined;
358
390
 
359
- // Reactivation should not overwrite a guardian-managed nickname (same
360
- // protection as the token-based redemption path above).
361
- const voiceContact = voiceContactResult?.contact ?? null;
362
- const preservedDisplayName = voiceContact?.displayName?.trim().length
391
+ // When the invite targets a specific contact (targetMismatch path), preserve
392
+ // the target contact's guardian-assigned display name if it has one.
393
+ let preservedDisplayName = voiceContact?.displayName?.trim().length
363
394
  ? voiceContact.displayName
364
395
  : (invite.friendName ?? undefined);
396
+ if (targetMismatch && invite.contactId) {
397
+ const targetContact = getContact(invite.contactId);
398
+ if (targetContact?.displayName?.trim().length) {
399
+ preservedDisplayName = targetContact.displayName;
400
+ }
401
+ }
365
402
 
366
403
  try {
367
404
  getSqlite()
@@ -376,6 +413,7 @@ export function redeemVoiceInviteCode(params: {
376
413
  inviteId: invite.id,
377
414
  verifiedAt: Date.now(),
378
415
  verifiedVia: "invite",
416
+ contactId: invite.contactId,
379
417
  });
380
418
  memberId = writeResult!.channel.id;
381
419
 
@@ -476,7 +514,17 @@ export function redeemInviteByCode(params: {
476
514
  const existingChannel = contactResult?.channel ?? null;
477
515
  const existingContact = contactResult?.contact ?? null;
478
516
 
479
- if (existingChannel && existingChannel.status === "active") {
517
+ // If the invite targets a specific contact and the sender's existing channel
518
+ // belongs to a different contact, ignore the existing match — the invite
519
+ // should bind the sender's identity to the target contact, not the existing one.
520
+ const targetMismatch =
521
+ existingContact && existingContact.id !== invite.contactId;
522
+
523
+ if (
524
+ existingChannel &&
525
+ existingChannel.status === "active" &&
526
+ !targetMismatch
527
+ ) {
480
528
  return { ok: true, type: "already_member", memberId: existingChannel.id };
481
529
  }
482
530
 
@@ -489,7 +537,7 @@ export function redeemInviteByCode(params: {
489
537
 
490
538
  // Inactive member reactivation: reactivate via upsertContactChannel and consume
491
539
  // an invite use atomically.
492
- if (existingChannel) {
540
+ if (existingChannel && !targetMismatch) {
493
541
  const STALE_INVITE_REACTIVATE = Symbol("stale_invite_reactivate");
494
542
  const canonicalMemberId = existingChannel.externalUserId
495
543
  ? canonicalizeInboundIdentity(
@@ -525,6 +573,7 @@ export function redeemInviteByCode(params: {
525
573
  inviteId: invite.id,
526
574
  verifiedAt: Date.now(),
527
575
  verifiedVia: "invite",
576
+ contactId: invite.contactId,
528
577
  });
529
578
 
530
579
  const recorded = recordInviteUse({
@@ -553,6 +602,16 @@ export function redeemInviteByCode(params: {
553
602
 
554
603
  // Fresh member creation: upsert into contacts tables and consume an invite
555
604
  // use atomically.
605
+ // When the invite targets a specific contact (targetMismatch path), preserve
606
+ // the target contact's guardian-assigned display name if it has one.
607
+ let freshDisplayName = displayName;
608
+ if (invite.contactId) {
609
+ const targetContact = getContact(invite.contactId);
610
+ if (targetContact?.displayName?.trim().length) {
611
+ freshDisplayName = targetContact.displayName;
612
+ }
613
+ }
614
+
556
615
  const STALE_INVITE_FRESH = Symbol("stale_invite_fresh");
557
616
  let freshResult: ReturnType<typeof upsertContactChannel> | undefined;
558
617
  try {
@@ -562,13 +621,14 @@ export function redeemInviteByCode(params: {
562
621
  sourceChannel,
563
622
  externalUserId,
564
623
  externalChatId,
565
- displayName,
624
+ displayName: freshDisplayName,
566
625
  username,
567
626
  status: "active",
568
627
  policy: "allow",
569
628
  inviteId: invite.id,
570
629
  verifiedAt: Date.now(),
571
630
  verifiedVia: "invite",
631
+ contactId: invite.contactId,
572
632
  });
573
633
 
574
634
  const recorded = recordInviteUse({
@@ -8,14 +8,17 @@
8
8
  * /v1/contacts/channels endpoints.
9
9
  */
10
10
 
11
+ import { startInviteCall } from "../calls/call-domain.js";
11
12
  import { isChannelId } from "../channels/types.js";
12
13
  import {
13
14
  createInvite,
15
+ findById,
14
16
  findByTokenHash,
15
17
  hashToken,
16
18
  type IngressInvite,
17
19
  type InviteStatus,
18
20
  listInvites,
21
+ markInviteExpired,
19
22
  revokeInvite,
20
23
  } from "../memory/invite-store.js";
21
24
  import {
@@ -156,11 +159,16 @@ export async function createIngressInvite(params: {
156
159
  voiceCodeDigits?: number;
157
160
  friendName?: string;
158
161
  guardianName?: string;
162
+ contactId: string;
159
163
  }): Promise<IngressResult<InviteResponseData>> {
160
164
  if (!params.sourceChannel) {
161
165
  return { ok: false, error: "sourceChannel is required for create" };
162
166
  }
163
167
 
168
+ if (!params.contactId) {
169
+ return { ok: false, error: "contactId is required for create" };
170
+ }
171
+
164
172
  // For voice invites: generate a one-time numeric code, hash it, and pass
165
173
  // the hash to the store. The plaintext code is included in the response
166
174
  // exactly once and never stored.
@@ -210,6 +218,7 @@ export async function createIngressInvite(params: {
210
218
 
211
219
  const { invite, rawToken } = createInvite({
212
220
  sourceChannel: params.sourceChannel,
221
+ contactId: params.contactId,
213
222
  note: params.note,
214
223
  maxUses: params.maxUses,
215
224
  expiresInMs: params.expiresInMs,
@@ -250,6 +259,10 @@ export async function createIngressInvite(params: {
250
259
  });
251
260
  }
252
261
 
262
+ if (isVoice && params.friendName) {
263
+ guardianInstruction = `${params.friendName} will need this code when they answer. Share it with them first.`;
264
+ }
265
+
253
266
  // Voice invites must not expose the token — callers must redeem via the
254
267
  // identity-bound voice code flow, not the generic token redemption path.
255
268
  return {
@@ -291,6 +304,36 @@ export function revokeIngressInvite(
291
304
  return { ok: true, data: inviteToResponse(revoked) };
292
305
  }
293
306
 
307
+ export async function triggerInviteCall(
308
+ inviteId: string,
309
+ ): Promise<IngressResult<{ callSid: string }>> {
310
+ if (!inviteId) return { ok: false, error: "inviteId is required" };
311
+ const invite = findById(inviteId);
312
+ if (!invite) return { ok: false, error: "Invite not found" };
313
+ if (invite.status !== "active")
314
+ return { ok: false, error: "Invite is not active" };
315
+ if (invite.expiresAt && invite.expiresAt <= Date.now()) {
316
+ markInviteExpired(invite.id);
317
+ return { ok: false, error: "Invite has expired" };
318
+ }
319
+ if (invite.sourceChannel !== "phone")
320
+ return { ok: false, error: "Only phone invites support call triggering" };
321
+ if (
322
+ !invite.expectedExternalUserId ||
323
+ !invite.friendName ||
324
+ !invite.guardianName
325
+ ) {
326
+ return { ok: false, error: "Invite is missing required voice metadata" };
327
+ }
328
+ const result = await startInviteCall({
329
+ phoneNumber: invite.expectedExternalUserId,
330
+ friendName: invite.friendName,
331
+ guardianName: invite.guardianName,
332
+ });
333
+ if (!result.ok) return { ok: false, error: result.error };
334
+ return { ok: true, data: { callSid: result.callSid } };
335
+ }
336
+
294
337
  export function redeemIngressInvite(params: {
295
338
  token?: string;
296
339
  externalUserId?: string;
@@ -57,7 +57,7 @@ export async function validateTwilioWebhook(
57
57
  ): Promise<{ body: string } | Response> {
58
58
  const rawBody = await req.text();
59
59
 
60
- const authToken = TwilioConversationRelayProvider.getAuthToken();
60
+ const authToken = await TwilioConversationRelayProvider.getAuthToken();
61
61
 
62
62
  if (!authToken) {
63
63
  log.error(
@@ -6,8 +6,8 @@
6
6
  * host_bash_request, host_file_request, or host_cu_request, the onEvent
7
7
  * callback registers the interaction here. Standalone HTTP endpoints
8
8
  * (/v1/confirm, /v1/secret, /v1/trust-rules, /v1/host-bash-result,
9
- * /v1/host-file-result, /v1/host-cu-result) look up the session from
10
- * this tracker to resolve the interaction.
9
+ * /v1/host-file-result, /v1/host-cu-result) look up the session from this
10
+ * tracker to resolve the interaction.
11
11
  */
12
12
 
13
13
  import type { Session } from "../daemon/session.js";
@@ -2,7 +2,7 @@
2
2
  * Route handlers for the brain graph visualization endpoint.
3
3
  *
4
4
  * Queries the memory database to return a knowledge graph shaped for brain-lobe
5
- * visualization, with entities mapped to brain regions based on their type.
5
+ * visualization, with memory items mapped to brain regions based on their kind.
6
6
  */
7
7
 
8
8
  import { readFileSync } from "node:fs";
@@ -11,65 +11,23 @@ import { join } from "node:path";
11
11
  import { count } from "drizzle-orm";
12
12
 
13
13
  import { getDb } from "../../memory/db.js";
14
- import {
15
- memoryEntities,
16
- memoryEntityRelations,
17
- memoryItems,
18
- } from "../../memory/schema.js";
14
+ import { memoryItems } from "../../memory/schema.js";
19
15
  import { resolveBundledDir } from "../../util/bundled-asset.js";
20
16
  import type { RouteDefinition } from "../http-router.js";
21
17
 
22
- function getLobeRegion(entityType: string): string {
23
- switch (entityType) {
24
- case "person":
25
- case "organization":
26
- return "right-social";
27
- case "project":
28
- case "company":
29
- return "left-planning";
30
- case "tool":
31
- return "left-technical";
32
- case "concept":
33
- return "right-creative";
34
- case "location":
35
- return "right-spatial";
36
- default:
37
- return "center";
38
- }
39
- }
40
-
41
- function getEntityColor(entityType: string): string {
42
- switch (entityType) {
43
- case "person":
44
- return "#22c55e";
45
- case "project":
46
- return "#f97316";
47
- case "tool":
48
- return "#06b6d4";
49
- case "company":
50
- return "#a855f7";
51
- case "organization":
52
- return "#a855f7";
53
- case "concept":
54
- return "#eab308";
55
- case "location":
56
- return "#14b8a6";
57
- default:
58
- return "#94a3b8";
59
- }
60
- }
61
-
62
18
  function getMemoryKindColor(kind: string): string {
63
19
  switch (kind) {
64
- case "profile":
20
+ case "identity":
65
21
  return "#8b5cf6";
66
22
  case "preference":
67
23
  return "#3b82f6";
24
+ case "project":
25
+ return "#10b981";
26
+ case "decision":
27
+ return "#f59e0b";
68
28
  case "constraint":
69
29
  return "#ef4444";
70
- case "instruction":
71
- return "#f59e0b";
72
- case "style":
30
+ case "event":
73
31
  return "#ec4899";
74
32
  default:
75
33
  return "#94a3b8";
@@ -80,27 +38,6 @@ export function handleGetBrainGraph(): Response {
80
38
  try {
81
39
  const db = getDb();
82
40
 
83
- const entityRows = db
84
- .select({
85
- id: memoryEntities.id,
86
- name: memoryEntities.name,
87
- type: memoryEntities.type,
88
- mentionCount: memoryEntities.mentionCount,
89
- firstSeenAt: memoryEntities.firstSeenAt,
90
- lastSeenAt: memoryEntities.lastSeenAt,
91
- })
92
- .from(memoryEntities)
93
- .all();
94
-
95
- const relationRows = db
96
- .select({
97
- sourceEntityId: memoryEntityRelations.sourceEntityId,
98
- targetEntityId: memoryEntityRelations.targetEntityId,
99
- relation: memoryEntityRelations.relation,
100
- })
101
- .from(memoryEntityRelations)
102
- .all();
103
-
104
41
  const kindCountRows = db
105
42
  .select({
106
43
  kind: memoryItems.kind,
@@ -110,23 +47,6 @@ export function handleGetBrainGraph(): Response {
110
47
  .groupBy(memoryItems.kind)
111
48
  .all();
112
49
 
113
- const entities = entityRows.map((entity) => ({
114
- id: entity.id,
115
- name: entity.name,
116
- type: entity.type,
117
- lobeRegion: getLobeRegion(entity.type),
118
- color: getEntityColor(entity.type),
119
- mentionCount: entity.mentionCount,
120
- firstSeenAt: entity.firstSeenAt,
121
- lastSeenAt: entity.lastSeenAt,
122
- }));
123
-
124
- const relations = relationRows.map((rel) => ({
125
- sourceId: rel.sourceEntityId,
126
- targetId: rel.targetEntityId,
127
- relation: rel.relation,
128
- }));
129
-
130
50
  const memorySummary = kindCountRows.map((row) => ({
131
51
  kind: row.kind,
132
52
  count: row.count,
@@ -139,8 +59,8 @@ export function handleGetBrainGraph(): Response {
139
59
  );
140
60
 
141
61
  return Response.json({
142
- entities,
143
- relations,
62
+ entities: [],
63
+ relations: [],
144
64
  memorySummary,
145
65
  totalKnowledgeCount,
146
66
  generatedAt: new Date().toISOString(),
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  import { buildToolDefinitions } from "../../daemon/session-tool-setup.js";
12
- import { getOrCreateConversation } from "../../memory/conversation-key-store.js";
12
+ import { getConversationByKey } from "../../memory/conversation-key-store.js";
13
13
  import {
14
14
  createTimeout,
15
15
  userMessage,
@@ -53,10 +53,15 @@ async function handleBtw(
53
53
  );
54
54
  }
55
55
 
56
- const mapping = getOrCreateConversation(conversationKey);
57
- const session = await deps.sendMessageDeps.getOrCreateSession(
58
- mapping.conversationId,
59
- );
56
+ // Look up an existing conversation — never create one. BTW is ephemeral
57
+ // (the file header promises "No messages are persisted"), so we must not
58
+ // call getOrCreateConversation which would insert a DB row. When no
59
+ // conversation exists (e.g. greeting generation for a draft thread), we
60
+ // still get a usable session via getOrCreateSession with the raw key; the
61
+ // session lives only in memory and disappears on restart.
62
+ const mapping = getConversationByKey(conversationKey);
63
+ const sessionId = mapping?.conversationId ?? conversationKey;
64
+ const session = await deps.sendMessageDeps.getOrCreateSession(sessionId);
60
65
 
61
66
  const messages = [...session.getMessages(), userMessage(content.trim())];
62
67
  const tools = buildToolDefinitions();
@@ -315,16 +315,53 @@ export function handleListMessages(
315
315
  let prevAssistantTimestamp = 0;
316
316
  const messages: RuntimeMessagePayload[] = parsed.map((m) => {
317
317
  let msgAttachments: RuntimeAttachmentMetadata[] = [];
318
- if (m.role === "assistant" && m.id) {
319
- const linked = attachmentsStore.getAttachmentMetadataForMessage(m.id);
320
- if (linked.length > 0) {
321
- msgAttachments = linked.map((a) => ({
322
- id: a.id,
323
- filename: a.originalFilename,
324
- mimeType: a.mimeType,
325
- sizeBytes: a.sizeBytes,
326
- kind: a.kind,
327
- }));
318
+ if (m.id) {
319
+ if (m.role === "user") {
320
+ // Use metadata-only query first to avoid loading large base64
321
+ // blobs for non-image attachments (documents, audio). Then
322
+ // selectively fetch full data only for images so the client can
323
+ // generate thumbnails for inline display on history restore.
324
+ const linked = attachmentsStore.getAttachmentMetadataForMessage(m.id);
325
+ if (linked.length > 0) {
326
+ msgAttachments = linked.map((a) => {
327
+ if (a.mimeType.startsWith("image/")) {
328
+ const full = attachmentsStore.getAttachmentById(a.id);
329
+ return {
330
+ id: a.id,
331
+ filename: a.originalFilename,
332
+ mimeType: a.mimeType,
333
+ sizeBytes: a.sizeBytes,
334
+ kind: a.kind,
335
+ ...(full?.dataBase64 ? { data: full.dataBase64 } : {}),
336
+ ...(a.thumbnailBase64
337
+ ? { thumbnailData: a.thumbnailBase64 }
338
+ : {}),
339
+ };
340
+ }
341
+ return {
342
+ id: a.id,
343
+ filename: a.originalFilename,
344
+ mimeType: a.mimeType,
345
+ sizeBytes: a.sizeBytes,
346
+ kind: a.kind,
347
+ ...(a.thumbnailBase64
348
+ ? { thumbnailData: a.thumbnailBase64 }
349
+ : {}),
350
+ };
351
+ });
352
+ }
353
+ } else {
354
+ const linked = attachmentsStore.getAttachmentMetadataForMessage(m.id);
355
+ if (linked.length > 0) {
356
+ msgAttachments = linked.map((a) => ({
357
+ id: a.id,
358
+ filename: a.originalFilename,
359
+ mimeType: a.mimeType,
360
+ sizeBytes: a.sizeBytes,
361
+ kind: a.kind,
362
+ ...(a.thumbnailBase64 ? { thumbnailData: a.thumbnailBase64 } : {}),
363
+ }));
364
+ }
328
365
  }
329
366
  }
330
367
 
@@ -648,9 +685,17 @@ export async function handleSendMessage(
648
685
  session.setHostFileProxy(fileProxy);
649
686
  }
650
687
  if (!session.isProcessing() || !session.hostCuProxy) {
651
- const cuProxy = new HostCuProxy(onEvent);
688
+ const cuProxy = new HostCuProxy(onEvent, (requestId) => {
689
+ pendingInteractions.resolve(requestId);
690
+ });
652
691
  session.setHostCuProxy(cuProxy);
653
692
  }
693
+ // Only preactivate CU when the session is idle — if the session is
694
+ // processing, this message will be queued and preactivation is deferred
695
+ // to dequeue time in drainQueueImpl to avoid mutating in-flight turn state.
696
+ if (!session.isProcessing()) {
697
+ session.addPreactivatedSkillId("computer-use");
698
+ }
654
699
  } else if (!session.isProcessing()) {
655
700
  session.setHostBashProxy(undefined);
656
701
  session.setHostFileProxy(undefined);
@@ -334,7 +334,7 @@ export async function enforceIngressAcl(
334
334
  dmCallbackUrl,
335
335
  {
336
336
  chatId: senderUserId,
337
- text: "I've notified the owner. They'll share a verification code with you if they approve access. You can reply with the code here.",
337
+ text: "I've notified the owner that you'd like to chat with me. If they approve your request, they'll share a 6-digit verification code with you. You can reply with the code here.",
338
338
  assistantId,
339
339
  },
340
340
  mintBearerToken(),
@@ -423,11 +423,12 @@ export async function enforceIngressAcl(
423
423
 
424
424
  if (resolvedMember) {
425
425
  if (resolvedMember.channel.status !== "active") {
426
+ const isBlockedMember = resolvedMember.channel.status === "blocked";
426
427
  // Same bypass logic as the no-member branch: verification codes and
427
- // bootstrap commands must pass through even when the member record is
428
- // revoked/blocked otherwise the user can never re-verify.
428
+ // bootstrap commands must pass through for re-verifiable states
429
+ // (pending/revoked), but never for blocked members.
429
430
  let denyInactiveMember = true;
430
- if (isGuardianVerifyCode) {
431
+ if (!isBlockedMember && isGuardianVerifyCode) {
431
432
  const hasPendingChallenge = !!getPendingSession(sourceChannel);
432
433
  const hasActiveOutboundSession = !!findActiveSession(sourceChannel);
433
434
  if (hasPendingChallenge || hasActiveOutboundSession) {
@@ -444,7 +445,7 @@ export async function enforceIngressAcl(
444
445
  );
445
446
  }
446
447
  }
447
- if (isBootstrapCommand) {
448
+ if (!isBlockedMember && isBootstrapCommand) {
448
449
  const bootstrapPayload = (
449
450
  rawCommandIntentForAcl as Record<string, unknown>
450
451
  ).payload as string;
@@ -471,9 +472,11 @@ export async function enforceIngressAcl(
471
472
  }
472
473
 
473
474
  // ── Invite token intercept (inactive member) ──
474
- // Same as the non-member branch: invite tokens can reactivate
475
- // revoked/pending members without requiring guardian approval.
476
- if (inviteToken && denyInactiveMember) {
475
+ // Invite tokens can reactivate revoked/pending members without
476
+ // requiring guardian approval, but blocked members are excluded so
477
+ // they are short-circuited at the ACL layer rather than entering the
478
+ // redemption path.
479
+ if (!isBlockedMember && inviteToken && denyInactiveMember) {
477
480
  const inviteResult = await handleInviteTokenIntercept({
478
481
  rawToken: inviteToken,
479
482
  sourceChannel,
@@ -496,9 +499,15 @@ export async function enforceIngressAcl(
496
499
  }
497
500
 
498
501
  // ── 6-digit invite code intercept (inactive member) ──
499
- // Same as the non-member branch: codes can reactivate revoked/pending
500
- // members. Non-matching codes fall through to normal processing.
501
- if (denyInactiveMember && /^\d{6}$/.test(trimmedContent)) {
502
+ // Codes can reactivate revoked/pending members; non-matching codes
503
+ // fall through. Blocked members are excluded here for consistency —
504
+ // the redemption service would reject them anyway, but early exit
505
+ // avoids unnecessary work.
506
+ if (
507
+ !isBlockedMember &&
508
+ denyInactiveMember &&
509
+ /^\d{6}$/.test(trimmedContent)
510
+ ) {
502
511
  const codeInterceptResult = await handleInviteCodeIntercept({
503
512
  code: trimmedContent,
504
513
  sourceChannel,
@@ -579,7 +588,7 @@ export async function enforceIngressAcl(
579
588
  dmCallbackUrl,
580
589
  {
581
590
  chatId: senderUserId,
582
- text: "I've notified the owner. They'll share a verification code with you if they approve access. You can reply with the code here.",
591
+ text: "I've notified the owner that you'd like to chat with me. If they approve your request, they'll share a 6-digit verification code with you. You can reply with the code here.",
583
592
  assistantId,
584
593
  },
585
594
  mintBearerToken(),
@@ -20,8 +20,8 @@ import type { RouteDefinition } from "../../../http-router.js";
20
20
  /**
21
21
  * GET /v1/integrations/slack/channel/config
22
22
  */
23
- export function handleGetSlackChannelConfig(): Response {
24
- const result = getSlackChannelConfig();
23
+ export async function handleGetSlackChannelConfig(): Promise<Response> {
24
+ const result = await getSlackChannelConfig();
25
25
  return Response.json(result);
26
26
  }
27
27
 
@@ -20,8 +20,8 @@ import type { RouteDefinition } from "../../http-router.js";
20
20
  /**
21
21
  * GET /v1/integrations/telegram/config
22
22
  */
23
- export function handleGetTelegramConfig(): Response {
24
- const result = getTelegramConfig();
23
+ export async function handleGetTelegramConfig(): Promise<Response> {
24
+ const result = await getTelegramConfig();
25
25
  return Response.json(result);
26
26
  }
27
27