@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
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test";
2
2
 
3
3
  import {
4
4
  buildTemporalContext,
5
- extractUserTimeZoneFromDynamicProfile,
5
+ extractUserTimeZoneFromRecall,
6
6
  } from "../daemon/date-context.js";
7
7
 
8
8
  // Fixed timestamps for deterministic assertions (all UTC midday to avoid DST edge cases).
@@ -152,82 +152,6 @@ describe("buildTemporalContext", () => {
152
152
  });
153
153
  });
154
154
 
155
- describe("extractUserTimeZoneFromDynamicProfile", () => {
156
- test("extracts canonical timezone from explicit timezone profile line", () => {
157
- const profile = [
158
- "<dynamic-user-profile>",
159
- "- timezone: Timezone is America/New_York.",
160
- "</dynamic-user-profile>",
161
- ].join("\n");
162
- expect(extractUserTimeZoneFromDynamicProfile(profile)).toBe(
163
- "America/New_York",
164
- );
165
- });
166
-
167
- test("extracts timezone token from generic profile text when explicit line is absent", () => {
168
- const profile = [
169
- "<dynamic-user-profile>",
170
- "- location: Travels often between Europe and Asia (currently Europe/Paris).",
171
- "</dynamic-user-profile>",
172
- ].join("\n");
173
- expect(extractUserTimeZoneFromDynamicProfile(profile)).toBe("Europe/Paris");
174
- });
175
-
176
- test("returns null when no valid timezone is present", () => {
177
- const profile = [
178
- "<dynamic-user-profile>",
179
- "- timezone: Pacific time",
180
- "</dynamic-user-profile>",
181
- ].join("\n");
182
- expect(extractUserTimeZoneFromDynamicProfile(profile)).toBeNull();
183
- });
184
-
185
- test("extracts UTC/GMT offset tokens from explicit timezone profile line", () => {
186
- const profile = [
187
- "<dynamic-user-profile>",
188
- "- timezone: UTC+2",
189
- "</dynamic-user-profile>",
190
- ].join("\n");
191
- expect(extractUserTimeZoneFromDynamicProfile(profile)).toBe("Etc/GMT-2");
192
- });
193
-
194
- test("extracts GMT negative offset tokens from generic profile text", () => {
195
- const profile = [
196
- "<dynamic-user-profile>",
197
- "- preferences: schedule notifications in GMT-5 whenever possible.",
198
- "</dynamic-user-profile>",
199
- ].join("\n");
200
- expect(extractUserTimeZoneFromDynamicProfile(profile)).toBe("Etc/GMT+5");
201
- });
202
-
203
- test("extracts fractional UTC offset tokens from explicit timezone profile line", () => {
204
- const profile = [
205
- "<dynamic-user-profile>",
206
- "- timezone: UTC+5:30",
207
- "</dynamic-user-profile>",
208
- ].join("\n");
209
- expect(extractUserTimeZoneFromDynamicProfile(profile)).toBe("+05:30");
210
- });
211
-
212
- test("extracts fractional GMT offset tokens from generic profile text", () => {
213
- const profile = [
214
- "<dynamic-user-profile>",
215
- "- preferences: default reminders to GMT+5:45.",
216
- "</dynamic-user-profile>",
217
- ].join("\n");
218
- expect(extractUserTimeZoneFromDynamicProfile(profile)).toBe("+05:45");
219
- });
220
-
221
- test("prefers IANA timezone tokens over UTC/GMT offsets in the same profile line", () => {
222
- const profile = [
223
- "<dynamic-user-profile>",
224
- "- timezone: UTC+1 (Europe/Paris)",
225
- "</dynamic-user-profile>",
226
- ].join("\n");
227
- expect(extractUserTimeZoneFromDynamicProfile(profile)).toBe("Europe/Paris");
228
- });
229
- });
230
-
231
155
  // ---------------------------------------------------------------------------
232
156
  // Weekday baseline — today is Wednesday
233
157
  // ---------------------------------------------------------------------------
@@ -615,3 +539,95 @@ describe("trip-planning: timezone-shifted weekend anchors", () => {
615
539
  expect(result).toContain("Next weekend: 2026-02-28 – 2026-03-01");
616
540
  });
