@vellumai/assistant 0.5.16 → 0.6.0

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 (407) hide show
  1. package/ARCHITECTURE.md +1 -1
  2. package/Dockerfile +0 -3
  3. package/knip.json +2 -1
  4. package/openapi.yaml +660 -80
  5. package/package.json +1 -1
  6. package/src/__tests__/actor-token-service.test.ts +68 -0
  7. package/src/__tests__/agent-loop.test.ts +0 -32
  8. package/src/__tests__/always-loaded-tools-guard.test.ts +2 -2
  9. package/src/__tests__/anthropic-provider.test.ts +57 -3
  10. package/src/__tests__/app-compiler.test.ts +120 -0
  11. package/src/__tests__/assistant-feature-flags-integration.test.ts +2 -2
  12. package/src/__tests__/call-conversation-messages.test.ts +2 -6
  13. package/src/__tests__/call-domain.test.ts +2 -6
  14. package/src/__tests__/call-pointer-messages.test.ts +2 -14
  15. package/src/__tests__/call-recovery.test.ts +2 -6
  16. package/src/__tests__/call-routes-http.test.ts +2 -6
  17. package/src/__tests__/call-store.test.ts +2 -6
  18. package/src/__tests__/cancel-resolves-conversation-key.test.ts +2 -6
  19. package/src/__tests__/canonical-guardian-store.test.ts +2 -6
  20. package/src/__tests__/channel-delivery-store.test.ts +2 -6
  21. package/src/__tests__/channel-retry-sweep.test.ts +2 -6
  22. package/src/__tests__/checker.test.ts +25 -3
  23. package/src/__tests__/clawhub.test.ts +54 -24
  24. package/src/__tests__/cli-command-risk-guard.test.ts +14 -0
  25. package/src/__tests__/cli-memory.test.ts +74 -69
  26. package/src/__tests__/config-schema.test.ts +1 -1
  27. package/src/__tests__/config-set-platform-guard.test.ts +302 -0
  28. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +2 -6
  29. package/src/__tests__/contacts-tools.test.ts +31 -0
  30. package/src/__tests__/context-overflow-reducer.test.ts +86 -0
  31. package/src/__tests__/context-token-estimator.test.ts +175 -10
  32. package/src/__tests__/conversation-agent-loop-overflow.test.ts +9 -0
  33. package/src/__tests__/conversation-agent-loop.test.ts +9 -0
  34. package/src/__tests__/conversation-attachments.test.ts +2 -6
  35. package/src/__tests__/conversation-attention-store.test.ts +2 -6
  36. package/src/__tests__/conversation-clear-safety.test.ts +2 -6
  37. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +4 -10
  38. package/src/__tests__/conversation-disk-view-integration.test.ts +2 -6
  39. package/src/__tests__/conversation-disk-view.test.ts +2 -6
  40. package/src/__tests__/conversation-error.test.ts +33 -2
  41. package/src/__tests__/conversation-fork-crud.test.ts +2 -6
  42. package/src/__tests__/conversation-history-web-search.test.ts +5 -0
  43. package/src/__tests__/conversation-load-history-repair.test.ts +5 -1
  44. package/src/__tests__/conversation-media-retry.test.ts +91 -0
  45. package/src/__tests__/conversation-starter-routes.test.ts +20 -11
  46. package/src/__tests__/conversation-store.test.ts +2 -6
  47. package/src/__tests__/conversation-usage.test.ts +2 -6
  48. package/src/__tests__/conversation-wipe.test.ts +11 -408
  49. package/src/__tests__/credential-execution-feature-gates.test.ts +3 -3
  50. package/src/__tests__/credential-execution-shell-lockdown.test.ts +2 -2
  51. package/src/__tests__/credential-security-e2e.test.ts +2 -0
  52. package/src/__tests__/followup-tools.test.ts +2 -6
  53. package/src/__tests__/graph-extraction-event-date.test.ts +186 -0
  54. package/src/__tests__/guardian-action-conversation-turn.test.ts +2 -6
  55. package/src/__tests__/guardian-action-followup-executor.test.ts +2 -6
  56. package/src/__tests__/guardian-action-followup-store.test.ts +2 -6
  57. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +2 -6
  58. package/src/__tests__/guardian-action-late-reply.test.ts +2 -6
  59. package/src/__tests__/guardian-action-store.test.ts +2 -6
  60. package/src/__tests__/guardian-binding-drift-heal.test.ts +2 -6
  61. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +8 -8
  62. package/src/__tests__/guardian-dispatch.test.ts +2 -6
  63. package/src/__tests__/guardian-grant-minting.test.ts +2 -14
  64. package/src/__tests__/guardian-principal-id-roundtrip.test.ts +2 -6
  65. package/src/__tests__/guardian-routing-invariants.test.ts +192 -6
  66. package/src/__tests__/guardian-routing-state.test.ts +2 -6
  67. package/src/__tests__/guardian-verification-voice-binding.test.ts +2 -6
  68. package/src/__tests__/inbound-invite-redemption.test.ts +2 -6
  69. package/src/__tests__/injection-block.test.ts +154 -0
  70. package/src/__tests__/install-meta.test.ts +506 -0
  71. package/src/__tests__/install-skill-routing.test.ts +292 -0
  72. package/src/__tests__/invite-redemption-service.test.ts +2 -6
  73. package/src/__tests__/invite-routes-http.test.ts +2 -6
  74. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +2 -14
  75. package/src/__tests__/list-messages-attachments.test.ts +2 -6
  76. package/src/__tests__/llm-context-route-provider.test.ts +2 -6
  77. package/src/__tests__/llm-request-log-turn-query.test.ts +2 -6
  78. package/src/__tests__/llm-usage-store.test.ts +2 -6
  79. package/src/__tests__/log-export-workspace.test.ts +2 -6
  80. package/src/__tests__/managed-store.test.ts +38 -11
  81. package/src/__tests__/memory-jobs-worker-backoff.test.ts +2 -8
  82. package/src/__tests__/memory-recall-log-store.test.ts +2 -6
  83. package/src/__tests__/memory-upsert-concurrency.test.ts +4 -112
  84. package/src/__tests__/non-member-access-request.test.ts +2 -6
  85. package/src/__tests__/notification-guardian-path.test.ts +2 -6
  86. package/src/__tests__/oauth-cli.test.ts +364 -2
  87. package/src/__tests__/oauth2-gateway-transport.test.ts +18 -3
  88. package/src/__tests__/outlook-attachments.test.ts +301 -0
  89. package/src/__tests__/outlook-automation-tools.test.ts +425 -0
  90. package/src/__tests__/outlook-categories.test.ts +212 -0
  91. package/src/__tests__/outlook-client-automation.test.ts +246 -0
  92. package/src/__tests__/outlook-compose-tools.test.ts +325 -0
  93. package/src/__tests__/outlook-declutter-tools.test.ts +585 -0
  94. package/src/__tests__/outlook-email-watcher.test.ts +322 -0
  95. package/src/__tests__/outlook-follow-up.test.ts +196 -0
  96. package/src/__tests__/outlook-messaging-provider.test.ts +498 -3
  97. package/src/__tests__/outlook-trash.test.ts +77 -0
  98. package/src/__tests__/outlook-unsubscribe.test.ts +250 -0
  99. package/src/__tests__/platform-callback-registration.test.ts +4 -4
  100. package/src/__tests__/playbook-execution.test.ts +76 -80
  101. package/src/__tests__/playbook-tools.test.ts +5 -7
  102. package/src/__tests__/provider-error-scenarios.test.ts +21 -0
  103. package/src/__tests__/rebuild-index-graph-nodes.test.ts +273 -0
  104. package/src/__tests__/registry.test.ts +2 -2
  105. package/src/__tests__/require-fresh-approval.test.ts +64 -2
  106. package/src/__tests__/runtime-events-sse-parity.test.ts +2 -6
  107. package/src/__tests__/runtime-events-sse.test.ts +2 -6
  108. package/src/__tests__/schedule-store.test.ts +2 -6
  109. package/src/__tests__/schedule-tools.test.ts +2 -6
  110. package/src/__tests__/scheduler-recurrence.test.ts +1 -5
  111. package/src/__tests__/scoped-approval-grants.test.ts +2 -6
  112. package/src/__tests__/scoped-grant-security-matrix.test.ts +2 -6
  113. package/src/__tests__/search-skills-unified.test.ts +421 -0
  114. package/src/__tests__/secret-onetime-send.test.ts +2 -0
  115. package/src/__tests__/send-endpoint-busy.test.ts +2 -6
  116. package/src/__tests__/sequence-store.test.ts +2 -6
  117. package/src/__tests__/server-history-render.test.ts +2 -6
  118. package/src/__tests__/skill-feature-flags-integration.test.ts +38 -31
  119. package/src/__tests__/skill-feature-flags.test.ts +6 -6
  120. package/src/__tests__/skill-load-feature-flag.test.ts +11 -11
  121. package/src/__tests__/skill-memory.test.ts +140 -98
  122. package/src/__tests__/skills-uninstall.test.ts +2 -2
  123. package/src/__tests__/skills.test.ts +1 -1
  124. package/src/__tests__/slack-inbound-verification.test.ts +2 -6
  125. package/src/__tests__/task-compiler.test.ts +2 -6
  126. package/src/__tests__/task-management-tools.test.ts +2 -6
  127. package/src/__tests__/task-memory-cleanup.test.ts +173 -229
  128. package/src/__tests__/task-runner.test.ts +2 -6
  129. package/src/__tests__/task-scheduler.test.ts +2 -6
  130. package/src/__tests__/test-preload.ts +3 -0
  131. package/src/__tests__/tool-approval-handler.test.ts +2 -6
  132. package/src/__tests__/tool-grant-request-escalation.test.ts +2 -6
  133. package/src/__tests__/tool-side-effects-slack-dm.test.ts +276 -0
  134. package/src/__tests__/trust-store.test.ts +1 -1
  135. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -6
  136. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +2 -6
  137. package/src/__tests__/trusted-contact-multichannel.test.ts +2 -6
  138. package/src/__tests__/trusted-contact-verification.test.ts +2 -6
  139. package/src/__tests__/turn-boundary-resolution.test.ts +2 -6
  140. package/src/__tests__/usage-cache-backfill-migration.test.ts +1 -6
  141. package/src/__tests__/usage-routes.test.ts +2 -6
  142. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  143. package/src/__tests__/voice-invite-redemption.test.ts +2 -6
  144. package/src/__tests__/voice-scoped-grant-consumer.test.ts +2 -6
  145. package/src/__tests__/voice-session-bridge.test.ts +2 -6
  146. package/src/__tests__/volume-security-guard.test.ts +2 -0
  147. package/src/__tests__/workspace-lifecycle.test.ts +29 -1
  148. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +2 -6
  149. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +2 -6
  150. package/src/__tests__/workspace-migration-026-backfill-install-meta.test.ts +558 -0
  151. package/src/__tests__/workspace-policy.test.ts +1 -1
  152. package/src/agent/attachments.ts +7 -2
  153. package/src/agent/image-optimize.ts +165 -0
  154. package/src/agent/loop.ts +1 -15
  155. package/src/bundler/app-compiler.ts +179 -2
  156. package/src/bundler/package-resolver.ts +3 -5
  157. package/src/cli/__tests__/notifications.test.ts +1 -2
  158. package/src/cli/cli-memory.ts +67 -64
  159. package/src/cli/commands/avatar.ts +3 -3
  160. package/src/cli/commands/config.ts +26 -13
  161. package/src/cli/commands/doctor.ts +2 -2
  162. package/src/cli/commands/memory.ts +41 -55
  163. package/src/cli/commands/oauth/__tests__/connect.test.ts +2 -2
  164. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +2 -2
  165. package/src/cli/commands/oauth/__tests__/mode.test.ts +8 -1
  166. package/src/cli/commands/oauth/__tests__/status.test.ts +2 -2
  167. package/src/cli/commands/oauth/connect.ts +11 -6
  168. package/src/cli/commands/oauth/mode.ts +7 -0
  169. package/src/cli/commands/oauth/shared.ts +39 -3
  170. package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
  171. package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
  172. package/src/cli/commands/platform/__tests__/status.test.ts +5 -5
  173. package/src/cli/commands/platform/index.ts +16 -16
  174. package/src/cli/commands/skills.ts +88 -16
  175. package/src/cli/commands/trust.ts +2 -2
  176. package/src/cli/lib/daemon-credential-client.ts +2 -3
  177. package/src/config/bundled-skills/acp/TOOLS.json +1 -1
  178. package/src/config/bundled-skills/contacts/SKILL.md +0 -1
  179. package/src/config/bundled-skills/contacts/TOOLS.json +0 -8
  180. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +0 -4
  181. package/src/config/bundled-skills/gmail/SKILL.md +2 -10
  182. package/src/config/bundled-skills/google-calendar/SKILL.md +1 -9
  183. package/src/config/bundled-skills/messaging/SKILL.md +10 -18
  184. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +40 -33
  185. package/src/config/bundled-skills/outlook/SKILL.md +189 -0
  186. package/src/config/bundled-skills/outlook/TOOLS.json +530 -0
  187. package/src/config/bundled-skills/outlook/tools/outlook-attachments.ts +85 -0
  188. package/src/config/bundled-skills/outlook/tools/outlook-categories.ts +77 -0
  189. package/src/config/bundled-skills/outlook/tools/outlook-draft.ts +84 -0
  190. package/src/config/bundled-skills/outlook/tools/outlook-follow-up.ts +94 -0
  191. package/src/config/bundled-skills/outlook/tools/outlook-forward.ts +49 -0
  192. package/src/config/bundled-skills/outlook/tools/outlook-outreach-scan.ts +237 -0
  193. package/src/config/bundled-skills/outlook/tools/outlook-rules.ts +161 -0
  194. package/src/config/bundled-skills/outlook/tools/outlook-send-draft.ts +32 -0
  195. package/src/config/bundled-skills/outlook/tools/outlook-sender-digest.ts +272 -0
  196. package/src/config/bundled-skills/outlook/tools/outlook-trash.ts +29 -0
  197. package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +129 -0
  198. package/src/config/bundled-skills/outlook/tools/outlook-vacation.ts +87 -0
  199. package/src/config/bundled-skills/outlook/tools/shared.ts +20 -0
  200. package/src/config/bundled-skills/outlook-calendar/SKILL.md +51 -0
  201. package/src/config/bundled-skills/outlook-calendar/TOOLS.json +221 -0
  202. package/src/config/bundled-skills/outlook-calendar/calendar-client.ts +252 -0
  203. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-check-availability.ts +53 -0
  204. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-create-event.ts +74 -0
  205. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-get-event.ts +18 -0
  206. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-list-events.ts +46 -0
  207. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-rsvp.ts +36 -0
  208. package/src/config/bundled-skills/outlook-calendar/tools/shared.ts +17 -0
  209. package/src/config/bundled-skills/outlook-calendar/types.ts +120 -0
  210. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +47 -40
  211. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +16 -29
  212. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +16 -18
  213. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +39 -47
  214. package/src/config/bundled-skills/slack/SKILL.md +1 -7
  215. package/src/config/bundled-tool-registry.ts +56 -4
  216. package/src/config/env-registry.ts +15 -8
  217. package/src/config/feature-flag-registry.json +21 -124
  218. package/src/config/schemas/platform.ts +8 -0
  219. package/src/config/schemas/timeouts.ts +1 -1
  220. package/src/config/skills.ts +18 -7
  221. package/src/context/token-estimator.ts +25 -18
  222. package/src/context/window-manager.ts +6 -2
  223. package/src/credential-execution/process-manager.ts +3 -1
  224. package/src/daemon/context-overflow-reducer.ts +46 -2
  225. package/src/daemon/conversation-agent-loop-handlers.ts +123 -82
  226. package/src/daemon/conversation-agent-loop.ts +96 -61
  227. package/src/daemon/conversation-error.ts +31 -8
  228. package/src/daemon/conversation-lifecycle.ts +33 -0
  229. package/src/daemon/conversation-media-retry.ts +85 -7
  230. package/src/daemon/conversation-notifiers.ts +4 -1
  231. package/src/daemon/conversation-runtime-assembly.ts +5 -0
  232. package/src/daemon/conversation.ts +41 -2
  233. package/src/daemon/daemon-control.ts +8 -2
  234. package/src/daemon/handlers/shared.ts +22 -12
  235. package/src/daemon/handlers/skills.ts +416 -202
  236. package/src/daemon/lifecycle.ts +40 -1
  237. package/src/daemon/main.ts +5 -1
  238. package/src/daemon/message-types/conversations.ts +4 -1
  239. package/src/daemon/message-types/messages.ts +3 -1
  240. package/src/daemon/message-types/skills.ts +97 -36
  241. package/src/daemon/providers-setup.ts +5 -0
  242. package/src/daemon/server.ts +11 -2
  243. package/src/daemon/tool-side-effects.ts +27 -5
  244. package/src/heartbeat/heartbeat-service.ts +1 -0
  245. package/src/hooks/cli.ts +2 -2
  246. package/src/hooks/runner.ts +15 -38
  247. package/src/inbound/platform-callback-registration.ts +14 -14
  248. package/src/memory/admin.ts +11 -45
  249. package/src/memory/conversation-bootstrap.ts +2 -0
  250. package/src/memory/conversation-crud.ts +242 -348
  251. package/src/memory/conversation-group-migration.ts +157 -0
  252. package/src/memory/conversation-queries.ts +4 -2
  253. package/src/memory/db-init.ts +30 -3
  254. package/src/memory/embed.ts +73 -0
  255. package/src/memory/embedding-backend.ts +8 -14
  256. package/src/memory/embedding-runtime-manager.ts +12 -114
  257. package/src/memory/fingerprint.ts +2 -2
  258. package/src/memory/graph/bootstrap.ts +512 -0
  259. package/src/memory/graph/capability-seed.ts +297 -0
  260. package/src/memory/graph/consolidation.ts +691 -0
  261. package/src/memory/graph/conversation-graph-memory.ts +630 -0
  262. package/src/memory/graph/decay.test.ts +208 -0
  263. package/src/memory/graph/decay.ts +195 -0
  264. package/src/memory/graph/extraction-job.ts +69 -0
  265. package/src/memory/graph/extraction.test.ts +936 -0
  266. package/src/memory/graph/extraction.ts +1254 -0
  267. package/src/memory/graph/graph-search.ts +266 -0
  268. package/src/memory/graph/image-ref-utils.ts +29 -0
  269. package/src/memory/graph/injection.test.ts +513 -0
  270. package/src/memory/graph/injection.ts +439 -0
  271. package/src/memory/graph/inspect.ts +534 -0
  272. package/src/memory/graph/narrative.ts +267 -0
  273. package/src/memory/graph/pattern-scan.ts +269 -0
  274. package/src/memory/graph/retriever.ts +1008 -0
  275. package/src/memory/graph/scoring.test.ts +548 -0
  276. package/src/memory/graph/scoring.ts +232 -0
  277. package/src/memory/graph/serendipity.ts +65 -0
  278. package/src/memory/graph/store.test.ts +1050 -0
  279. package/src/memory/graph/store.ts +699 -0
  280. package/src/memory/graph/tool-handlers.ts +426 -0
  281. package/src/memory/graph/tools.ts +141 -0
  282. package/src/memory/graph/triggers.test.ts +487 -0
  283. package/src/memory/graph/triggers.ts +223 -0
  284. package/src/memory/graph/types.ts +271 -0
  285. package/src/memory/group-crud.ts +191 -0
  286. package/src/memory/indexer.ts +37 -19
  287. package/src/memory/job-handlers/cleanup.ts +0 -53
  288. package/src/memory/job-handlers/conversation-starters.ts +91 -53
  289. package/src/memory/job-handlers/embedding.ts +5 -31
  290. package/src/memory/job-handlers/index-maintenance.ts +23 -11
  291. package/src/memory/job-handlers/summarization.ts +32 -17
  292. package/src/memory/job-utils.ts +1 -1
  293. package/src/memory/jobs-store.ts +50 -70
  294. package/src/memory/jobs-worker.ts +147 -112
  295. package/src/memory/message-content.ts +1 -0
  296. package/src/memory/migrations/202-memory-graph-tables.ts +130 -0
  297. package/src/memory/migrations/203-drop-memory-items-tables.ts +23 -0
  298. package/src/memory/migrations/204-rename-memory-graph-type-values.ts +46 -0
  299. package/src/memory/migrations/205-memory-graph-image-refs.ts +11 -0
  300. package/src/memory/migrations/index.ts +4 -0
  301. package/src/memory/migrations/registry.ts +8 -0
  302. package/src/memory/qdrant-client.ts +44 -17
  303. package/src/memory/schema/index.ts +1 -0
  304. package/src/memory/schema/memory-graph.ts +139 -0
  305. package/src/memory/search/semantic.ts +47 -91
  306. package/src/memory/task-memory-cleanup.ts +28 -50
  307. package/src/messaging/providers/outlook/adapter.ts +8 -1
  308. package/src/messaging/providers/outlook/client.ts +299 -0
  309. package/src/messaging/providers/outlook/types.ts +118 -0
  310. package/src/notifications/adapters/macos.ts +1 -0
  311. package/src/notifications/copy-composer.ts +9 -0
  312. package/src/notifications/signal.ts +16 -0
  313. package/src/oauth/seed-providers.ts +2 -1
  314. package/src/permissions/checker.ts +24 -3
  315. package/src/permissions/defaults.ts +4 -4
  316. package/src/permissions/workspace-policy.ts +1 -1
  317. package/src/playbooks/playbook-compiler.ts +19 -18
  318. package/src/playbooks/types.ts +4 -3
  319. package/src/prompts/system-prompt.ts +3 -29
  320. package/src/providers/anthropic/client.ts +47 -19
  321. package/src/providers/gemini/client.ts +1 -1
  322. package/src/providers/openai/client.ts +1 -1
  323. package/src/providers/registry.ts +1 -1
  324. package/src/providers/retry.ts +19 -3
  325. package/src/runtime/actor-trust-resolver.ts +5 -1
  326. package/src/runtime/auth/route-policy.ts +7 -0
  327. package/src/runtime/guardian-reply-router.ts +5 -1
  328. package/src/runtime/http-server.ts +23 -3
  329. package/src/runtime/middleware/auth.ts +20 -0
  330. package/src/runtime/routes/attachment-routes.test.ts +106 -0
  331. package/src/runtime/routes/attachment-routes.ts +106 -16
  332. package/src/runtime/routes/brain-graph-routes.ts +21 -22
  333. package/src/runtime/routes/btw-routes.ts +8 -0
  334. package/src/runtime/routes/conversation-management-routes.ts +2 -0
  335. package/src/runtime/routes/conversation-starter-routes.ts +2 -2
  336. package/src/runtime/routes/debug-routes.ts +1 -1
  337. package/src/runtime/routes/global-search-routes.ts +21 -19
  338. package/src/runtime/routes/group-routes.ts +207 -0
  339. package/src/runtime/routes/guardian-action-routes.ts +21 -10
  340. package/src/runtime/routes/guardian-bootstrap-routes.ts +23 -19
  341. package/src/runtime/routes/inbound-message-handler.ts +19 -0
  342. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +292 -0
  343. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +207 -0
  344. package/src/runtime/routes/memory-item-routes.test.ts +2 -14
  345. package/src/runtime/routes/memory-item-routes.ts +341 -388
  346. package/src/runtime/routes/schedule-routes.ts +2 -0
  347. package/src/runtime/routes/skills-routes.ts +103 -37
  348. package/src/runtime/routes/work-items-routes.test.ts +2 -6
  349. package/src/schedule/scheduler.ts +8 -1
  350. package/src/security/oauth2.ts +1 -1
  351. package/src/security/secure-keys.ts +4 -8
  352. package/src/shared/provider-env-vars.ts +19 -0
  353. package/src/skills/catalog-cache.ts +5 -0
  354. package/src/skills/catalog-install.ts +15 -14
  355. package/src/skills/clawhub.ts +134 -154
  356. package/src/skills/install-meta.ts +208 -0
  357. package/src/skills/managed-store.ts +27 -16
  358. package/src/skills/skill-memory.ts +152 -77
  359. package/src/skills/skillssh-registry.ts +19 -17
  360. package/src/tasks/task-runner.ts +3 -1
  361. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -5
  362. package/src/tools/browser/runtime-check.ts +3 -1
  363. package/src/tools/memory/register.ts +63 -46
  364. package/src/tools/permission-checker.ts +7 -1
  365. package/src/tools/shared/filesystem/image-read.ts +22 -85
  366. package/src/tools/terminal/safe-env.ts +1 -0
  367. package/src/tools/tool-manifest.ts +3 -3
  368. package/src/util/browser.ts +25 -10
  369. package/src/util/bun-runtime.ts +172 -0
  370. package/src/watcher/providers/outlook-calendar.ts +343 -0
  371. package/src/watcher/providers/outlook.ts +198 -0
  372. package/src/workspace/migrations/025-remove-oauth-app-setup-skills.ts +76 -0
  373. package/src/workspace/migrations/026-backfill-install-meta.ts +325 -0
  374. package/src/workspace/migrations/027-remove-orphaned-optimized-images-cache.ts +42 -0
  375. package/src/workspace/migrations/registry.ts +6 -0
  376. package/src/__tests__/context-memory-e2e.test.ts +0 -415
  377. package/src/__tests__/journal-context.test.ts +0 -268
  378. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -297
  379. package/src/__tests__/memory-lifecycle-e2e.test.ts +0 -459
  380. package/src/__tests__/memory-query-builder.test.ts +0 -59
  381. package/src/__tests__/memory-recall-quality.test.ts +0 -1046
  382. package/src/__tests__/memory-regressions.experimental.test.ts +0 -629
  383. package/src/__tests__/memory-regressions.test.ts +0 -3696
  384. package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -295
  385. package/src/daemon/conversation-memory.ts +0 -207
  386. package/src/memory/conversation-starters-cadence.ts +0 -74
  387. package/src/memory/items-extractor.ts +0 -860
  388. package/src/memory/job-handlers/batch-extraction.ts +0 -753
  389. package/src/memory/job-handlers/extraction.ts +0 -40
  390. package/src/memory/job-handlers/journal-carry-forward.test.ts +0 -355
  391. package/src/memory/job-handlers/journal-carry-forward.ts +0 -255
  392. package/src/memory/journal-memory.ts +0 -224
  393. package/src/memory/query-builder.ts +0 -47
  394. package/src/memory/query-expansion.ts +0 -83
  395. package/src/memory/retriever.test.ts +0 -1592
  396. package/src/memory/retriever.ts +0 -1331
  397. package/src/memory/search/formatting.test.ts +0 -140
  398. package/src/memory/search/formatting.ts +0 -262
  399. package/src/memory/search/mmr.ts +0 -139
  400. package/src/memory/search/ranking.ts +0 -15
  401. package/src/memory/search/staleness.ts +0 -40
  402. package/src/memory/search/tier-classifier.ts +0 -18
  403. package/src/memory/search/types.ts +0 -121
  404. package/src/prompts/journal-context.ts +0 -154
  405. package/src/tools/memory/definitions.ts +0 -69
  406. package/src/tools/memory/handlers.test.ts +0 -562
  407. package/src/tools/memory/handlers.ts +0 -434
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Route handlers for conversation group management.
3
+ *
4
+ * GET /v1/groups — list all groups
5
+ * POST /v1/groups — create a custom group
6
+ * PATCH /v1/groups/:groupId — update a group
7
+ * DELETE /v1/groups/:groupId — delete a group
8
+ * POST /v1/groups/reorder — reorder groups
9
+ */
10
+
11
+ import { z } from "zod";
12
+
13
+ import {
14
+ createGroup,
15
+ deleteGroup,
16
+ getGroup,
17
+ listGroups,
18
+ reorderGroups,
19
+ updateGroup,
20
+ } from "../../memory/group-crud.js";
21
+ import { httpError } from "../http-errors.js";
22
+ import type { RouteDefinition } from "../http-router.js";
23
+
24
+ function serializeGroup(group: ReturnType<typeof getGroup>) {
25
+ if (!group) return null;
26
+ return {
27
+ id: group.id,
28
+ name: group.name,
29
+ sortPosition: group.sortPosition,
30
+ isSystemGroup: group.isSystemGroup,
31
+ };
32
+ }
33
+
34
+ export function groupRouteDefinitions(): RouteDefinition[] {
35
+ return [
36
+ {
37
+ endpoint: "groups",
38
+ method: "GET",
39
+ policyKey: "groups",
40
+ summary: "List groups",
41
+ description: "Return all conversation groups.",
42
+ tags: ["groups"],
43
+ handler: () => {
44
+ const groups = listGroups();
45
+ return Response.json({
46
+ groups: groups.map(serializeGroup),
47
+ });
48
+ },
49
+ },
50
+ {
51
+ endpoint: "groups",
52
+ method: "POST",
53
+ policyKey: "groups",
54
+ summary: "Create group",
55
+ description:
56
+ "Create a new custom conversation group. Server assigns sort_position.",
57
+ tags: ["groups"],
58
+ requestBody: z.object({
59
+ name: z.string().describe("Group name"),
60
+ }),
61
+ handler: async ({ req }) => {
62
+ const body = (await req.json()) as { name?: string };
63
+ if (!body.name || typeof body.name !== "string") {
64
+ return httpError("BAD_REQUEST", "Missing or invalid name", 400);
65
+ }
66
+ const group = createGroup(body.name);
67
+ return Response.json(serializeGroup(group), { status: 201 });
68
+ },
69
+ },
70
+ {
71
+ endpoint: "groups/:groupId",
72
+ method: "PATCH",
73
+ policyKey: "groups",
74
+ summary: "Update group",
75
+ description: "Update a conversation group's name or sort position.",
76
+ tags: ["groups"],
77
+ requestBody: z.object({
78
+ name: z.string().optional(),
79
+ sortPosition: z.number().optional(),
80
+ }),
81
+ handler: async ({ req, params }) => {
82
+ const groupId = params.groupId;
83
+ const existing = getGroup(groupId);
84
+ if (!existing) {
85
+ return httpError("NOT_FOUND", "Group not found", 404);
86
+ }
87
+ const body = (await req.json()) as {
88
+ name?: string;
89
+ sortPosition?: number;
90
+ };
91
+ if (body.name !== undefined && typeof body.name !== "string") {
92
+ return httpError("BAD_REQUEST", "name must be a string", 400);
93
+ }
94
+ if (
95
+ body.sortPosition !== undefined &&
96
+ typeof body.sortPosition !== "number"
97
+ ) {
98
+ return httpError("BAD_REQUEST", "sortPosition must be a number", 400);
99
+ }
100
+ // System groups allow name changes but block sortPosition/delete.
101
+ if (existing.isSystemGroup && body.sortPosition !== undefined) {
102
+ return httpError(
103
+ "FORBIDDEN",
104
+ "System group sort position cannot be changed",
105
+ 403,
106
+ );
107
+ }
108
+ // Custom group sort_position must be >= 3
109
+ if (
110
+ body.sortPosition !== undefined &&
111
+ (typeof body.sortPosition !== "number" ||
112
+ !isFinite(body.sortPosition) ||
113
+ body.sortPosition < 3)
114
+ ) {
115
+ return httpError(
116
+ "BAD_REQUEST",
117
+ "Custom group sort_position must be >= 3",
118
+ 400,
119
+ );
120
+ }
121
+ const updated = updateGroup(groupId, {
122
+ name: body.name,
123
+ sortPosition: body.sortPosition,
124
+ });
125
+ if (!updated) {
126
+ return httpError("NOT_FOUND", "Group not found", 404);
127
+ }
128
+ return Response.json(serializeGroup(updated));
129
+ },
130
+ },
131
+ {
132
+ endpoint: "groups/:groupId",
133
+ method: "DELETE",
134
+ policyKey: "groups",
135
+ summary: "Delete group",
136
+ description: "Delete a custom conversation group.",
137
+ tags: ["groups"],
138
+ handler: ({ params }) => {
139
+ const groupId = params.groupId;
140
+ const existing = getGroup(groupId);
141
+ if (!existing) {
142
+ return httpError("NOT_FOUND", "Group not found", 404);
143
+ }
144
+ // System groups cannot be deleted
145
+ if (existing.isSystemGroup) {
146
+ return httpError("FORBIDDEN", "System groups cannot be deleted", 403);
147
+ }
148
+ deleteGroup(groupId);
149
+ return new Response(null, { status: 204 });
150
+ },
151
+ },
152
+ {
153
+ endpoint: "groups/reorder",
154
+ method: "POST",
155
+ policyKey: "groups/reorder",
156
+ summary: "Reorder groups",
157
+ description: "Batch-update sort positions for conversation groups.",
158
+ tags: ["groups"],
159
+ requestBody: z.object({
160
+ updates: z
161
+ .array(
162
+ z.object({
163
+ groupId: z.string(),
164
+ sortPosition: z.number(),
165
+ }),
166
+ )
167
+ .describe("Array of { groupId, sortPosition } objects"),
168
+ }),
169
+ handler: async ({ req }) => {
170
+ const body = (await req.json()) as {
171
+ updates?: Array<{
172
+ groupId: string;
173
+ sortPosition: number;
174
+ }>;
175
+ };
176
+ if (!Array.isArray(body.updates)) {
177
+ return httpError("BAD_REQUEST", "Missing updates array", 400);
178
+ }
179
+ // Validate: no system group reordering, no sort_position < 3 for custom groups
180
+ for (const update of body.updates) {
181
+ const group = getGroup(update.groupId);
182
+ if (!group) continue;
183
+ if (group.isSystemGroup) {
184
+ return httpError(
185
+ "FORBIDDEN",
186
+ `Cannot reorder system group: ${update.groupId}`,
187
+ 403,
188
+ );
189
+ }
190
+ if (
191
+ typeof update.sortPosition !== "number" ||
192
+ !isFinite(update.sortPosition) ||
193
+ update.sortPosition < 3
194
+ ) {
195
+ return httpError(
196
+ "BAD_REQUEST",
197
+ `Custom group sort_position must be >= 3 (got ${update.sortPosition} for ${update.groupId})`,
198
+ 400,
199
+ );
200
+ }
201
+ }
202
+ reorderGroups(body.updates);
203
+ return Response.json({ ok: true });
204
+ },
205
+ },
206
+ ];
207
+ }
@@ -23,7 +23,10 @@ import { requireBoundGuardian } from "../auth/require-bound-guardian.js";
23
23
  import type { AuthContext } from "../auth/types.js";
