@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
@@ -733,31 +733,60 @@ export function injectChannelTurnContext(
733
733
  export function buildInboundActorContextBlock(
734
734
  ctx: InboundActorContext,
735
735
  ): string {
736
+ const sanitizeInlineContextValue = (
737
+ value: string | null | undefined,
738
+ ): string => {
739
+ if (!value) {
740
+ return "unknown";
741
+ }
742
+ const singleLine = value
743
+ // Replace ASCII and Unicode line/paragraph separators.
744
+ .replace(/[\r\n\u0085\u2028\u2029]+/g, " ")
745
+ // Replace remaining ASCII C0/C1 control characters and DEL.
746
+ .replace(/[\x00-\x1F\x7F-\x9F]/g, " ")
747
+ .trim();
748
+ return singleLine.length > 0 ? singleLine : "unknown";
749
+ };
750
+
736
751
  const lines: string[] = ["<inbound_actor_context>"];
737
- lines.push(`source_channel: ${ctx.sourceChannel}`);
738
752
  lines.push(
739
- `canonical_actor_identity: ${ctx.canonicalActorIdentity ?? "unknown"}`,
753
+ `source_channel: ${sanitizeInlineContextValue(ctx.sourceChannel)}`,
754
+ );
755
+ lines.push(
756
+ `canonical_actor_identity: ${sanitizeInlineContextValue(ctx.canonicalActorIdentity)}`,
757
+ );
758
+ lines.push(
759
+ `actor_identifier: ${sanitizeInlineContextValue(ctx.actorIdentifier)}`,
760
+ );
761
+ lines.push(
762
+ `actor_display_name: ${sanitizeInlineContextValue(ctx.actorDisplayName)}`,
740
763
  );
741
- lines.push(`actor_identifier: ${ctx.actorIdentifier ?? "unknown"}`);
742
- lines.push(`actor_display_name: ${ctx.actorDisplayName ?? "unknown"}`);
743
764
  lines.push(
744
- `actor_sender_display_name: ${ctx.actorSenderDisplayName ?? "unknown"}`,
765
+ `actor_sender_display_name: ${sanitizeInlineContextValue(ctx.actorSenderDisplayName)}`,
745
766
  );
746
767
  lines.push(
747
- `actor_member_display_name: ${ctx.actorMemberDisplayName ?? "unknown"}`,
768
+ `actor_member_display_name: ${sanitizeInlineContextValue(ctx.actorMemberDisplayName)}`,
769
+ );
770
+ lines.push(`trust_class: ${sanitizeInlineContextValue(ctx.trustClass)}`);
771
+ lines.push(
772
+ `guardian_identity: ${sanitizeInlineContextValue(ctx.guardianIdentity)}`,
748
773
  );
749
- lines.push(`trust_class: ${ctx.trustClass}`);
750
- lines.push(`guardian_identity: ${ctx.guardianIdentity ?? "unknown"}`);
751
774
  if (ctx.memberStatus) {
752
- lines.push(`member_status: ${ctx.memberStatus}`);
775
+ lines.push(
776
+ `member_status: ${sanitizeInlineContextValue(ctx.memberStatus)}`,
777
+ );
753
778
  }
754
779
  if (ctx.memberPolicy) {
755
- lines.push(`member_policy: ${ctx.memberPolicy}`);
780
+ lines.push(
781
+ `member_policy: ${sanitizeInlineContextValue(ctx.memberPolicy)}`,
782
+ );
756
783
  }
757
784
  // Contact metadata — only included when the sender has a contact record
758
785
  // with non-default values.
759
786
  if (ctx.contactNotes) {
760
- lines.push(`contact_notes: ${ctx.contactNotes}`);
787
+ lines.push(
788
+ `contact_notes: ${sanitizeInlineContextValue(ctx.contactNotes)}`,
789
+ );
761
790
  }
762
791
  if (ctx.contactInteractionCount != null && ctx.contactInteractionCount > 0) {
763
792
  lines.push(`contact_interaction_count: ${ctx.contactInteractionCount}`);
@@ -765,7 +794,8 @@ export function buildInboundActorContextBlock(
765
794
  if (
766
795
  ctx.actorMemberDisplayName &&
767
796
  ctx.actorSenderDisplayName &&
768
- ctx.actorMemberDisplayName !== ctx.actorSenderDisplayName
797
+ sanitizeInlineContextValue(ctx.actorMemberDisplayName) !==
798
+ sanitizeInlineContextValue(ctx.actorSenderDisplayName)
769
799
  ) {
770
800
  lines.push(
771
801
  "name_preference_note: actor_member_display_name is the guardian-preferred nickname for this person; actor_sender_display_name is the channel-provided display name.",
@@ -781,9 +811,12 @@ export function buildInboundActorContextBlock(
781
811
  lines.push(
782
812
  "This is a trusted contact (non-guardian). When the actor makes a reasonable actionable request, attempt to fulfill it normally using the appropriate tool. If the action requires guardian approval, the tool execution layer will automatically deny it and escalate to the guardian for approval — you do not need to pre-screen or decline on behalf of the guardian. Do not self-approve, bypass security gates, or claim to have permissions you do not have. Do not explain the verification system, mention other access methods, or suggest the requester might be the guardian on another device — this leaks system internals and invites social engineering.",
783
813
  );
784
- if (ctx.actorDisplayName && ctx.actorDisplayName !== "unknown") {
814
+ if (
815
+ ctx.actorDisplayName &&
816
+ sanitizeInlineContextValue(ctx.actorDisplayName) !== "unknown"
817
+ ) {
785
818
  lines.push(
786
- `When this person asks about their name or identity, their name is "${ctx.actorDisplayName}".`,
819
+ `When this person asks about their name or identity, their name is "${sanitizeInlineContextValue(ctx.actorDisplayName)}".`,
787
820
  );
788
821
  }
789
822
  } else if (ctx.trustClass === "unknown") {
@@ -999,20 +1032,17 @@ const RUNTIME_INJECTION_PREFIXES = [
999
1032
  *
1000
1033
  * Composes:
1001
1034
  * 1. `stripMemoryRecallMessages` (caller-supplied, handles its own logic)
1002
- * 2. `stripDynamicProfileMessages` (caller-supplied, handles its own logic)
1003
- * 3. Prefix-based stripping for channel capabilities, workspace top-level,
1035
+ * 2. Prefix-based stripping for channel capabilities, workspace top-level,
1004
1036
  * temporal context, and active surface context (single pass).
1005
1037
  */
1006
1038
  export function stripInjectedContext(
1007
1039
  messages: Message[],
1008
1040
  options: {
1009
1041
  stripRecall: (msgs: Message[]) => Message[];
1010
- stripDynamicProfile: (msgs: Message[]) => Message[];
1011
1042
  },
1012
1043
  ): Message[] {
1013
1044
  const afterRecall = options.stripRecall(messages);
1014
- const afterProfile = options.stripDynamicProfile(afterRecall);
1015
- return stripUserTextBlocksByPrefix(afterProfile, RUNTIME_INJECTION_PREFIXES);
1045
+ return stripUserTextBlocksByPrefix(afterRecall, RUNTIME_INJECTION_PREFIXES);
1016
1046
  }
1017
1047
 
1018
1048
  /**
@@ -166,7 +166,7 @@ function resolveProviderModelCommand(content: string): SlashResolution | null {
166
166
  if (provider !== "ollama" && !config.apiKeys[provider]) {
167
167
  return {
168
168
  kind: "unknown",
169
- message: `Cannot switch to ${displayName}. No API key configured for ${provider}.\n\nSet it with: \`config set apiKeys.${provider} <your-key>\``,
169
+ message: `Cannot switch to ${displayName}. No API key configured for ${provider}.\n\nSet it with: \`keys set ${provider} <your-key>\``,
170
170
  };
171
171
  }
172
172
 
@@ -216,9 +216,7 @@ function resolveModelList(): SlashResolution {
216
216
  }
217
217
 
218
218
  lines.push("\n✓ = API key configured, ✗ = not configured");
219
- lines.push(
220
- "\nTip: Configure a provider with `config set apiKeys.<provider> <key>`",
221
- );
219
+ lines.push("\nTip: Configure a provider with `keys set <provider> <key>`");
222
220
 
223
221
  return {
224
222
  kind: "unknown",
@@ -286,7 +284,7 @@ function resolveModelCommand(content: string): SlashResolution | null {
286
284
  const displayName = MODEL_DISPLAY_NAMES[matched] ?? matched;
287
285
  return {
288
286
  kind: "unknown",
289
- message: `Cannot switch to ${displayName}. No API key configured for Anthropic.\n\nSet it with: \`config set apiKeys.anthropic <your-key>\``,
287
+ message: `Cannot switch to ${displayName}. No API key configured for Anthropic.\n\nSet it with: \`keys set anthropic <your-key>\``,
290
288
  };
291
289
  }
292
290
 
@@ -614,6 +614,7 @@ export function handleSurfaceAction(
614
614
  userMessage: `Something went wrong: ${message}`,
615
615
  retryable: false,
616
616
  debugDetails: `History-restored relay prompt processing failed: ${message}`,
617
+ errorCategory: "processing_failed",
617
618
  }),
618
619
  );
619
620
  });
@@ -938,10 +939,11 @@ export async function surfaceProxyResolver(
938
939
  ctx: SurfaceSessionContext,
939
940
  toolName: string,
940
941
  input: Record<string, unknown>,
942
+ signal?: AbortSignal,
941
943
  ): Promise<ToolExecutionResult> {
942
944
  // Route CU proxy tools (all computer_use_* action tools)
943
945
  if (toolName.startsWith("computer_use_")) {
944
- if (!ctx.hostCuProxy) {
946
+ if (!ctx.hostCuProxy || !ctx.hostCuProxy.isAvailable()) {
945
947
  return {
946
948
  content: "Computer use is not available — no desktop client connected.",
947
949
  isError: true,
@@ -973,6 +975,7 @@ export async function surfaceProxyResolver(
973
975
  ctx.conversationId,
974
976
  ctx.hostCuProxy.stepCount,
975
977
  reasoning,
978
+ signal,
976
979
  );
977
980
  }
978
981
 
@@ -208,7 +208,13 @@ export function createToolExecutor(
208
208
  proxyToolResolver: (
209
209
  toolName: string,
210
210
  proxyInput: Record<string, unknown>,
211
- ) => surfaceProxyResolver(ctx, toolName, proxyInput),
211
+ ) =>
212
+ surfaceProxyResolver(
213
+ ctx,
214
+ toolName,
215
+ proxyInput,
216
+ ctx.abortController?.signal,
217
+ ),
212
218
  proxyApprovalCallback: createProxyApprovalCallback(prompter, ctx),
213
219
  requestSecret: async (params) => {
214
220
  return secretPrompter.prompt(
@@ -65,7 +65,6 @@ import type {
65
65
  ConfirmationStateChanged,
66
66
  } from "./message-types/messages.js";
67
67
  import { runAgentLoopImpl } from "./session-agent-loop.js";
68
- import { ConflictGate } from "./session-conflict-gate.js";
69
68
  import type { HistorySessionContext } from "./session-history.js";
70
69
  import {
71
70
  regenerate as regenerateImpl,
@@ -159,7 +158,6 @@ export class Session {
159
158
  /** @internal */ contextCompactedMessageCount = 0;
160
159
  /** @internal */ contextCompactedAt: number | null = null;
161
160
  /** @internal */ currentRequestId?: string;
162
- /** @internal */ conflictGate = new ConflictGate();
163
161
  /** @internal */ hasNoClient = false;
164
162
  /** @internal */ hasAttachments = false;
165
163
  /** @internal */ headlessLock = false;
@@ -822,6 +820,18 @@ export class Session {
822
820
  this.preactivatedSkillIds = ids;
823
821
  }
824
822
 
823
+ /**
824
+ * Add a skill ID to the preactivated set without replacing existing entries.
825
+ * No-op if the ID is already present.
826
+ */
827
+ addPreactivatedSkillId(id: string): void {
828
+ if (!this.preactivatedSkillIds) {
829
+ this.preactivatedSkillIds = [id];
830
+ } else if (!this.preactivatedSkillIds.includes(id)) {
831
+ this.preactivatedSkillIds.push(id);
832
+ }
833
+ }
834
+
825
835
  setTurnChannelContext(ctx: TurnChannelContext): void {
826
836
  this.currentTurnChannelContext = ctx;
827
837
  }
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { getNestedValue, loadRawConfig } from "../../config/loader.js";
8
8
  import { credentialKey } from "../../security/credential-key.js";
9
- import { getSecureKey } from "../../security/secure-keys.js";
9
+ import { getSecureKeyAsync } from "../../security/secure-keys.js";
10
10
  import { ConfigError } from "../../util/errors.js";
11
11
  import type { EmailProvider } from "../provider.js";
12
12
 
@@ -47,7 +47,7 @@ export async function createProvider(
47
47
  const candidates = PROVIDER_KEY_MAP.agentmail;
48
48
  let apiKey: string | undefined;
49
49
  for (const account of candidates) {
50
- apiKey = getSecureKey(account);
50
+ apiKey = await getSecureKeyAsync(account);
51
51
  if (apiKey) break;
52
52
  }
53
53
  if (!apiKey) {
package/src/instrument.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { arch, platform, release } from "node:os";
1
+ import { arch, hostname, platform, release } from "node:os";
2
2
 
3
3
  import * as Sentry from "@sentry/node";
4
4
 
@@ -47,12 +47,14 @@ export function initSentry(): void {
47
47
  dist: COMMIT_SHA,
48
48
  environment: APP_VERSION === "0.0.0-dev" ? "development" : "production",
49
49
  sendDefaultPii: false,
50
+ serverName: hostname(),
50
51
  initialScope: {
51
52
  tags: {
52
53
  commit: COMMIT_SHA,
53
54
  os_platform: platform(),
54
55
  os_release: release(),
55
56
  os_arch: arch(),
57
+ server_name: hostname(),
56
58
  runtime: "bun",
57
59
  runtime_version:
58
60
  typeof Bun !== "undefined" ? Bun.version : process.version,
@@ -90,3 +92,61 @@ export function initSentry(): void {
90
92
  export async function closeSentry(): Promise<void> {
91
93
  await Sentry.close();
92
94
  }
95
+
96
+ // ── Dynamic session-scoped Sentry tags ──────────────────────────────
97
+ //
98
+ // These tags change per conversation turn and are set on the current
99
+ // Sentry scope before the agent loop runs. Any `Sentry.captureException`
100
+ // call within that async execution chain (e.g. inside agent/loop.ts)
101
+ // will inherit these tags, enabling filtering by conversation, session,
102
+ // user, or assistant in the Sentry dashboard.
103
+
104
+ /** Tag keys set by {@link setSentrySessionContext}. */
105
+ const SESSION_TAG_KEYS = [
106
+ "assistant_id",
107
+ "conversation_id",
108
+ "session_id",
109
+ "message_count",
110
+ "user_identifier",
111
+ ] as const;
112
+
113
+ export interface SentrySessionContext {
114
+ /** Internal assistant ID (daemon uses 'self'). */
115
+ assistantId: string;
116
+ /** Conversation/session identifier. */
117
+ conversationId: string;
118
+ /** Number of messages in the conversation at time of the turn. */
119
+ messageCount: number;
120
+ /** Stable per-user identifier (guardian principal ID or similar). */
121
+ userIdentifier?: string;
122
+ }
123
+
124
+ /**
125
+ * Set session-scoped tags on the current Sentry scope.
126
+ *
127
+ * Call at the start of each agent loop turn so that any exceptions
128
+ * captured within the turn include conversation/session context.
129
+ */
130
+ export function setSentrySessionContext(ctx: SentrySessionContext): void {
131
+ Sentry.setTag("assistant_id", ctx.assistantId);
132
+ Sentry.setTag("conversation_id", ctx.conversationId);
133
+ // session_id mirrors conversation_id — in this codebase they are the
134
+ // same value, but downstream Sentry users may search by either name.
135
+ Sentry.setTag("session_id", ctx.conversationId);
136
+ Sentry.setTag("message_count", String(ctx.messageCount));
137
+ if (ctx.userIdentifier) {
138
+ Sentry.setTag("user_identifier", ctx.userIdentifier);
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Clear session-scoped tags from the current Sentry scope.
144
+ *
145
+ * Call in the finally block after the agent loop completes so tags
146
+ * from one conversation do not leak into unrelated error captures.
147
+ */
148
+ export function clearSentrySessionContext(): void {
149
+ for (const key of SESSION_TAG_KEYS) {
150
+ Sentry.setTag(key, undefined);
151
+ }
152
+ }
@@ -32,7 +32,7 @@ export async function generateAvatar(
32
32
 
33
33
  if (!credentials) {
34
34
  throw new ConfigError(
35
- "Gemini API key is not configured. Set it via `config set apiKeys.gemini <key>` or the GEMINI_API_KEY environment variable.",
35
+ "Gemini API key is not configured. Set it via `keys set gemini <key>` or the GEMINI_API_KEY environment variable.",
36
36
  );
37
37
  }
38
38
 
@@ -1,14 +1,9 @@
1
1
  import { getConfig } from "../config/loader.js";
2
2
  import { getLogger } from "../util/logger.js";
3
- import {
4
- listPendingConflictDetails,
5
- resolveConflict,
6
- } from "./conflict-store.js";
7
3
  import { rawGet } from "./db.js";
8
4
  import { getMemoryBackendStatus } from "./embedding-backend.js";
9
5
  import { enqueueBackfillJob, enqueueRebuildIndexJob } from "./indexer.js";
10
6
  import {
11
- enqueueCleanupResolvedConflictsJob,
12
7
  enqueueCleanupStaleSupersededItemsJob,
13
8
  getMemoryJobCounts,
14
9
  } from "./jobs-store.js";
@@ -28,94 +23,18 @@ export interface MemorySystemStatus {
28
23
  summaries: number;
29
24
  embeddings: number;
30
25
  };
31
- conflicts: {
32
- pending: number;
33
- resolved: number;
34
- oldestPendingAgeMs: number | null;
35
- };
36
26
  cleanup: {
37
- resolvedBacklog: number;
38
27
  supersededBacklog: number;
39
- resolvedCompleted24h: number;
40
28
  supersededCompleted24h: number;
41
29
  };
42
30
  jobs: Record<string, number>;
43
31
  }
44
32
 
45
- export interface MemoryConflictAndCleanupStats {
46
- conflicts: MemorySystemStatus["conflicts"];
47
- cleanup: MemorySystemStatus["cleanup"];
48
- }
49
-
50
- interface ConflictStatsRow {
51
- pending_count: number | null;
52
- resolved_count: number | null;
53
- oldest_pending_created_at: number | null;
54
- }
55
-
56
33
  interface CleanupStatsRow {
57
- resolved_backlog: number | null;
58
34
  superseded_backlog: number | null;
59
- resolved_completed_24h: number | null;
60
35
  superseded_completed_24h: number | null;
61
36
  }
62
37
 
63
- /** Lightweight query for conflict/cleanup metrics only — no table counts or job totals. */
64
- export function getMemoryConflictAndCleanupStats(): MemoryConflictAndCleanupStats {
65
- const conflictStats = rawGet<ConflictStatsRow>(`
66
- SELECT
67
- SUM(CASE WHEN status = 'pending_clarification' THEN 1 ELSE 0 END) AS pending_count,
68
- SUM(CASE WHEN status != 'pending_clarification' THEN 1 ELSE 0 END) AS resolved_count,
69
- MIN(CASE WHEN status = 'pending_clarification' THEN created_at END) AS oldest_pending_created_at
70
- FROM memory_item_conflicts
71
- `);
72
- const pending = conflictStats?.pending_count ?? 0;
73
- const oldestPendingCreatedAt =
74
- conflictStats?.oldest_pending_created_at ?? null;
75
- const oldestPendingAgeMs =
76
- oldestPendingCreatedAt == null
77
- ? null
78
- : Math.max(0, Date.now() - oldestPendingCreatedAt);
79
- const throughputWindowStartMs = Date.now() - 24 * 60 * 60 * 1000;
80
- const cleanupStats = rawGet<CleanupStatsRow>(
81
- `
82
- SELECT
83
- SUM(CASE
84
- WHEN type = 'cleanup_resolved_conflicts' AND status IN ('pending', 'running')
85
- THEN 1 ELSE 0 END
86
- ) AS resolved_backlog,
87
- SUM(CASE
88
- WHEN type = 'cleanup_stale_superseded_items' AND status IN ('pending', 'running')
89
- THEN 1 ELSE 0 END
90
- ) AS superseded_backlog,
91
- SUM(CASE
92
- WHEN type = 'cleanup_resolved_conflicts' AND status = 'completed' AND updated_at >= ?
93
- THEN 1 ELSE 0 END
94
- ) AS resolved_completed_24h,
95
- SUM(CASE
96
- WHEN type = 'cleanup_stale_superseded_items' AND status = 'completed' AND updated_at >= ?
97
- THEN 1 ELSE 0 END
98
- ) AS superseded_completed_24h
99
- FROM memory_jobs
100
- `,
101
- throughputWindowStartMs,
102
- throughputWindowStartMs,
103
- );
104
- return {
105
- conflicts: {
106
- pending,
107
- resolved: conflictStats?.resolved_count ?? 0,
108
- oldestPendingAgeMs,
109
- },
110
- cleanup: {
111
- resolvedBacklog: cleanupStats?.resolved_backlog ?? 0,
112
- supersededBacklog: cleanupStats?.superseded_backlog ?? 0,
113
- resolvedCompleted24h: cleanupStats?.resolved_completed_24h ?? 0,
114
- supersededCompleted24h: cleanupStats?.superseded_completed_24h ?? 0,
115
- },
116
- };
117
- }
118
-
119
38
  export function getMemorySystemStatus(): MemorySystemStatus {
120
39
  const config = getConfig();
121
40
  const backend = getMemoryBackendStatus(config);
@@ -125,36 +44,14 @@ export function getMemorySystemStatus(): MemorySystemStatus {
125
44
  summaries: countTable("memory_summaries"),
126
45
  embeddings: countTable("memory_embeddings"),
127
46
  };
128
- const conflictStats = rawGet<ConflictStatsRow>(`
129
- SELECT
130
- SUM(CASE WHEN status = 'pending_clarification' THEN 1 ELSE 0 END) AS pending_count,
131
- SUM(CASE WHEN status != 'pending_clarification' THEN 1 ELSE 0 END) AS resolved_count,
132
- MIN(CASE WHEN status = 'pending_clarification' THEN created_at END) AS oldest_pending_created_at
133
- FROM memory_item_conflicts
134
- `);
135
- const pending = conflictStats?.pending_count ?? 0;
136
- const oldestPendingCreatedAt =
137
- conflictStats?.oldest_pending_created_at ?? null;
138
- const oldestPendingAgeMs =
139
- oldestPendingCreatedAt == null
140
- ? null
141
- : Math.max(0, Date.now() - oldestPendingCreatedAt);
142
47
  const throughputWindowStartMs = Date.now() - 24 * 60 * 60 * 1000;
143
48
  const cleanupStats = rawGet<CleanupStatsRow>(
144
49
  `
145
50
  SELECT
146
- SUM(CASE
147
- WHEN type = 'cleanup_resolved_conflicts' AND status IN ('pending', 'running')
148
- THEN 1 ELSE 0 END
149
- ) AS resolved_backlog,
150
51
  SUM(CASE
151
52
  WHEN type = 'cleanup_stale_superseded_items' AND status IN ('pending', 'running')
152
53
  THEN 1 ELSE 0 END
153
54
  ) AS superseded_backlog,
154
- SUM(CASE
155
- WHEN type = 'cleanup_resolved_conflicts' AND status = 'completed' AND updated_at >= ?
156
- THEN 1 ELSE 0 END
157
- ) AS resolved_completed_24h,
158
55
  SUM(CASE
159
56
  WHEN type = 'cleanup_stale_superseded_items' AND status = 'completed' AND updated_at >= ?
160
57
  THEN 1 ELSE 0 END
@@ -162,7 +59,6 @@ export function getMemorySystemStatus(): MemorySystemStatus {
162
59
  FROM memory_jobs
163
60
  `,
164
61
  throughputWindowStartMs,
165
- throughputWindowStartMs,
166
62
  );
167
63
  return {
168
64
  enabled: backend.enabled,
@@ -171,15 +67,8 @@ export function getMemorySystemStatus(): MemorySystemStatus {
171
67
  provider: backend.provider,
172
68
  model: backend.model,
173
69
  counts,
174
- conflicts: {
175
- pending,
176
- resolved: conflictStats?.resolved_count ?? 0,
177
- oldestPendingAgeMs,
178
- },
179
70
  cleanup: {
180
- resolvedBacklog: cleanupStats?.resolved_backlog ?? 0,
181
71
  supersededBacklog: cleanupStats?.superseded_backlog ?? 0,
182
- resolvedCompleted24h: cleanupStats?.resolved_completed_24h ?? 0,
183
72
  supersededCompleted24h: cleanupStats?.superseded_completed_24h ?? 0,
184
73
  },
185
74
  jobs: getMemoryJobCounts(),
@@ -203,95 +92,17 @@ export function requestMemoryRebuildIndex(): string {
203
92
  }
204
93
 
205
94
  export function requestMemoryCleanup(retentionMs?: number): {
206
- resolvedConflictsJobId: string;
207
95
  staleSupersededItemsJobId: string;
208
96
  } {
209
- const resolvedConflictsJobId =
210
- enqueueCleanupResolvedConflictsJob(retentionMs);
211
97
  const staleSupersededItemsJobId =
212
98
  enqueueCleanupStaleSupersededItemsJob(retentionMs);
213
99
  log.info(
214
- { resolvedConflictsJobId, staleSupersededItemsJobId, retentionMs },
100
+ { staleSupersededItemsJobId, retentionMs },
215
101
  "Queued memory cleanup jobs",
216
102
  );
217
- return { resolvedConflictsJobId, staleSupersededItemsJobId };
103
+ return { staleSupersededItemsJobId };
218
104
  }
219
105
 
220
106
  export async function queryMemory(query: string, conversationId: string) {
221
107
  return queryMemoryForCli(query, conversationId, getConfig());
222
108
  }
223
-
224
- export interface DismissConflictsResult {
225
- dismissed: number;
226
- remaining: number;
227
- details: Array<{
228
- id: string;
229
- existingStatement: string;
230
- candidateStatement: string;
231
- }>;
232
- }
233
-
234
- /**
235
- * Dismiss pending memory conflicts. With `all: true`, dismisses every
236
- * pending conflict. Otherwise, dismisses only conflicts matching the
237
- * given `pattern` regex against either statement.
238
- */
239
- export function dismissPendingConflicts(options: {
240
- all?: boolean;
241
- pattern?: RegExp;
242
- scopeId?: string;
243
- }): DismissConflictsResult {
244
- const scopeId = options.scopeId ?? "default";
245
- const dismissed: DismissConflictsResult["details"] = [];
246
- const BATCH_SIZE = 1000;
247
-
248
- // Cursor-based pagination: track last seen (createdAt, id) to advance past
249
- // previously inspected rows. This ensures pattern mode scans all pending
250
- // conflicts even when non-matching rows outnumber the batch size.
251
- let cursor: { createdAt: number; id: string } | undefined;
252
- let batch = listPendingConflictDetails(scopeId, BATCH_SIZE);
253
- while (batch.length > 0) {
254
- for (const conflict of batch) {
255
- const matches =
256
- options.all ||
257
- (options.pattern &&
258
- (options.pattern.test(conflict.existingStatement) ||
259
- options.pattern.test(conflict.candidateStatement)));
260
- if (!matches) continue;
261
-
262
- resolveConflict(conflict.id, {
263
- status: "dismissed",
264
- resolutionNote: options.all
265
- ? "Bulk dismissed via CLI (dismiss-conflicts --all)."
266
- : `Dismissed via CLI (pattern: ${options.pattern?.source}).`,
267
- });
268
- dismissed.push({
269
- id: conflict.id,
270
- existingStatement: conflict.existingStatement,
271
- candidateStatement: conflict.candidateStatement,
272
- });
273
- }
274
- // No more rows to inspect
275
- if (batch.length < BATCH_SIZE) break;
276
- const lastRow = batch[batch.length - 1];
277
- cursor = { createdAt: lastRow.createdAt, id: lastRow.id };
278
- batch = listPendingConflictDetails(scopeId, BATCH_SIZE, cursor);
279
- }
280
-
281
- // Get true remaining count via SQL to avoid batch-size truncation
282
- const remaining =
283
- rawGet<{ c: number }>(
284
- `SELECT COUNT(*) AS c FROM memory_item_conflicts WHERE scope_id = ? AND status = 'pending_clarification'`,
285
- scopeId,
286
- )?.c ?? 0;
287
-
288
- log.info(
289
- { dismissed: dismissed.length, remaining, scopeId },
290
- "Dismissed pending conflicts",
291
- );
292
- return {
293
- dismissed: dismissed.length,
294
- remaining,
295
- details: dismissed,
296
- };
297
- }
@@ -17,6 +17,21 @@ import {
17
17
  canonicalGuardianRequests,
18
18
  } from "./schema.js";
19
19
 
20
+ // ---------------------------------------------------------------------------
21
+ // Expiry helpers
22
+ // ---------------------------------------------------------------------------
23
+
24
+ /**
25
+ * Returns true when a canonical request has passed its `expiresAt` deadline.
26
+ * Requests without an `expiresAt` are never considered expired by this check.
27
+ */
28
+ export function isRequestExpired(
29
+ request: Pick<CanonicalGuardianRequest, "expiresAt">,
30
+ ): boolean {
31
+ if (!request.expiresAt) return false;
32
+ return new Date(request.expiresAt).getTime() < Date.now();
33
+ }
34
+
20
35
  // ---------------------------------------------------------------------------
21
36
  // Types
22
37
  // ---------------------------------------------------------------------------
@@ -444,6 +459,27 @@ export function resolveCanonicalGuardianRequest(
444
459
  return getCanonicalGuardianRequest(id);
445
460
  }
446
461
 
462
+ /**
463
+ * Expire all pending canonical guardian requests in a single bulk update.
464
+ *
465
+ * Called at daemon startup to clean up requests that can never be completed
466
+ * because the in-memory pending-interactions Map (which holds session
467
+ * references needed by resolvers) was wiped on restart.
468
+ *
469
+ * Returns the number of requests transitioned from pending → expired.
470
+ */
471
+ export function expireAllPendingCanonicalRequests(): number {
472
+ const db = getDb();
473
+ const now = new Date().toISOString();
474
+
475
+ db.update(canonicalGuardianRequests)
476
+ .set({ status: "expired", updatedAt: now })
477
+ .where(eq(canonicalGuardianRequests.status, "pending"))
478
+ .run();
479
+
480
+ return rawChanges();
481
+ }
482
+
447
483
  // ---------------------------------------------------------------------------
448
484
  // Canonical Guardian Deliveries
449
485
  // ---------------------------------------------------------------------------
@@ -624,14 +660,14 @@ export function listPendingRequestsByConversationScope(
624
660
  const result: CanonicalGuardianRequest[] = [];
625
661
 
626
662
  for (const req of bySource) {
627
- if (!seen.has(req.id)) {
663
+ if (!seen.has(req.id) && !isRequestExpired(req)) {
628
664
  seen.add(req.id);
629
665
  result.push(req);
630
666
  }
631
667
  }
632
668
 
633
669
  for (const req of byDestination) {
634
- if (!seen.has(req.id)) {
670
+ if (!seen.has(req.id) && !isRequestExpired(req)) {
635
671
  seen.add(req.id);
636
672
  result.push(req);
637
673
  }