617
541
  });
542
+
543
+ // ---------------------------------------------------------------------------
544
+ // extractUserTimeZoneFromRecall
545
+ // ---------------------------------------------------------------------------
546
+
547
+ describe("extractUserTimeZoneFromRecall", () => {
548
+ test("returns null for empty input", () => {
549
+ expect(extractUserTimeZoneFromRecall("")).toBeNull();
550
+ expect(extractUserTimeZoneFromRecall(" ")).toBeNull();
551
+ });
552
+
553
+ test("extracts IANA timezone from user_identity section", () => {
554
+ const text = `<memory_context>
555
+
556
+ <user_identity>
557
+ User's timezone is America/New_York
558
+ User works as a software engineer
559
+ </user_identity>
560
+
561
+ </memory_context>`;
562
+ expect(extractUserTimeZoneFromRecall(text)).toBe("America/New_York");
563
+ });
564
+
565
+ test("extracts timezone from 'timezone: ...' line in identity", () => {
566
+ const text = `<memory_context>
567
+
568
+ <user_identity>
569
+ - name: Alice
570
+ - timezone: Europe/London
571
+ - role: designer
572
+ </user_identity>
573
+
574
+ </memory_context>`;
575
+ expect(extractUserTimeZoneFromRecall(text)).toBe("Europe/London");
576
+ });
577
+
578
+ test("extracts UTC offset timezone", () => {
579
+ const text = `<memory_context>
580
+
581
+ <user_identity>
582
+ User's time zone is UTC+5:30
583
+ </user_identity>
584
+
585
+ </memory_context>`;
586
+ const result = extractUserTimeZoneFromRecall(text);
587
+ expect(result).not.toBeNull();
588
+ // UTC+5:30 should canonicalize to +05:30
589
+ expect(result).toBe("+05:30");
590
+ });
591
+
592
+ test("falls back to scanning full text when no identity section", () => {
593
+ const text = `<memory_context>
594
+
595
+ <relevant_context>
596
+ <episode source="Mar 5">
597
+ User mentioned their timezone is Asia/Tokyo
598
+ </episode>
599
+ </relevant_context>
600
+
601
+ </memory_context>`;
602
+ expect(extractUserTimeZoneFromRecall(text)).toBe("Asia/Tokyo");
603
+ });
604
+
605
+ test("returns null when no timezone info present", () => {
606
+ const text = `<memory_context>
607
+
608
+ <user_identity>
609
+ User's name is Bob
610
+ User works at Acme Corp
611
+ </user_identity>
612
+
613
+ </memory_context>`;
614
+ expect(extractUserTimeZoneFromRecall(text)).toBeNull();
615
+ });
616
+
617
+ test("prefers identity section over other sections", () => {
618
+ const text = `<memory_context>
619
+
620
+ <user_identity>
621
+ User's timezone is America/Chicago
622
+ </user_identity>
623
+
624
+ <relevant_context>
625
+ <episode source="Mar 5">
626
+ Discussed timezone America/Los_Angeles for the deployment
627
+ </episode>
628
+ </relevant_context>
629
+
630
+ </memory_context>`;
631
+ expect(extractUserTimeZoneFromRecall(text)).toBe("America/Chicago");
632
+ });
633
+ });
@@ -399,4 +399,68 @@ describe("Verification control messages are deterministic (guard)", () => {
399
399
  globalThis.fetch = originalFetch;
400
400
  }
401
401
  });