24
24
  import { processGuardianDecision } from "../guardian-action-service.js";
25
25
  import type { GuardianDecisionPrompt } from "../guardian-decision-types.js";
26
- import { buildDecisionActions } from "../guardian-decision-types.js";
26
+ import {
27
+ buildDecisionActions,
28
+ GUARDIAN_DECISION_ACTIONS,
29
+ } from "../guardian-decision-types.js";
27
30
  import { httpError } from "../http-errors.js";
28
31
  import type { RouteDefinition } from "../http-router.js";
29
32
 
@@ -190,13 +193,15 @@ export function listGuardianDecisionPrompts(params: {
190
193
  * Map a canonical guardian request to the client-facing prompt format.
191
194
  *
192
195
  * Generates kind-specific questionText and action sets:
193
- * - `tool_approval`: "Approve tool: <name>" with approve/reject actions
194
- * - `pending_question`: voice-originated question with approve/reject actions
195
- * - `access_request`: explicit "Access Request" label with approve/reject actions
196
- * and text fallback instructions (request code + "open invite flow")
196
+ * - `tool_approval`: temporal modes (approve_once, approve_10m, approve_conversation) + reject
197
+ * - `pending_question`: approve_once + reject only
198
+ * - `access_request`: approve_once + reject only, with text fallback instructions
199
+ * (request code + "open invite flow")
200
+ * - `tool_grant_request`: approve_once + reject only
197
201
  *
198
- * All kinds use `forGuardianOnBehalf: true` (no approve_always) since the
199
- * guardian is acting on behalf of a requester.
202
+ * Only `tool_approval` receives temporal modes because time-scoped grants
203
+ * are meaningful only for tool execution. All other kinds get a simple
204
+ * approve_once/reject pair.
200
205
  */
201
206
  function mapCanonicalRequestToPrompt(
202
207
  req: CanonicalGuardianRequest,
@@ -204,9 +209,15 @@ function mapCanonicalRequestToPrompt(
204
209
  ): GuardianDecisionPrompt {
205
210
  const questionText = buildKindAwareQuestionText(req);
206
211
 
207
- // Guardian-on-behalf prompts include approve_once, temporal modes
208
- // (approve_10m, approve_conversation), and reject but not approve_always.
209
- const actions = buildDecisionActions({ forGuardianOnBehalf: true });
212
+ // Only tool_approval gets temporal modes (approve_10m, approve_conversation);
213
+ // all other kinds get a simple approve_once + reject pair.
214
+ const actions =
215
+ req.kind === "tool_approval"
216
+ ? buildDecisionActions({ forGuardianOnBehalf: true })
217
+ : [
218
+ GUARDIAN_DECISION_ACTIONS.approve_once,
219
+ GUARDIAN_DECISION_ACTIONS.reject,
220
+ ];
210
221
 
211
222
  const expiresAt = req.expiresAt
212
223
  ? new Date(req.expiresAt).getTime()
@@ -19,7 +19,7 @@ import { getLogger } from "../../util/logger.js";
19
19
  import { DAEMON_INTERNAL_ASSISTANT_ID } from "../assistant-scope.js";
20
20
  import { mintCredentialPair } from "../auth/credential-service.js";
21
21
  import { httpError } from "../http-errors.js";
22
- import { isPrivateAddress } from "../middleware/auth.js";
22
+ import { isLoopbackAddress, isPrivateAddress } from "../middleware/auth.js";
23
23
 
24
24
  /** Bun server shape needed for requestIP -- avoids importing the full Bun type. */
25
25
  type ServerWithRequestIP = {
@@ -87,30 +87,34 @@ export async function handleGuardianBootstrap(
87
87
  req: Request,
88
88
  server: ServerWithRequestIP,
89
89
  ): Promise<Response> {
90
- // Reject non-private-network peers (allows loopback, Docker bridge, etc.)
90
+ // In non-containerized (bare-metal) mode, restrict to loopback only
91
+ // the runtime binds to localhost and LAN peers should not be able to
92
+ // bootstrap even if they somehow reach the endpoint.
93
+ // In containerized (Docker) mode, accept any private-network peer because
94
+ // the gateway connects over the Docker bridge (e.g. 172.17.0.1) and the
95
+ // GUARDIAN_BOOTSTRAP_SECRET enforced at the gateway layer provides the
96
+ // real authentication.
91
97
  const peerIp = server.requestIP(req)?.address;
92
- if ((!peerIp || !isPrivateAddress(peerIp)) && !isHttpAuthDisabled()) {
98
+ const containerized = getIsContainerized();
99
+ const peerAllowed = containerized
100
+ ? isPrivateAddress(peerIp ?? "")
101
+ : isLoopbackAddress(peerIp ?? "");
102
+ if (!peerAllowed && !isHttpAuthDisabled()) {
93
103
  return httpError("FORBIDDEN", "Bootstrap endpoint is local-only", 403);
94
104
  }
95
105
 
96
- // Reject requests forwarded from public networks. The gateway sets
97
- // x-forwarded-for to the real client IP; if that IP is on a private
98
- // network (loopback, Docker bridge, RFC 1918) the request is still
99
- // considered local. Only reject when the forwarded IP is public.
106
+ // In non-containerized mode, any x-forwarded-for header means the request
107
+ // came through the gateway from a non-loopback client. Legitimate bare-metal
108
+ // bootstrap clients connect from localhost; the gateway does not inject
109
+ // x-forwarded-for for loopback peers (see gateway/src/index.ts). Reject
110
+ // forwarded requests to prevent LAN-adjacent clients from bootstrapping.
100
111
  //
101
- // Skip this check when running in a container: the peer IP was already
102
- // validated above (Docker bridge network = private), so the request
103
- // reached us through a co-located gateway. The x-forwarded-for header
104
- // reflects the original external client (e.g. platform proxy) and is
105
- // not meaningful for local-only enforcement in this topology.
112
+ // In containerized mode, skip this check: the peer IP was already validated
113
+ // above (Docker bridge network = private), and the x-forwarded-for header
114
+ // reflects the original external client which is not meaningful for
115
+ // local-only enforcement in this topology.
106
116
  const forwarded = req.headers.get("x-forwarded-for");
107
- const forwardedIp = forwarded ? forwarded.split(",")[0].trim() : null;
108
- if (
109
- forwardedIp &&
110
- !isPrivateAddress(forwardedIp) &&
111
- !isHttpAuthDisabled() &&
112
- !getIsContainerized()
113
- ) {
117
+ if (forwarded && !isHttpAuthDisabled() && !getIsContainerized()) {
114
118
  return httpError("FORBIDDEN", "Bootstrap endpoint is local-only", 403);
115
119
  }
116
120
 
@@ -44,6 +44,7 @@ import { processChannelMessageInBackground } from "./inbound-stages/background-d
44
44
  import { handleBootstrapIntercept } from "./inbound-stages/bootstrap-intercept.js";
45
45
  import { handleEditIntercept } from "./inbound-stages/edit-intercept.js";
46
46
  import { handleEscalationIntercept } from "./inbound-stages/escalation-intercept.js";
47
+ import { handleGuardianActivationIntercept } from "./inbound-stages/guardian-activation-intercept.js";
47
48
  import { handleGuardianReplyIntercept } from "./inbound-stages/guardian-reply-intercept.js";
48
49
  import { runSecretIngressCheck } from "./inbound-stages/secret-ingress-check.js";
49
50
  import { tryTranscribeAudioAttachments } from "./inbound-stages/transcribe-audio.js";
@@ -199,6 +200,24 @@ export async function handleChannelInbound(
199
200
  // ACL deny path rather than bypassing it.
200
201
  const hasSenderIdentityClaim = rawSenderId !== undefined;
201
202
 
203
+ // ── Guardian channel activation ──
204
+ // When a bare /start arrives on a channel with no guardian, auto-initiate
205
+ // guardian verification so the first user can claim the channel.
206
+ const guardianActivationResponse = await handleGuardianActivationIntercept({
207
+ sourceChannel,
208
+ conversationExternalId,
209
+ rawSenderId,
210
+ canonicalSenderId,
211
+ actorDisplayName: body.actorDisplayName,
212
+ actorUsername: body.actorUsername,
213
+ sourceMetadata: body.sourceMetadata,
214
+ replyCallbackUrl: body.replyCallbackUrl,
215
+ mintBearerToken,
216
+ assistantId,
217
+ externalMessageId,
218
+ });
219
+ if (guardianActivationResponse) return guardianActivationResponse;
220
+
202
221
  // ── Ingress ACL enforcement ──
203
222
  const aclResult = await enforceIngressAcl({
204
223
  canonicalSenderId,
@@ -0,0 +1,292 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Mocks — must be set up before importing the module under test
5
+ // ---------------------------------------------------------------------------
6
+
7
+ let mockGuardian: { contact: unknown; channel: unknown } | null = null;
8
+ let mockActiveSession: Record<string, unknown> | null = null;
9
+ let mockSessionResult = {
10
+ sessionId: "sess-1",
11
+ secret: "123456",
12
+ challengeHash: "hash-1",
13
+ expiresAt: Date.now() + 600_000,
14
+ ttlSeconds: 600,
15
+ };
16
+
17
+ // Track calls manually to avoid TypeScript issues with mock() generics
18
+ let createOutboundSessionCalls: unknown[] = [];
19
+ let deliverChannelReplyCalls: unknown[][] = [];
20
+ let emitNotificationSignalCalls: unknown[] = [];
21
+ let messageIdCounter = 0;
22
+
23
+ mock.module("../../../contacts/contact-store.js", () => ({
24
+ findGuardianForChannel: () => mockGuardian,
25
+ }));
26
+
27
+ mock.module("../../channel-verification-service.js", () => ({
28
+ createOutboundSession: (params: unknown) => {
29
+ createOutboundSessionCalls.push(params);
30
+ return mockSessionResult;
31
+ },
32
+ findActiveSession: () => mockActiveSession,
33
+ }));
34
+
35
+ mock.module("../../gateway-client.js", () => ({
36
+ deliverChannelReply: (url: unknown, payload: unknown, token: unknown) => {
37
+ deliverChannelReplyCalls.push([url, payload, token]);
38
+ return Promise.resolve({ ok: true });
39
+ },
40
+ }));
41
+
42
+ mock.module("../../../notifications/emit-signal.js", () => ({
43
+ emitNotificationSignal: (params: unknown) => {
44
+ emitNotificationSignalCalls.push(params);
45
+ return Promise.resolve({
46
+ signalId: "sig-1",
47
+ deduplicated: false,
48
+ dispatched: true,
49
+ reason: "ok",
50
+ deliveryResults: [],
51
+ });
52
+ },
53
+ }));
54
+
55
+ mock.module("../../../util/logger.js", () => ({
56
+ getLogger: () => ({
57
+ debug: () => {},
58
+ info: () => {},
59
+ warn: () => {},
60
+ error: () => {},
61
+ }),
62
+ }));
63
+
64
+ // Import after mocks are installed
65
+ const { handleGuardianActivationIntercept } =
66
+ await import("./guardian-activation-intercept.js");
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // Helpers
70
+ // ---------------------------------------------------------------------------
71
+
72
+ function makeParams(
73
+ overrides: Partial<
74
+ Parameters<typeof handleGuardianActivationIntercept>[0]
75
+ > = {},
76
+ ) {
77
+ messageIdCounter++;
78
+ return {
79
+ sourceChannel: "telegram" as const,
80
+ conversationExternalId: "chat-123",
81
+ rawSenderId: "user-42",
82
+ canonicalSenderId: "user-42",
83
+ actorDisplayName: "Alice",
84
+ actorUsername: "alice",
85
+ sourceMetadata: { commandIntent: { type: "start" } },
86
+ replyCallbackUrl: "https://gateway/reply",
87
+ mintBearerToken: () => "token-123",
88
+ assistantId: "self",
89
+ externalMessageId: `msg-${messageIdCounter}`,
90
+ ...overrides,
91
+ };
92
+ }
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // Tests
96
+ // ---------------------------------------------------------------------------
97
+
98
+ describe("handleGuardianActivationIntercept", () => {
99
+ beforeEach(() => {
100
+ mockGuardian = null;
101
+ mockActiveSession = null;
102
+ mockSessionResult = {
103
+ sessionId: "sess-1",
104
+ secret: "123456",
105
+ challengeHash: "hash-1",
106
+ expiresAt: Date.now() + 600_000,
107
+ ttlSeconds: 600,
108
+ };
109
+ createOutboundSessionCalls = [];
110
+ deliverChannelReplyCalls = [];
111
+ emitNotificationSignalCalls = [];
112
+ });
113
+
114
+ afterEach(() => {
115
+ createOutboundSessionCalls = [];
116
+ deliverChannelReplyCalls = [];
117
+ emitNotificationSignalCalls = [];
118
+ });
119
+
120
+ test("bare /start with no guardian creates session and returns early", async () => {
121
+ const result = await handleGuardianActivationIntercept(makeParams());
122
+
123
+ expect(result).not.toBeNull();
124
+ const body = await result!.json();
125
+ expect(body).toEqual({ accepted: true, guardianActivation: true });
126
+
127
+ // Verify createOutboundSession was called with correct params
128
+ expect(createOutboundSessionCalls).toHaveLength(1);
129
+ expect(createOutboundSessionCalls[0]).toEqual({
130
+ channel: "telegram",
131
+ expectedExternalUserId: "user-42",
132
+ expectedChatId: "chat-123",
133
+ identityBindingStatus: "bound",
134
+ destinationAddress: "chat-123",
135
+ verificationPurpose: "guardian",
136
+ });
137
+
138
+ // Verify deliverChannelReply was called with the welcome/verify message
139
+ expect(deliverChannelReplyCalls).toHaveLength(1);
140
+ expect(deliverChannelReplyCalls[0][0]).toBe("https://gateway/reply");
141
+ expect(deliverChannelReplyCalls[0][1]).toEqual({
142
+ chatId: "chat-123",
143
+ text: "Welcome! To verify your identity as guardian, check your assistant app for a verification code and enter it here.",
144
+ assistantId: "self",
145
+ });
146
+
147
+ // Verify emitNotificationSignal was called with guardian.channel_activation
148
+ expect(emitNotificationSignalCalls).toHaveLength(1);
149
+ const signalArgs = emitNotificationSignalCalls[0] as Record<string, any>;
150
+ expect(signalArgs.sourceEventName).toBe("guardian.channel_activation");
151
+ expect(signalArgs.contextPayload.verificationCode).toBe("123456");
152
+ expect(signalArgs.contextPayload.sourceChannel).toBe("telegram");
153
+ expect(signalArgs.contextPayload.actorExternalId).toBe("user-42");
154
+ expect(signalArgs.contextPayload.sessionId).toBe("sess-1");
155
+ expect(signalArgs.dedupeKey).toBe("guardian-activation:sess-1");
156
+ });
157
+
158
+ test("bare /start with existing guardian returns null", async () => {
159
+ mockGuardian = {
160
+ contact: { id: "contact-1", role: "guardian" },
161
+ channel: { id: "ch-1", type: "telegram" },
162
+ };
163
+
164
+ const result = await handleGuardianActivationIntercept(makeParams());
165
+ expect(result).toBeNull();
166
+ expect(createOutboundSessionCalls).toHaveLength(0);
167
+ });
168
+
169
+ test("/start with payload returns null", async () => {
170
+ const result = await handleGuardianActivationIntercept(
171
+ makeParams({
172
+ sourceMetadata: {
173
+ commandIntent: { type: "start", payload: "gv_token" },
174
+ },
175
+ }),
176
+ );
177
+ expect(result).toBeNull();
178
+ expect(createOutboundSessionCalls).toHaveLength(0);
179
+ });
180
+
181
+ test("non-/start message returns null", async () => {
182
+ const result = await handleGuardianActivationIntercept(
183
+ makeParams({
184
+ sourceMetadata: { commandIntent: { type: "other" } },
185
+ }),
186
+ );
187
+ expect(result).toBeNull();
188
+ expect(createOutboundSessionCalls).toHaveLength(0);
189
+ });
190
+
191
+ test("no commandIntent returns null", async () => {
192
+ const result = await handleGuardianActivationIntercept(
193
+ makeParams({ sourceMetadata: {} }),
194
+ );
195
+ expect(result).toBeNull();
196
+
197
+ const result2 = await handleGuardianActivationIntercept(
198
+ makeParams({ sourceMetadata: undefined }),
199
+ );
200
+ expect(result2).toBeNull();
201
+ expect(createOutboundSessionCalls).toHaveLength(0);
202
+ });
203
+
204
+ test("non-telegram channel returns null", async () => {
205
+ const result = await handleGuardianActivationIntercept(
206
+ makeParams({ sourceChannel: "slack" as any }),
207
+ );
208
+ expect(result).toBeNull();
209
+ expect(createOutboundSessionCalls).toHaveLength(0);
210
+ });
211
+
212
+ test("missing sender ID returns null", async () => {
213
+ const result = await handleGuardianActivationIntercept(
214
+ makeParams({ rawSenderId: undefined }),
215
+ );
216
+ expect(result).toBeNull();
217
+ expect(createOutboundSessionCalls).toHaveLength(0);
218
+ });
219
+
220
+ test("existing active session from same sender sends 'already in progress' reply", async () => {
221
+ mockActiveSession = {
222
+ id: "existing-sess",
223
+ channel: "telegram",
224
+ status: "awaiting_response",
225
+ expectedExternalUserId: "user-42",
226
+ expectedChatId: "chat-123",
227
+ };
228
+
229
+ const result = await handleGuardianActivationIntercept(makeParams());
230
+
231
+ expect(result).not.toBeNull();
232
+ const body = await result!.json();
233
+ expect(body).toEqual({ accepted: true, guardianActivationPending: true });
234
+
235
+ // createOutboundSession should NOT be called
236
+ expect(createOutboundSessionCalls).toHaveLength(0);
237
+
238
+ // deliverChannelReply should be called with the "already in progress" message
239
+ expect(deliverChannelReplyCalls).toHaveLength(1);
240
+ expect(deliverChannelReplyCalls[0][1]).toEqual({
241
+ chatId: "chat-123",
242
+ text: "A verification is already in progress. Check your assistant app for the code and enter it here.",
243
+ assistantId: "self",
244
+ });
245
+
246
+ // emitNotificationSignal should NOT be called
247
+ expect(emitNotificationSignalCalls).toHaveLength(0);
248
+ });
249
+
250
+ test("existing active session from different sender allows superseding", async () => {
251
+ mockActiveSession = {
252
+ id: "existing-sess",
253
+ channel: "telegram",
254
+ status: "awaiting_response",
255
+ expectedExternalUserId: "user-OTHER",
256
+ expectedChatId: "chat-OTHER",
257
+ };
258
+
259
+ const result = await handleGuardianActivationIntercept(makeParams());
260
+
261
+ // Should proceed and create a new session (superseding the stale one)
262
+ expect(result).not.toBeNull();
263
+ const body = await result!.json();
264
+ expect(body).toEqual({ accepted: true, guardianActivation: true });
265
+ expect(createOutboundSessionCalls).toHaveLength(1);
266
+ expect(emitNotificationSignalCalls).toHaveLength(1);
267
+ });
268
+
269
+ test("duplicate webhook retry is silently deduped", async () => {
270
+ const params = makeParams({ externalMessageId: "dedup-test-msg" });
271
+
272
+ // First call should process normally
273
+ const result1 = await handleGuardianActivationIntercept(params);
274
+ expect(result1).not.toBeNull();
275
+ const body1 = await result1!.json();
276
+ expect(body1).toEqual({ accepted: true, guardianActivation: true });
277
+ expect(createOutboundSessionCalls).toHaveLength(1);
278
+ expect(deliverChannelReplyCalls).toHaveLength(1);
279
+ expect(emitNotificationSignalCalls).toHaveLength(1);
280
+
281
+ // Second call with same externalMessageId should be deduped
282
+ const result2 = await handleGuardianActivationIntercept(params);
283
+ expect(result2).not.toBeNull();
284
+ const body2 = await result2!.json();
285
+ expect(body2).toEqual({ accepted: true, guardianActivation: true });
286
+
287
+ // No additional session/reply/signal calls
288
+ expect(createOutboundSessionCalls).toHaveLength(1);
289
+ expect(deliverChannelReplyCalls).toHaveLength(1);
290
+ expect(emitNotificationSignalCalls).toHaveLength(1);
291
+ });
292
+ });