402
+
403
+ test("handleChannelInbound does not allow blocked members to bootstrap with /start gv_<token>", async () => {
404
+ const { createHash, randomBytes } = await import("node:crypto");
405
+ const { handleChannelInbound } =
406
+ await import("../runtime/routes/inbound-message-handler.js");
407
+ const { createOutboundSession } =
408
+ await import("../runtime/channel-verification-service.js");
409
+ const { upsertContactChannel } =
410
+ await import("../contacts/contacts-write.js");
411
+
412
+ const blockedIdentity = {
413
+ sourceChannel: "telegram",
414
+ externalUserId: "user-blocked-bootstrap",
415
+ externalChatId: "chat-blocked-bootstrap",
416
+ displayName: "Blocked Bootstrap User",
417
+ status: "blocked",
418
+ policy: "deny",
419
+ } as const;
420
+ upsertContactChannel(blockedIdentity);
421
+
422
+ const bootstrapToken = randomBytes(16).toString("hex");
423
+ const bootstrapTokenHash = createHash("sha256")
424
+ .update(bootstrapToken)
425
+ .digest("hex");
426
+
427
+ createOutboundSession({
428
+ channel: "telegram",
429
+ identityBindingStatus: "pending_bootstrap",
430
+ destinationAddress: blockedIdentity.externalUserId,
431
+ bootstrapTokenHash,
432
+ });
433
+
434
+ let processMessageCalled = false;
435
+ const processMessage = async () => {
436
+ processMessageCalled = true;
437
+ return { messageId: "msg-1" };
438
+ };
439
+
440
+ const req = new Request("http://localhost/channels/inbound", {
441
+ method: "POST",
442
+ headers: { "Content-Type": "application/json" },
443
+ body: JSON.stringify({
444
+ sourceChannel: "telegram",
445
+ interface: "telegram",
446
+ conversationExternalId: blockedIdentity.externalChatId,
447
+ externalMessageId: `msg-blocked-bootstrap-${Date.now()}`,
448
+ content: `/start gv_${bootstrapToken}`,
449
+ actorExternalId: blockedIdentity.externalUserId,
450
+ actorDisplayName: blockedIdentity.displayName,
451
+ sourceMetadata: {
452
+ commandIntent: { type: "start", payload: `gv_${bootstrapToken}` },
453
+ },
454
+ }),
455
+ });
456
+
457
+ const response = await handleChannelInbound(req, processMessage);
458
+ const body = (await response.json()) as Record<string, unknown>;
459
+
460
+ expect(body.accepted).toBe(true);
461
+ expect(body.denied).toBe(true);
462
+ expect(body.reason).toBe("member_blocked");
463
+ expect(body.verificationOutcome).toBeUndefined();
464
+ expect(processMessageCalled).toBe(false);
465
+ });
402
466
  });
@@ -1427,3 +1427,96 @@ describe("routing invariant: invite handoff bypass for access requests", () => {
1427
1427
  expect(resolved!.status).toBe("approved");
1428
1428
  });
1429
1429
  });
1430
+
1431
+ // ===========================================================================
1432
+ // SECTION 11: Expired requests are excluded from routing
1433
+ // ===========================================================================
1434
+
1435
+ describe("routing invariant: expired requests are excluded from pending discovery", () => {
1436
+ beforeEach(() => resetTables());
1437
+
1438
+ test("expired request with hinted IDs is excluded from disambiguation", async () => {
1439
+ const expired = createCanonicalGuardianRequest({
1440
+ kind: "tool_approval",
1441
+ sourceType: "channel",
1442
+ conversationId: "conv-1",
1443
+ guardianExternalUserId: "guardian-1",
1444
+ guardianPrincipalId: TEST_PRINCIPAL_ID,
1445
+ requestCode: "EXP001",
1446
+ toolName: "shell",
1447
+ expiresAt: new Date(Date.now() - 10_000).toISOString(),
1448
+ });
1449
+
1450
+ const active = createCanonicalGuardianRequest({
1451
+ kind: "tool_approval",
1452
+ sourceType: "channel",
1453
+ conversationId: "conv-1",
1454
+ guardianExternalUserId: "guardian-1",
1455
+ guardianPrincipalId: TEST_PRINCIPAL_ID,
1456
+ requestCode: "ACT001",
1457
+ toolName: "file_write",
1458
+ expiresAt: new Date(Date.now() + 60_000).toISOString(),
1459
+ });
1460
+ registerPendingToolApprovalInteraction(active.id, "conv-1", "file_write");
1461
+
1462
+ // Both IDs are hinted but only the active one should be considered
1463
+ const result = await routeGuardianReply(
1464
+ replyCtx({
1465
+ messageText: "approve",
1466
+ conversationId: "conv-guardian-thread",
1467
+ pendingRequestIds: [expired.id, active.id],
1468
+ approvalConversationGenerator: undefined,
1469
+ }),
1470
+ );
1471
+
1472
+ // Single active request — should apply directly, no disambiguation
1473
+ expect(result.consumed).toBe(true);
1474
+ expect(result.type).toBe("canonical_decision_applied");
1475
+ expect(result.decisionApplied).toBe(true);
1476
+
1477
+ const resolvedActive = getCanonicalGuardianRequest(active.id);
1478
+ expect(resolvedActive!.status).toBe("approved");
1479
+
1480
+ // Expired request untouched
1481
+ const resolvedExpired = getCanonicalGuardianRequest(expired.id);
1482
+ expect(resolvedExpired!.status).toBe("pending");
1483
+ });
1484
+
1485
+ test("all expired hinted requests means no pending found — not consumed", async () => {
1486
+ const expired1 = createCanonicalGuardianRequest({
1487
+ kind: "tool_approval",
1488
+ sourceType: "channel",
1489
+ conversationId: "conv-1",
1490
+ guardianExternalUserId: "guardian-1",
1491
+ guardianPrincipalId: TEST_PRINCIPAL_ID,
1492
+ requestCode: "EXP002",
1493
+ toolName: "shell",
1494
+ expiresAt: new Date(Date.now() - 10_000).toISOString(),
1495
+ });
1496
+
1497
+ const expired2 = createCanonicalGuardianRequest({
1498
+ kind: "tool_approval",
1499
+ sourceType: "channel",
1500
+ conversationId: "conv-1",
1501
+ guardianExternalUserId: "guardian-1",
1502
+ guardianPrincipalId: TEST_PRINCIPAL_ID,
1503
+ requestCode: "EXP003",
1504
+ toolName: "file_write",
1505
+ expiresAt: new Date(Date.now() - 5_000).toISOString(),
1506
+ });
1507
+
1508
+ const result = await routeGuardianReply(
1509
+ replyCtx({
1510
+ messageText: "approve",
1511
+ conversationId: "conv-guardian-thread",
1512
+ pendingRequestIds: [expired1.id, expired2.id],
1513
+ approvalConversationGenerator: undefined,
1514
+ }),
1515
+ );
1516
+
1517
+ // No active pending requests — falls through
1518
+ expect(result.consumed).toBe(false);
1519
+ expect(result.type).toBe("not_consumed");
1520
+ expect(result.decisionApplied).toBe(false);
1521
+ });
1522
+ });
@@ -428,6 +428,251 @@ describe("repairHistory", () => {
428
428
  expect(stats.orphanToolResultsDowngraded).toBe(0);
429
429
  expect(stats.consecutiveSameRoleMerged).toBe(0);
430
430
  });
431
+
432
+ test("keeps server_tool_use + web_search_tool_result paired in assistant message", () => {
433
+ // Both blocks should stay in the assistant message (self-paired)
434
+ const messages: Message[] = [
435
+ { role: "user", content: [{ type: "text", text: "Search for cats" }] },
436
+ {
437
+ role: "assistant",
438
+ content: [
439
+ {
440
+ type: "server_tool_use",
441
+ id: "stu_1",
442
+ name: "web_search",
443
+ input: { query: "cats" },
444
+ },
445
+ {
446
+ type: "web_search_tool_result",
447
+ tool_use_id: "stu_1",
448
+ content: [{ type: "web_search_result", url: "https://cats.com" }],
449
+ },
450
+ { type: "text", text: "Here are some results about cats." },
451
+ ],
452
+ },
453
+ ];
454
+
455
+ const { messages: repaired, stats } = repairHistory(messages);
456
+
457
+ expect(repaired).toEqual(messages);
458
+ expect(stats.missingToolResultsInserted).toBe(0);
459
+ expect(stats.orphanToolResultsDowngraded).toBe(0);
460
+ });
461
+
462
+ test("synthesizes web_search_tool_result in assistant message when missing (interrupted stream)", () => {
463
+ // If server_tool_use has no paired result (e.g. stream was interrupted),
464
+ // the synthetic result goes in the SAME assistant message, not a user message
465
+ const messages: Message[] = [
466
+ { role: "user", content: [{ type: "text", text: "Search" }] },
467
+ {
468
+ role: "assistant",
469
+ content: [
470
+ {
471
+ type: "server_tool_use",
472
+ id: "stu_1",
473
+ name: "web_search",
474
+ input: { query: "test" },
475
+ },
476
+ ],
477
+ },
478
+ {
479
+ role: "user",
480
+ content: [{ type: "text", text: "next message" }],
481
+ },
482
+ ];
483
+
484
+ const { messages: repaired, stats } = repairHistory(messages);
485
+
486
+ expect(stats.missingToolResultsInserted).toBe(1);
487
+
488
+ // Synthetic result is in the assistant message, not the user message
489
+ const assistantMsg = repaired[1];
490
+ expect(assistantMsg.content).toHaveLength(2);
491
+ expect(assistantMsg.content[1]).toMatchObject({
492
+ type: "web_search_tool_result",
493
+ tool_use_id: "stu_1",
494
+ content: { type: "web_search_tool_result_error", error_code: "unavailable" },
495
+ });
496
+
497
+ // User message has no web_search_tool_result
498
+ const userMsg = repaired[2];
499
+ expect(userMsg.content.every((b) => b.type !== "web_search_tool_result")).toBe(true);
500
+ });
501
+
502
+ test("migrates legacy web_search_tool_result from user message to assistant message", () => {
503
+ // Old history format: server_tool_use in assistant, web_search_tool_result in user.
504
+ // Repair: synthesize result in assistant, orphan-downgrade the user-side result.
505
+ const messages: Message[] = [
506
+ { role: "user", content: [{ type: "text", text: "Search" }] },
507
+ {
508
+ role: "assistant",
509
+ content: [
510
+ {
511
+ type: "server_tool_use",
512
+ id: "srvtoolu_abc",
513
+ name: "web_search",
514
+ input: { query: "test" },
515
+ },
516
+ {
517
+ type: "tool_use",
518
+ id: "tu_1",
519
+ name: "bash",
520
+ input: { cmd: "ls" },
521
+ },
522
+ ],
523
+ },
524
+ {
525
+ role: "user",
526
+ content: [
527
+ {
528
+ type: "web_search_tool_result",
529
+ tool_use_id: "srvtoolu_abc",
530
+ content: [{ type: "web_search_result", url: "https://example.com" }],
531
+ },
532
+ { type: "tool_result", tool_use_id: "tu_1", content: "files" },
533
+ ],
534
+ },
535
+ {
536
+ role: "assistant",
537
+ content: [{ type: "text", text: "Done" }],
538
+ },
539
+ ];
540
+
541
+ const { messages: repaired, stats } = repairHistory(messages);
542
+
543
+ // Synthetic web_search_tool_result added to assistant message
544
+ expect(stats.missingToolResultsInserted).toBe(1);
545
+
546
+ // The assistant message now has the server pair + client tool_use
547
+ const assistantMsg = repaired[1];
548
+ const serverToolUse = assistantMsg.content.find((b) => b.type === "server_tool_use");
549
+ const webSearchResult = assistantMsg.content.find((b) => b.type === "web_search_tool_result");
550
+ expect(serverToolUse).toBeDefined();
551
+ expect(webSearchResult).toBeDefined();
552
+
553
+ // The user message has tool_result for tu_1, and the old web_search_tool_result is downgraded
554
+ const userMsg = repaired[2];
555
+ expect(stats.orphanToolResultsDowngraded).toBe(1);
556
+ expect(userMsg.content.some((b) => b.type === "tool_result")).toBe(true);
557
+ expect(userMsg.content.every((b) => b.type !== "web_search_tool_result")).toBe(true);
558
+ });
559
+
560
+ test("trailing server_tool_use gets synthetic result in same assistant message", () => {
561
+ // No trailing user message needed — result goes in the assistant message
562
+ const messages: Message[] = [
563
+ { role: "user", content: [{ type: "text", text: "Go" }] },
564
+ {
565
+ role: "assistant",
566
+ content: [
567
+ {
568
+ type: "server_tool_use",
569
+ id: "stu_1",
570
+ name: "web_search",
571
+ input: { query: "test" },
572
+ },
573
+ ],
574
+ },
575
+ ];
576
+
577
+ const { messages: repaired, stats } = repairHistory(messages);
578
+
579
+ expect(stats.missingToolResultsInserted).toBe(1);
580
+ // Result is in the assistant message
581
+ expect(repaired).toHaveLength(2);
582
+ expect(repaired[1].role).toBe("assistant");
583
+ expect(repaired[1].content).toHaveLength(2);
584
+ expect(repaired[1].content[1]).toMatchObject({
585
+ type: "web_search_tool_result",
586
+ tool_use_id: "stu_1",
587
+ content: { type: "web_search_tool_result_error", error_code: "unavailable" },
588
+ });
589
+ });
590
+
591
+ test("downgrades type-mismatched tool_result for server_tool_use", () => {
592
+ // A tool_result in the user message for a server_tool_use ID is orphaned —
593
+ // server-side results belong in the assistant message
594
+ const messages: Message[] = [
595
+ { role: "user", content: [{ type: "text", text: "Search" }] },
596
+ {
597
+ role: "assistant",
598
+ content: [
599
+ {
600
+ type: "server_tool_use",
601
+ id: "stu_1",
602
+ name: "web_search",
603
+ input: { query: "test" },
604
+ },
605
+ ],
606
+ },
607
+ {
608
+ role: "user",
609
+ content: [
610
+ { type: "tool_result", tool_use_id: "stu_1", content: "wrong type" },
611
+ ],
612
+ },
613
+ {
614
+ role: "assistant",
615
+ content: [{ type: "text", text: "Done" }],
616
+ },
617
+ ];
618
+
619
+ const { messages: repaired, stats } = repairHistory(messages);
620
+
621
+ // Synthetic web_search_tool_result added to assistant message
622
+ expect(stats.missingToolResultsInserted).toBe(1);
623
+ // The mismatched tool_result in user message is orphaned (no pending client tool_use)
624
+ expect(stats.orphanToolResultsDowngraded).toBe(1);
625
+
626
+ // Assistant message has the server pair
627
+ const assistantMsg = repaired[1];
628
+ expect(assistantMsg.content.some((b) => b.type === "web_search_tool_result")).toBe(true);
629
+
630
+ // User message has no web_search_tool_result — the tool_result was downgraded to text
631
+ const userMsg = repaired[2];
632
+ expect(userMsg.content.every((b) => b.type !== "web_search_tool_result")).toBe(true);
633
+ expect(userMsg.content.every((b) => b.type !== "tool_result")).toBe(true);
634
+ });
635
+
636
+ test("downgrades type-mismatched web_search_tool_result for tool_use", () => {
637
+ // A web_search_tool_result paired with a regular tool_use ID is a type mismatch
638
+ const messages: Message[] = [
639
+ { role: "user", content: [{ type: "text", text: "Go" }] },
640
+ {
641
+ role: "assistant",
642
+ content: [
643
+ { type: "tool_use", id: "tu_1", name: "bash", input: { cmd: "ls" } },
644
+ ],
645
+ },
646
+ {
647
+ role: "user",
648
+ content: [
649
+ {
650
+ type: "web_search_tool_result",
651
+ tool_use_id: "tu_1",
652
+ content: [{ type: "web_search_result", url: "https://example.com" }],
653
+ },
654
+ ],
655
+ },
656
+ {
657
+ role: "assistant",
658
+ content: [{ type: "text", text: "Done" }],
659
+ },
660
+ ];
661
+
662
+ const { messages: repaired, stats } = repairHistory(messages);
663
+
664
+ expect(stats.orphanToolResultsDowngraded).toBe(1);
665
+ expect(stats.missingToolResultsInserted).toBe(1);
666
+
667
+ const userMsg = repaired[2];
668
+ const trBlocks = userMsg.content.filter((b) => b.type === "tool_result");
669
+ expect(trBlocks).toHaveLength(1);
670
+ expect(trBlocks[0]).toMatchObject({
671
+ type: "tool_result",
672
+ tool_use_id: "tu_1",
673
+ is_error: true,
674
+ });
675
+ });
431
676
  });
432
677
 
433
678
  describe("deepRepairHistory", () => {