@vellumai/assistant 0.5.16 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (407) hide show
  1. package/ARCHITECTURE.md +1 -1
  2. package/Dockerfile +0 -3
  3. package/knip.json +2 -1
  4. package/openapi.yaml +660 -80
  5. package/package.json +1 -1
  6. package/src/__tests__/actor-token-service.test.ts +68 -0
  7. package/src/__tests__/agent-loop.test.ts +0 -32
  8. package/src/__tests__/always-loaded-tools-guard.test.ts +2 -2
  9. package/src/__tests__/anthropic-provider.test.ts +57 -3
  10. package/src/__tests__/app-compiler.test.ts +120 -0
  11. package/src/__tests__/assistant-feature-flags-integration.test.ts +2 -2
  12. package/src/__tests__/call-conversation-messages.test.ts +2 -6
  13. package/src/__tests__/call-domain.test.ts +2 -6
  14. package/src/__tests__/call-pointer-messages.test.ts +2 -14
  15. package/src/__tests__/call-recovery.test.ts +2 -6
  16. package/src/__tests__/call-routes-http.test.ts +2 -6
  17. package/src/__tests__/call-store.test.ts +2 -6
  18. package/src/__tests__/cancel-resolves-conversation-key.test.ts +2 -6
  19. package/src/__tests__/canonical-guardian-store.test.ts +2 -6
  20. package/src/__tests__/channel-delivery-store.test.ts +2 -6
  21. package/src/__tests__/channel-retry-sweep.test.ts +2 -6
  22. package/src/__tests__/checker.test.ts +25 -3
  23. package/src/__tests__/clawhub.test.ts +54 -24
  24. package/src/__tests__/cli-command-risk-guard.test.ts +14 -0
  25. package/src/__tests__/cli-memory.test.ts +74 -69
  26. package/src/__tests__/config-schema.test.ts +1 -1
  27. package/src/__tests__/config-set-platform-guard.test.ts +302 -0
  28. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +2 -6
  29. package/src/__tests__/contacts-tools.test.ts +31 -0
  30. package/src/__tests__/context-overflow-reducer.test.ts +86 -0
  31. package/src/__tests__/context-token-estimator.test.ts +175 -10
  32. package/src/__tests__/conversation-agent-loop-overflow.test.ts +9 -0
  33. package/src/__tests__/conversation-agent-loop.test.ts +9 -0
  34. package/src/__tests__/conversation-attachments.test.ts +2 -6
  35. package/src/__tests__/conversation-attention-store.test.ts +2 -6
  36. package/src/__tests__/conversation-clear-safety.test.ts +2 -6
  37. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +4 -10
  38. package/src/__tests__/conversation-disk-view-integration.test.ts +2 -6
  39. package/src/__tests__/conversation-disk-view.test.ts +2 -6
  40. package/src/__tests__/conversation-error.test.ts +33 -2
  41. package/src/__tests__/conversation-fork-crud.test.ts +2 -6
  42. package/src/__tests__/conversation-history-web-search.test.ts +5 -0
  43. package/src/__tests__/conversation-load-history-repair.test.ts +5 -1
  44. package/src/__tests__/conversation-media-retry.test.ts +91 -0
  45. package/src/__tests__/conversation-starter-routes.test.ts +20 -11
  46. package/src/__tests__/conversation-store.test.ts +2 -6
  47. package/src/__tests__/conversation-usage.test.ts +2 -6
  48. package/src/__tests__/conversation-wipe.test.ts +11 -408
  49. package/src/__tests__/credential-execution-feature-gates.test.ts +3 -3
  50. package/src/__tests__/credential-execution-shell-lockdown.test.ts +2 -2
  51. package/src/__tests__/credential-security-e2e.test.ts +2 -0
  52. package/src/__tests__/followup-tools.test.ts +2 -6
  53. package/src/__tests__/graph-extraction-event-date.test.ts +186 -0
  54. package/src/__tests__/guardian-action-conversation-turn.test.ts +2 -6
  55. package/src/__tests__/guardian-action-followup-executor.test.ts +2 -6
  56. package/src/__tests__/guardian-action-followup-store.test.ts +2 -6
  57. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +2 -6
  58. package/src/__tests__/guardian-action-late-reply.test.ts +2 -6
  59. package/src/__tests__/guardian-action-store.test.ts +2 -6
  60. package/src/__tests__/guardian-binding-drift-heal.test.ts +2 -6
  61. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +8 -8
  62. package/src/__tests__/guardian-dispatch.test.ts +2 -6
  63. package/src/__tests__/guardian-grant-minting.test.ts +2 -14
  64. package/src/__tests__/guardian-principal-id-roundtrip.test.ts +2 -6
  65. package/src/__tests__/guardian-routing-invariants.test.ts +192 -6
  66. package/src/__tests__/guardian-routing-state.test.ts +2 -6
  67. package/src/__tests__/guardian-verification-voice-binding.test.ts +2 -6
  68. package/src/__tests__/inbound-invite-redemption.test.ts +2 -6
  69. package/src/__tests__/injection-block.test.ts +154 -0
  70. package/src/__tests__/install-meta.test.ts +506 -0
  71. package/src/__tests__/install-skill-routing.test.ts +292 -0
  72. package/src/__tests__/invite-redemption-service.test.ts +2 -6
  73. package/src/__tests__/invite-routes-http.test.ts +2 -6
  74. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +2 -14
  75. package/src/__tests__/list-messages-attachments.test.ts +2 -6
  76. package/src/__tests__/llm-context-route-provider.test.ts +2 -6
  77. package/src/__tests__/llm-request-log-turn-query.test.ts +2 -6
  78. package/src/__tests__/llm-usage-store.test.ts +2 -6
  79. package/src/__tests__/log-export-workspace.test.ts +2 -6
  80. package/src/__tests__/managed-store.test.ts +38 -11
  81. package/src/__tests__/memory-jobs-worker-backoff.test.ts +2 -8
  82. package/src/__tests__/memory-recall-log-store.test.ts +2 -6
  83. package/src/__tests__/memory-upsert-concurrency.test.ts +4 -112
  84. package/src/__tests__/non-member-access-request.test.ts +2 -6
  85. package/src/__tests__/notification-guardian-path.test.ts +2 -6
  86. package/src/__tests__/oauth-cli.test.ts +364 -2
  87. package/src/__tests__/oauth2-gateway-transport.test.ts +18 -3
  88. package/src/__tests__/outlook-attachments.test.ts +301 -0
  89. package/src/__tests__/outlook-automation-tools.test.ts +425 -0
  90. package/src/__tests__/outlook-categories.test.ts +212 -0
  91. package/src/__tests__/outlook-client-automation.test.ts +246 -0
  92. package/src/__tests__/outlook-compose-tools.test.ts +325 -0
  93. package/src/__tests__/outlook-declutter-tools.test.ts +585 -0
  94. package/src/__tests__/outlook-email-watcher.test.ts +322 -0
  95. package/src/__tests__/outlook-follow-up.test.ts +196 -0
  96. package/src/__tests__/outlook-messaging-provider.test.ts +498 -3
  97. package/src/__tests__/outlook-trash.test.ts +77 -0
  98. package/src/__tests__/outlook-unsubscribe.test.ts +250 -0
  99. package/src/__tests__/platform-callback-registration.test.ts +4 -4
  100. package/src/__tests__/playbook-execution.test.ts +76 -80
  101. package/src/__tests__/playbook-tools.test.ts +5 -7
  102. package/src/__tests__/provider-error-scenarios.test.ts +21 -0
  103. package/src/__tests__/rebuild-index-graph-nodes.test.ts +273 -0
  104. package/src/__tests__/registry.test.ts +2 -2
  105. package/src/__tests__/require-fresh-approval.test.ts +64 -2
  106. package/src/__tests__/runtime-events-sse-parity.test.ts +2 -6
  107. package/src/__tests__/runtime-events-sse.test.ts +2 -6
  108. package/src/__tests__/schedule-store.test.ts +2 -6
  109. package/src/__tests__/schedule-tools.test.ts +2 -6
  110. package/src/__tests__/scheduler-recurrence.test.ts +1 -5
  111. package/src/__tests__/scoped-approval-grants.test.ts +2 -6
  112. package/src/__tests__/scoped-grant-security-matrix.test.ts +2 -6
  113. package/src/__tests__/search-skills-unified.test.ts +421 -0
  114. package/src/__tests__/secret-onetime-send.test.ts +2 -0
  115. package/src/__tests__/send-endpoint-busy.test.ts +2 -6
  116. package/src/__tests__/sequence-store.test.ts +2 -6
  117. package/src/__tests__/server-history-render.test.ts +2 -6
  118. package/src/__tests__/skill-feature-flags-integration.test.ts +38 -31
  119. package/src/__tests__/skill-feature-flags.test.ts +6 -6
  120. package/src/__tests__/skill-load-feature-flag.test.ts +11 -11
  121. package/src/__tests__/skill-memory.test.ts +140 -98
  122. package/src/__tests__/skills-uninstall.test.ts +2 -2
  123. package/src/__tests__/skills.test.ts +1 -1
  124. package/src/__tests__/slack-inbound-verification.test.ts +2 -6
  125. package/src/__tests__/task-compiler.test.ts +2 -6
  126. package/src/__tests__/task-management-tools.test.ts +2 -6
  127. package/src/__tests__/task-memory-cleanup.test.ts +173 -229
  128. package/src/__tests__/task-runner.test.ts +2 -6
  129. package/src/__tests__/task-scheduler.test.ts +2 -6
  130. package/src/__tests__/test-preload.ts +3 -0
  131. package/src/__tests__/tool-approval-handler.test.ts +2 -6
  132. package/src/__tests__/tool-grant-request-escalation.test.ts +2 -6
  133. package/src/__tests__/tool-side-effects-slack-dm.test.ts +276 -0
  134. package/src/__tests__/trust-store.test.ts +1 -1
  135. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -6
  136. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +2 -6
  137. package/src/__tests__/trusted-contact-multichannel.test.ts +2 -6
  138. package/src/__tests__/trusted-contact-verification.test.ts +2 -6
  139. package/src/__tests__/turn-boundary-resolution.test.ts +2 -6
  140. package/src/__tests__/usage-cache-backfill-migration.test.ts +1 -6
  141. package/src/__tests__/usage-routes.test.ts +2 -6
  142. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  143. package/src/__tests__/voice-invite-redemption.test.ts +2 -6
  144. package/src/__tests__/voice-scoped-grant-consumer.test.ts +2 -6
  145. package/src/__tests__/voice-session-bridge.test.ts +2 -6
  146. package/src/__tests__/volume-security-guard.test.ts +2 -0
  147. package/src/__tests__/workspace-lifecycle.test.ts +29 -1
  148. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +2 -6
  149. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +2 -6
  150. package/src/__tests__/workspace-migration-026-backfill-install-meta.test.ts +558 -0
  151. package/src/__tests__/workspace-policy.test.ts +1 -1
  152. package/src/agent/attachments.ts +7 -2
  153. package/src/agent/image-optimize.ts +165 -0
  154. package/src/agent/loop.ts +1 -15
  155. package/src/bundler/app-compiler.ts +179 -2
  156. package/src/bundler/package-resolver.ts +3 -5
  157. package/src/cli/__tests__/notifications.test.ts +1 -2
  158. package/src/cli/cli-memory.ts +67 -64
  159. package/src/cli/commands/avatar.ts +3 -3
  160. package/src/cli/commands/config.ts +26 -13
  161. package/src/cli/commands/doctor.ts +2 -2
  162. package/src/cli/commands/memory.ts +41 -55
  163. package/src/cli/commands/oauth/__tests__/connect.test.ts +2 -2
  164. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +2 -2
  165. package/src/cli/commands/oauth/__tests__/mode.test.ts +8 -1
  166. package/src/cli/commands/oauth/__tests__/status.test.ts +2 -2
  167. package/src/cli/commands/oauth/connect.ts +11 -6
  168. package/src/cli/commands/oauth/mode.ts +7 -0
  169. package/src/cli/commands/oauth/shared.ts +39 -3
  170. package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
  171. package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
  172. package/src/cli/commands/platform/__tests__/status.test.ts +5 -5
  173. package/src/cli/commands/platform/index.ts +16 -16
  174. package/src/cli/commands/skills.ts +88 -16
  175. package/src/cli/commands/trust.ts +2 -2
  176. package/src/cli/lib/daemon-credential-client.ts +2 -3
  177. package/src/config/bundled-skills/acp/TOOLS.json +1 -1
  178. package/src/config/bundled-skills/contacts/SKILL.md +0 -1
  179. package/src/config/bundled-skills/contacts/TOOLS.json +0 -8
  180. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +0 -4
  181. package/src/config/bundled-skills/gmail/SKILL.md +2 -10
  182. package/src/config/bundled-skills/google-calendar/SKILL.md +1 -9
  183. package/src/config/bundled-skills/messaging/SKILL.md +10 -18
  184. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +40 -33
  185. package/src/config/bundled-skills/outlook/SKILL.md +189 -0
  186. package/src/config/bundled-skills/outlook/TOOLS.json +530 -0
  187. package/src/config/bundled-skills/outlook/tools/outlook-attachments.ts +85 -0
  188. package/src/config/bundled-skills/outlook/tools/outlook-categories.ts +77 -0
  189. package/src/config/bundled-skills/outlook/tools/outlook-draft.ts +84 -0
  190. package/src/config/bundled-skills/outlook/tools/outlook-follow-up.ts +94 -0
  191. package/src/config/bundled-skills/outlook/tools/outlook-forward.ts +49 -0
  192. package/src/config/bundled-skills/outlook/tools/outlook-outreach-scan.ts +237 -0
  193. package/src/config/bundled-skills/outlook/tools/outlook-rules.ts +161 -0
  194. package/src/config/bundled-skills/outlook/tools/outlook-send-draft.ts +32 -0
  195. package/src/config/bundled-skills/outlook/tools/outlook-sender-digest.ts +272 -0
  196. package/src/config/bundled-skills/outlook/tools/outlook-trash.ts +29 -0
  197. package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +129 -0
  198. package/src/config/bundled-skills/outlook/tools/outlook-vacation.ts +87 -0
  199. package/src/config/bundled-skills/outlook/tools/shared.ts +20 -0
  200. package/src/config/bundled-skills/outlook-calendar/SKILL.md +51 -0
  201. package/src/config/bundled-skills/outlook-calendar/TOOLS.json +221 -0
  202. package/src/config/bundled-skills/outlook-calendar/calendar-client.ts +252 -0
  203. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-check-availability.ts +53 -0
  204. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-create-event.ts +74 -0
  205. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-get-event.ts +18 -0
  206. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-list-events.ts +46 -0
  207. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-rsvp.ts +36 -0
  208. package/src/config/bundled-skills/outlook-calendar/tools/shared.ts +17 -0
  209. package/src/config/bundled-skills/outlook-calendar/types.ts +120 -0
  210. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +47 -40
  211. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +16 -29
  212. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +16 -18
  213. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +39 -47
  214. package/src/config/bundled-skills/slack/SKILL.md +1 -7
  215. package/src/config/bundled-tool-registry.ts +56 -4
  216. package/src/config/env-registry.ts +15 -8
  217. package/src/config/feature-flag-registry.json +21 -124
  218. package/src/config/schemas/platform.ts +8 -0
  219. package/src/config/schemas/timeouts.ts +1 -1
  220. package/src/config/skills.ts +18 -7
  221. package/src/context/token-estimator.ts +25 -18
  222. package/src/context/window-manager.ts +6 -2
  223. package/src/credential-execution/process-manager.ts +3 -1
  224. package/src/daemon/context-overflow-reducer.ts +46 -2
  225. package/src/daemon/conversation-agent-loop-handlers.ts +123 -82
  226. package/src/daemon/conversation-agent-loop.ts +96 -61
  227. package/src/daemon/conversation-error.ts +31 -8
  228. package/src/daemon/conversation-lifecycle.ts +33 -0
  229. package/src/daemon/conversation-media-retry.ts +85 -7
  230. package/src/daemon/conversation-notifiers.ts +4 -1
  231. package/src/daemon/conversation-runtime-assembly.ts +5 -0
  232. package/src/daemon/conversation.ts +41 -2
  233. package/src/daemon/daemon-control.ts +8 -2
  234. package/src/daemon/handlers/shared.ts +22 -12
  235. package/src/daemon/handlers/skills.ts +416 -202
  236. package/src/daemon/lifecycle.ts +40 -1
  237. package/src/daemon/main.ts +5 -1
  238. package/src/daemon/message-types/conversations.ts +4 -1
  239. package/src/daemon/message-types/messages.ts +3 -1
  240. package/src/daemon/message-types/skills.ts +97 -36
  241. package/src/daemon/providers-setup.ts +5 -0
  242. package/src/daemon/server.ts +11 -2
  243. package/src/daemon/tool-side-effects.ts +27 -5
  244. package/src/heartbeat/heartbeat-service.ts +1 -0
  245. package/src/hooks/cli.ts +2 -2
  246. package/src/hooks/runner.ts +15 -38
  247. package/src/inbound/platform-callback-registration.ts +14 -14
  248. package/src/memory/admin.ts +11 -45
  249. package/src/memory/conversation-bootstrap.ts +2 -0
  250. package/src/memory/conversation-crud.ts +242 -348
  251. package/src/memory/conversation-group-migration.ts +157 -0
  252. package/src/memory/conversation-queries.ts +4 -2
  253. package/src/memory/db-init.ts +30 -3
  254. package/src/memory/embed.ts +73 -0
  255. package/src/memory/embedding-backend.ts +8 -14
  256. package/src/memory/embedding-runtime-manager.ts +12 -114
  257. package/src/memory/fingerprint.ts +2 -2
  258. package/src/memory/graph/bootstrap.ts +512 -0
  259. package/src/memory/graph/capability-seed.ts +297 -0
  260. package/src/memory/graph/consolidation.ts +691 -0
  261. package/src/memory/graph/conversation-graph-memory.ts +630 -0
  262. package/src/memory/graph/decay.test.ts +208 -0
  263. package/src/memory/graph/decay.ts +195 -0
  264. package/src/memory/graph/extraction-job.ts +69 -0
  265. package/src/memory/graph/extraction.test.ts +936 -0
  266. package/src/memory/graph/extraction.ts +1254 -0
  267. package/src/memory/graph/graph-search.ts +266 -0
  268. package/src/memory/graph/image-ref-utils.ts +29 -0
  269. package/src/memory/graph/injection.test.ts +513 -0
  270. package/src/memory/graph/injection.ts +439 -0
  271. package/src/memory/graph/inspect.ts +534 -0
  272. package/src/memory/graph/narrative.ts +267 -0
  273. package/src/memory/graph/pattern-scan.ts +269 -0
  274. package/src/memory/graph/retriever.ts +1008 -0
  275. package/src/memory/graph/scoring.test.ts +548 -0
  276. package/src/memory/graph/scoring.ts +232 -0
  277. package/src/memory/graph/serendipity.ts +65 -0
  278. package/src/memory/graph/store.test.ts +1050 -0
  279. package/src/memory/graph/store.ts +699 -0
  280. package/src/memory/graph/tool-handlers.ts +426 -0
  281. package/src/memory/graph/tools.ts +141 -0
  282. package/src/memory/graph/triggers.test.ts +487 -0
  283. package/src/memory/graph/triggers.ts +223 -0
  284. package/src/memory/graph/types.ts +271 -0
  285. package/src/memory/group-crud.ts +191 -0
  286. package/src/memory/indexer.ts +37 -19
  287. package/src/memory/job-handlers/cleanup.ts +0 -53
  288. package/src/memory/job-handlers/conversation-starters.ts +91 -53
  289. package/src/memory/job-handlers/embedding.ts +5 -31
  290. package/src/memory/job-handlers/index-maintenance.ts +23 -11
  291. package/src/memory/job-handlers/summarization.ts +32 -17
  292. package/src/memory/job-utils.ts +1 -1
  293. package/src/memory/jobs-store.ts +50 -70
  294. package/src/memory/jobs-worker.ts +147 -112
  295. package/src/memory/message-content.ts +1 -0
  296. package/src/memory/migrations/202-memory-graph-tables.ts +130 -0
  297. package/src/memory/migrations/203-drop-memory-items-tables.ts +23 -0
  298. package/src/memory/migrations/204-rename-memory-graph-type-values.ts +46 -0
  299. package/src/memory/migrations/205-memory-graph-image-refs.ts +11 -0
  300. package/src/memory/migrations/index.ts +4 -0
  301. package/src/memory/migrations/registry.ts +8 -0
  302. package/src/memory/qdrant-client.ts +44 -17
  303. package/src/memory/schema/index.ts +1 -0
  304. package/src/memory/schema/memory-graph.ts +139 -0
  305. package/src/memory/search/semantic.ts +47 -91
  306. package/src/memory/task-memory-cleanup.ts +28 -50
  307. package/src/messaging/providers/outlook/adapter.ts +8 -1
  308. package/src/messaging/providers/outlook/client.ts +299 -0
  309. package/src/messaging/providers/outlook/types.ts +118 -0
  310. package/src/notifications/adapters/macos.ts +1 -0
  311. package/src/notifications/copy-composer.ts +9 -0
  312. package/src/notifications/signal.ts +16 -0
  313. package/src/oauth/seed-providers.ts +2 -1
  314. package/src/permissions/checker.ts +24 -3
  315. package/src/permissions/defaults.ts +4 -4
  316. package/src/permissions/workspace-policy.ts +1 -1
  317. package/src/playbooks/playbook-compiler.ts +19 -18
  318. package/src/playbooks/types.ts +4 -3
  319. package/src/prompts/system-prompt.ts +3 -29
  320. package/src/providers/anthropic/client.ts +47 -19
  321. package/src/providers/gemini/client.ts +1 -1
  322. package/src/providers/openai/client.ts +1 -1
  323. package/src/providers/registry.ts +1 -1
  324. package/src/providers/retry.ts +19 -3
  325. package/src/runtime/actor-trust-resolver.ts +5 -1
  326. package/src/runtime/auth/route-policy.ts +7 -0
  327. package/src/runtime/guardian-reply-router.ts +5 -1
  328. package/src/runtime/http-server.ts +23 -3
  329. package/src/runtime/middleware/auth.ts +20 -0
  330. package/src/runtime/routes/attachment-routes.test.ts +106 -0
  331. package/src/runtime/routes/attachment-routes.ts +106 -16
  332. package/src/runtime/routes/brain-graph-routes.ts +21 -22
  333. package/src/runtime/routes/btw-routes.ts +8 -0
  334. package/src/runtime/routes/conversation-management-routes.ts +2 -0
  335. package/src/runtime/routes/conversation-starter-routes.ts +2 -2
  336. package/src/runtime/routes/debug-routes.ts +1 -1
  337. package/src/runtime/routes/global-search-routes.ts +21 -19
  338. package/src/runtime/routes/group-routes.ts +207 -0
  339. package/src/runtime/routes/guardian-action-routes.ts +21 -10
  340. package/src/runtime/routes/guardian-bootstrap-routes.ts +23 -19
  341. package/src/runtime/routes/inbound-message-handler.ts +19 -0
  342. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +292 -0
  343. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +207 -0
  344. package/src/runtime/routes/memory-item-routes.test.ts +2 -14
  345. package/src/runtime/routes/memory-item-routes.ts +341 -388
  346. package/src/runtime/routes/schedule-routes.ts +2 -0
  347. package/src/runtime/routes/skills-routes.ts +103 -37
  348. package/src/runtime/routes/work-items-routes.test.ts +2 -6
  349. package/src/schedule/scheduler.ts +8 -1
  350. package/src/security/oauth2.ts +1 -1
  351. package/src/security/secure-keys.ts +4 -8
  352. package/src/shared/provider-env-vars.ts +19 -0
  353. package/src/skills/catalog-cache.ts +5 -0
  354. package/src/skills/catalog-install.ts +15 -14
  355. package/src/skills/clawhub.ts +134 -154
  356. package/src/skills/install-meta.ts +208 -0
  357. package/src/skills/managed-store.ts +27 -16
  358. package/src/skills/skill-memory.ts +152 -77
  359. package/src/skills/skillssh-registry.ts +19 -17
  360. package/src/tasks/task-runner.ts +3 -1
  361. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -5
  362. package/src/tools/browser/runtime-check.ts +3 -1
  363. package/src/tools/memory/register.ts +63 -46
  364. package/src/tools/permission-checker.ts +7 -1
  365. package/src/tools/shared/filesystem/image-read.ts +22 -85
  366. package/src/tools/terminal/safe-env.ts +1 -0
  367. package/src/tools/tool-manifest.ts +3 -3
  368. package/src/util/browser.ts +25 -10
  369. package/src/util/bun-runtime.ts +172 -0
  370. package/src/watcher/providers/outlook-calendar.ts +343 -0
  371. package/src/watcher/providers/outlook.ts +198 -0
  372. package/src/workspace/migrations/025-remove-oauth-app-setup-skills.ts +76 -0
  373. package/src/workspace/migrations/026-backfill-install-meta.ts +325 -0
  374. package/src/workspace/migrations/027-remove-orphaned-optimized-images-cache.ts +42 -0
  375. package/src/workspace/migrations/registry.ts +6 -0
  376. package/src/__tests__/context-memory-e2e.test.ts +0 -415
  377. package/src/__tests__/journal-context.test.ts +0 -268
  378. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -297
  379. package/src/__tests__/memory-lifecycle-e2e.test.ts +0 -459
  380. package/src/__tests__/memory-query-builder.test.ts +0 -59
  381. package/src/__tests__/memory-recall-quality.test.ts +0 -1046
  382. package/src/__tests__/memory-regressions.experimental.test.ts +0 -629
  383. package/src/__tests__/memory-regressions.test.ts +0 -3696
  384. package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -295
  385. package/src/daemon/conversation-memory.ts +0 -207
  386. package/src/memory/conversation-starters-cadence.ts +0 -74
  387. package/src/memory/items-extractor.ts +0 -860
  388. package/src/memory/job-handlers/batch-extraction.ts +0 -753
  389. package/src/memory/job-handlers/extraction.ts +0 -40
  390. package/src/memory/job-handlers/journal-carry-forward.test.ts +0 -355
  391. package/src/memory/job-handlers/journal-carry-forward.ts +0 -255
  392. package/src/memory/journal-memory.ts +0 -224
  393. package/src/memory/query-builder.ts +0 -47
  394. package/src/memory/query-expansion.ts +0 -83
  395. package/src/memory/retriever.test.ts +0 -1592
  396. package/src/memory/retriever.ts +0 -1331
  397. package/src/memory/search/formatting.test.ts +0 -140
  398. package/src/memory/search/formatting.ts +0 -262
  399. package/src/memory/search/mmr.ts +0 -139
  400. package/src/memory/search/ranking.ts +0 -15
  401. package/src/memory/search/staleness.ts +0 -40
  402. package/src/memory/search/tier-classifier.ts +0 -18
  403. package/src/memory/search/types.ts +0 -121
  404. package/src/prompts/journal-context.ts +0 -154
  405. package/src/tools/memory/definitions.ts +0 -69
  406. package/src/tools/memory/handlers.test.ts +0 -562
  407. package/src/tools/memory/handlers.ts +0 -434
@@ -1,860 +0,0 @@
1
- import { and, desc, eq, like, sql } from "drizzle-orm";
2
- import { v4 as uuid } from "uuid";
3
-
4
- import { getConfig } from "../config/loader.js";
5
- import type { MemoryExtractionConfig } from "../config/types.js";
6
- import { getAssistantName } from "../daemon/identity-helpers.js";
7
- import { resolveGuardianPersona } from "../prompts/persona-resolver.js";
8
- import { buildCoreIdentityContext } from "../prompts/system-prompt.js";
9
- import {
10
- extractToolUse,
11
- getConfiguredProvider,
12
- userMessage,
13
- } from "../providers/provider-send-message.js";
14
- import { BackendUnavailableError, ProviderError } from "../util/errors.js";
15
- import { getLogger } from "../util/logger.js";
16
- import { truncate } from "../util/truncate.js";
17
- import { maybeEnqueueConversationStartersJob } from "./conversation-starters-cadence.js";
18
- import { getDb } from "./db.js";
19
- import { computeMemoryFingerprint } from "./fingerprint.js";
20
- import { enqueueMemoryJob } from "./jobs-store.js";
21
- import { upsertJournalMemoriesFromDisk } from "./journal-memory.js";
22
- import { extractTextFromStoredMessageContent } from "./message-content.js";
23
- import { withQdrantBreaker } from "./qdrant-circuit-breaker.js";
24
- import { getQdrantClient } from "./qdrant-client.js";
25
- import { memoryItems, memoryItemSources, messages } from "./schema.js";
26
- import { isConversationFailed } from "./task-memory-cleanup.js";
27
- import { clampUnitInterval } from "./validation.js";
28
-
29
- const log = getLogger("memory-items-extractor");
30
-
31
- export type MemoryItemKind =
32
- | "identity"
33
- | "preference"
34
- | "project"
35
- | "decision"
36
- | "constraint"
37
- | "event"
38
- | "journal";
39
-
40
- export type OverrideConfidence = "explicit" | "tentative" | "inferred";
41
-
42
- export interface ExtractedItem {
43
- kind: MemoryItemKind;
44
- subject: string;
45
- statement: string;
46
- confidence: number;
47
- importance: number;
48
- fingerprint: string;
49
- supersedes: string | null;
50
- overrideConfidence: OverrideConfidence;
51
- /** True when the LLM emitted a supersedes ID that was rejected (hallucinated). */
52
- supersedesRejected?: boolean;
53
- /** Which speaker's message this item was extracted from (batch extraction only). */
54
- sourceRole?: "user" | "assistant";
55
- }
56
-
57
- export const VALID_KINDS = new Set<string>([
58
- "identity",
59
- "preference",
60
- "project",
61
- "decision",
62
- "constraint",
63
- "event",
64
- "journal",
65
- ]);
66
-
67
- /**
68
- * Kinds the LLM is allowed to produce during extraction. Excludes "journal"
69
- * because journal memories are created directly from disk files — any
70
- * LLM-produced journal items would be silently dropped, wasting tokens.
71
- */
72
- export const EXTRACTION_KINDS = [...VALID_KINDS].filter((k) => k !== "journal");
73
-
74
- /** Maps old kind names to their new equivalents for graceful migration. */
75
- export const KIND_MIGRATION_MAP: Record<string, MemoryItemKind> = {
76
- profile: "identity",
77
- fact: "identity",
78
- relationship: "identity",
79
- opinion: "preference",
80
- todo: "project",
81
- instruction: "constraint",
82
- style: "preference",
83
- };
84
-
85
- export const SUPERSEDE_KINDS = new Set<MemoryItemKind>([
86
- "identity",
87
- "preference",
88
- "project",
89
- "decision",
90
- "constraint",
91
- ]);
92
-
93
- // ── LLM-powered extraction ────────────────────────────────────────────
94
-
95
- // Budget for the extraction system prompt (in characters). This is a
96
- // conservative estimate that fits comfortably within even small model
97
- // context windows (latency-optimized models like Haiku). The remaining
98
- // context budget is consumed by the user message, tool schema, and response
99
- // tokens. ~6000 tokens ≈ 24 000 chars is a safe ceiling.
100
- const EXTRACTION_SYSTEM_PROMPT_CHAR_BUDGET = 24_000;
101
-
102
- export function buildExtractionSystemPrompt(
103
- existingItems: Array<{
104
- id: string;
105
- kind: string;
106
- subject: string;
107
- statement: string;
108
- }>,
109
- messageRole: string,
110
- userPersona?: string | null,
111
- ): string {
112
- // Build the fixed instruction body first so we can measure it and allocate
113
- // the remaining budget to identity context.
114
- let instructions = `You are a memory extraction system. Given a message from a conversation, extract structured memory items that would be valuable to remember for future interactions.
115
-
116
- Extract items in these categories:
117
- - identity: Personal info (name, role, location, timezone, background), notable facts, relationships between people/teams/systems
118
- - preference: User likes, dislikes, preferred approaches/tools/styles, communication style patterns, opinions and evaluations
119
- - project: Project names, repos, tech stacks, architecture details, action items, follow-ups, things to do later
120
- - decision: Choices made, approaches selected, trade-offs resolved
121
- - constraint: Rules, requirements, things that must/must not be done, explicit directives on how the assistant should behave
122
- - event: Moments that mattered — turning points, breakthroughs, significant shared experiences, deadlines, milestones
123
-
124
- For each item, provide:
125
- - kind: One of the categories above
126
- - subject: A short label (2-8 words) identifying what this is about
127
- - statement: A relationship-rich statement to remember (1-2 sentences). Include context about *why this mattered*, not just what happened. Write statements your future self would recognize as significant, not just accurate. Include relational context — who recommended it, why it matters, how it connects to other facts. For example, write "Data processing library that Sarah from Marketing recommended for the Q4 pipeline rewrite" instead of just "Uses pandas".
128
- - confidence: How confident you are this is accurate (0.0-1.0)
129
- - importance: How valuable this is to remember (0.0-1.0). Not all facts are equally worth remembering — some moments are load-bearing.
130
- - 1.0: Moments that changed the working relationship, breakthroughs, hard-won lessons, turning points
131
- - 0.8-0.9: Significant decisions, strong preferences, personal facts that shape how you work together
132
- - 0.6-0.7: Project details, constraints, opinions, recurring patterns
133
- - 0.3-0.5: Contextual details, minor preferences, transient facts
134
- - 0.1-0.2: Mundane facts (timezone, editor choice) — worth storing but shouldn't dominate retrieval
135
- - supersedes: If this item replaces an existing memory item, set this to the ID of the item it replaces. Use null if it does not replace anything. Determine supersession by understanding the semantic meaning — do not rely on keyword matching.
136
- - overrideConfidence: How confident you are that this overrides an existing item:
137
- - "explicit": Clear override signal (e.g., "Actually I now prefer X", "I changed my mind about Y", "We switched from A to B")
138
- - "tentative": Ambiguous — the new information might override the old, but it's not certain
139
- - "inferred": Weak signal — possibly related to an existing item but no clear override intent
140
-
141
- Rules:
142
- - Only extract genuinely memorable information. Skip pleasantries, filler, and transient discussion.
143
- - Do NOT extract information about what tools the assistant used or what files it read — only extract substantive facts about the user, their projects, and their preferences.
144
- - Do NOT extract raw code snippets, JSON fragments, YAML, configuration values, log output, or data structures. Only extract the human-readable meaning or intent behind such content, not the literal syntax.
145
- - Prefer fewer high-quality items over many low-quality ones.
146
- - If the message contains no memorable information, return an empty array.
147
- - The preceding conversation context (if provided) is for disambiguation only. Extract items ONLY from the final message after the --- separator, not from the context messages.
148
- - When superseding an existing memory, capture what shifted — the old state, the new state, and what prompted the change. The evolution is as valuable as the current state.`;
149
-
150
- // Try to extract user name from persona text
151
- let userName = "the user";
152
- if (userPersona) {
153
- const nameMatch = userPersona.match(/Preferred name\/reference:\s*(.+)/);
154
- if (nameMatch) {
155
- userName = nameMatch[1].trim();
156
- }
157
- }
158
-
159
- if (messageRole === "assistant") {
160
- instructions += `
161
-
162
- IMPORTANT: The message below is from the ASSISTANT. You may extract facts about actions taken, decisions made, and outcomes achieved. However, do NOT attribute the assistant's own identity, personality, or self-descriptions to the user. If the assistant is just introducing itself or expressing uncertainty about its own nature, extract nothing.`;
163
- }
164
-
165
- instructions += `
166
-
167
- ## Examples
168
-
169
- Good extractions from user messages:
170
- - "I'm a backend engineer at Acme Corp, mostly working with Go and PostgreSQL"
171
- → kind: identity, subject: "Role at Acme Corp", statement: "${userName} is a backend engineer at Acme Corp, works primarily with Go and PostgreSQL"
172
-
173
- - "Always use semantic commits in this repo. I hate squash merges."
174
- → kind: constraint, subject: "Git conventions", statement: "${userName} requires semantic commit messages. Strongly dislikes squash merges."
175
-
176
- - "We decided to go with Redis for the cache layer because DynamoDB was too expensive at our read volume"
177
- → kind: decision, subject: "Cache layer choice", statement: "${userName} chose Redis over DynamoDB for caching due to cost at high read volumes"
178
-
179
- - "We pair-debugged a 4-hour outage and found the bug was a missing semicolon — user said it was the most frustrating and satisfying debug session"
180
- → kind: event, importance: 0.95, subject: "4-hour outage debug session", statement: "${userName} and assistant pair-debugged a 4-hour outage caused by a missing semicolon — ${userName} described it as the most frustrating and satisfying debug session"
181
-
182
- - "User scrapped the entire approach mid-project and the rewrite turned out way better"
183
- → kind: decision, importance: 0.9, subject: "Architecture rewrite decision", statement: "${userName} scrapped the entire approach mid-project and rewrote it — the rewrite turned out significantly better, validating the decision to start over"
184
-
185
- Good extractions from assistant messages:
186
- - "Based on your earlier mention, I see you're using Next.js 14 with the app router for the dashboard project."
187
- → kind: project, subject: "Dashboard tech stack", statement: "${userName}'s dashboard project uses Next.js 14 with the app router"
188
-
189
- - "Since you mentioned your team follows trunk-based development, I'll keep the changes in a single commit."
190
- → kind: constraint, subject: "Team branching strategy", statement: "${userName}'s team follows trunk-based development"
191
-
192
- - "I've refactored the auth middleware to use JWT validation and added rate limiting to the login endpoint."
193
- → kind: project, subject: "Auth middleware changes", statement: "Auth middleware was refactored to use JWT validation with rate limiting on the login endpoint"
194
-
195
- Do NOT extract:
196
- - "I'll check that file for you" → assistant operational statement with no lasting information
197
- - "I think the best approach would be to refactor this" → speculative, no action taken yet
198
- - "The tests passed" → transient status
199
- - "Sure, sounds good" → filler
200
- - "\`\`\`json {"key": "val"} \`\`\`" → raw code/data, extract meaning not syntax
201
-
202
- Low importance (still worth storing):
203
- - "User uses VS Code" → kind: preference, importance: 0.15, subject: "Editor choice", statement: "${userName} uses VS Code as their editor"`;
204
-
205
- if (existingItems.length > 0) {
206
- instructions += `\n\nExisting memory items (use these to identify supersession targets — set \`supersedes\` to the item ID if the new information replaces one of these):\n`;
207
- for (const item of existingItems) {
208
- instructions += `- [${item.id}] (${item.kind}) ${item.subject}: ${item.statement}\n`;
209
- }
210
- }
211
-
212
- // Inject identity context so extracted memories use real names instead of
213
- // generic "User ..." labels. Budget is dynamically computed: whatever
214
- // remains after the fixed instructions fits within the system prompt
215
- // ceiling, preventing oversized prompts from exceeding the provider input
216
- // window (which would cause sendMessage to error).
217
- const rawIdentityContext = buildCoreIdentityContext(
218
- userPersona ? { userPersona } : undefined,
219
- );
220
-
221
- let prompt = "";
222
- if (rawIdentityContext) {
223
- // Reserve space for the wrapping text: "# Identity Context\n\n" + "\n\n---\n\n"
224
- const wrapperOverhead = "# Identity Context\n\n\n\n---\n\n".length;
225
- const identityBudget =
226
- EXTRACTION_SYSTEM_PROMPT_CHAR_BUDGET -
227
- instructions.length -
228
- wrapperOverhead;
229
-
230
- if (identityBudget > 0) {
231
- const identityContext = truncate(
232
- rawIdentityContext,
233
- identityBudget,
234
- "\n...[identity context truncated]",
235
- );
236
- prompt += `# Identity Context\n\n${identityContext}\n\n---\n\n`;
237
- }
238
- }
239
-
240
- prompt += instructions;
241
- return prompt;
242
- }
243
-
244
- export const VALID_OVERRIDE_CONFIDENCES = new Set<string>([
245
- "explicit",
246
- "tentative",
247
- "inferred",
248
- ]);
249
-
250
- interface LLMExtractedItem {
251
- kind: string;
252
- subject: string;
253
- statement: string;
254
- confidence: number;
255
- importance: number;
256
- supersedes: string | null;
257
- overrideConfidence: string;
258
- }
259
-
260
- /**
261
- * Query top-10 active items by kind + subject similarity to give the
262
- * extraction LLM awareness of existing items it might supersede.
263
- * This is a write-path-only heuristic — not used at read time.
264
- */
265
- export function queryExistingItemsForContext(
266
- scopeId: string,
267
- text: string,
268
- ): Array<{ id: string; kind: string; subject: string; statement: string }> {
269
- const db = getDb();
270
-
271
- // Extract a rough subject prefix from the first few words of the text
272
- const words = text.trim().split(/\s+/).slice(0, 3).join(" ");
273
- // Escape LIKE wildcards so user text with % or _ doesn't alter query semantics
274
- const escaped = words.replace(/%/g, "").replace(/_/g, "");
275
- const subjectPrefix = escaped.length > 0 ? `${escaped}%` : "%";
276
-
277
- // Query active items matching subject prefix, limited to 10
278
- const rows = db
279
- .select({
280
- id: memoryItems.id,
281
- kind: memoryItems.kind,
282
- subject: memoryItems.subject,
283
- statement: memoryItems.statement,
284
- })
285
- .from(memoryItems)
286
- .where(
287
- and(
288
- eq(memoryItems.scopeId, scopeId),
289
- eq(memoryItems.status, "active"),
290
- like(memoryItems.subject, subjectPrefix),
291
- ),
292
- )
293
- .limit(10)
294
- .all();
295
-
296
- // If prefix match yielded few results, backfill with recent active items
297
- if (rows.length < 10) {
298
- const existingIds = new Set(rows.map((r) => r.id));
299
- const backfill = db
300
- .select({
301
- id: memoryItems.id,
302
- kind: memoryItems.kind,
303
- subject: memoryItems.subject,
304
- statement: memoryItems.statement,
305
- })
306
- .from(memoryItems)
307
- .where(
308
- and(eq(memoryItems.scopeId, scopeId), eq(memoryItems.status, "active")),
309
- )
310
- .limit(10 - rows.length)
311
- .all();
312
-
313
- for (const row of backfill) {
314
- if (!existingIds.has(row.id)) {
315
- rows.push(row);
316
- existingIds.add(row.id);
317
- }
318
- }
319
- }
320
-
321
- return rows;
322
- }
323
-
324
- async function extractItemsWithLLM(
325
- text: string,
326
- extractionConfig: MemoryExtractionConfig,
327
- scopeId: string,
328
- messageRole: string,
329
- precedingMessages: Array<{ role: string; content: string }>,
330
- userPersona?: string | null,
331
- ): Promise<ExtractedItem[]> {
332
- const provider = await getConfiguredProvider();
333
- if (!provider) {
334
- throw new BackendUnavailableError(
335
- "Provider unavailable for memory extraction",
336
- );
337
- }
338
-
339
- // Query existing items to give the LLM supersession context
340
- const existingItems = queryExistingItemsForContext(scopeId, text);
341
- const systemPrompt = buildExtractionSystemPrompt(
342
- existingItems,
343
- messageRole,
344
- userPersona,
345
- );
346
-
347
- const assistantName = getAssistantName() ?? "the assistant";
348
- const messagePrefix =
349
- messageRole === "assistant"
350
- ? `[This message is from ${assistantName}]\n\n`
351
- : `[This message is from the user]\n\n`;
352
-
353
- // Build user content with optional preceding conversation context
354
- const contextParts: string[] = [];
355
- for (const msg of precedingMessages) {
356
- const msgText = extractTextFromStoredMessageContent(msg.content);
357
- if (msgText.length === 0) continue;
358
- const roleLabel =
359
- msg.role === "assistant"
360
- ? (getAssistantName() ?? "assistant")
361
- : "user";
362
- contextParts.push(`[${roleLabel}]: ${msgText}`);
363
- }
364
- let userContent = `${messagePrefix}${text}`;
365
- if (contextParts.length > 0) {
366
- userContent = `Preceding conversation context:\n${contextParts.join("\n\n")}\n\n---\n\nMessage to extract from:\n${messagePrefix}${text}`;
367
- }
368
-
369
- const response = await provider.sendMessage(
370
- [userMessage(userContent)],
371
- [
372
- {
373
- name: "store_memory_items",
374
- description: "Store extracted memory items from the message",
375
- input_schema: {
376
- type: "object" as const,
377
- properties: {
378
- items: {
379
- type: "array",
380
- items: {
381
- type: "object",
382
- properties: {
383
- kind: {
384
- type: "string",
385
- enum: EXTRACTION_KINDS,
386
- description: "Category of memory item",
387
- },
388
- subject: {
389
- type: "string",
390
- description:
391
- "Short label (2-8 words) for what this is about",
392
- },
393
- statement: {
394
- type: "string",
395
- description:
396
- "Relationship-rich factual statement to remember (1-2 sentences). Include relational context.",
397
- },
398
- confidence: {
399
- type: "number",
400
- description: "Confidence that this is accurate (0.0-1.0)",
401
- },
402
- importance: {
403
- type: "number",
404
- description: "How valuable this is to remember (0.0-1.0)",
405
- },
406
- supersedes: {
407
- type: ["string", "null"],
408
- description:
409
- "ID of the existing memory item this replaces, or null if not replacing anything",
410
- },
411
- overrideConfidence: {
412
- type: "string",
413
- enum: ["explicit", "tentative", "inferred"],
414
- description:
415
- "How confident you are that this overrides an existing item: explicit (clear override), tentative (ambiguous), inferred (weak signal)",
416
- },
417
- },
418
- required: [
419
- "kind",
420
- "subject",
421
- "statement",
422
- "confidence",
423
- "importance",
424
- "supersedes",
425
- "overrideConfidence",
426
- ],
427
- },
428
- },
429
- },
430
- required: ["items"],
431
- },
432
- },
433
- ],
434
- systemPrompt,
435
- {
436
- config: {
437
- modelIntent: "quality-optimized" as const,
438
- tool_choice: { type: "tool" as const, name: "store_memory_items" },
439
- },
440
- },
441
- );
442
-
443
- const toolBlock = extractToolUse(response);
444
- if (!toolBlock) {
445
- throw new ProviderError(
446
- "No tool_use block in LLM extraction response",
447
- "unknown",
448
- 502,
449
- );
450
- }
451
-
452
- const input = toolBlock.input as { items?: LLMExtractedItem[] };
453
- if (!Array.isArray(input.items)) {
454
- throw new ProviderError(
455
- "Invalid items structure in LLM extraction response",
456
- "unknown",
457
- 502,
458
- );
459
- }
460
-
461
- // Build set of known existing item IDs for supersession validation
462
- const existingItemIds = new Set(existingItems.map((e) => e.id));
463
-
464
- const items: ExtractedItem[] = [];
465
- for (const raw of input.items) {
466
- // Apply kind migration map for old kind names, then validate
467
- const resolvedKind = KIND_MIGRATION_MAP[raw.kind] ?? raw.kind;
468
- if (resolvedKind === "journal") continue; // journal memories created directly from disk
469
- if (!VALID_KINDS.has(resolvedKind)) continue;
470
- if (!raw.subject || !raw.statement) continue;
471
- const subject = String(raw.subject).trim();
472
- const statement = String(raw.statement).trim();
473
- const confidence = clampUnitInterval(parseScore(raw.confidence, 0.5));
474
- const importance = clampUnitInterval(parseScore(raw.importance, 0.5));
475
- const fingerprint = computeMemoryFingerprint(
476
- scopeId,
477
- resolvedKind,
478
- subject,
479
- statement,
480
- );
481
-
482
- // Validate supersedes: must reference a known existing item ID.
483
- // Reject hallucinated IDs that don't match any item we showed the LLM.
484
- const rawSupersedes =
485
- typeof raw.supersedes === "string" && raw.supersedes.length > 0
486
- ? raw.supersedes
487
- : null;
488
- const supersedes =
489
- rawSupersedes && existingItemIds.has(rawSupersedes)
490
- ? rawSupersedes
491
- : null;
492
- const supersedesRejected = !!rawSupersedes && !supersedes;
493
- const overrideConfidence = VALID_OVERRIDE_CONFIDENCES.has(
494
- raw.overrideConfidence,
495
- )
496
- ? (raw.overrideConfidence as OverrideConfidence)
497
- : "inferred";
498
-
499
- items.push({
500
- kind: resolvedKind as MemoryItemKind,
501
- subject,
502
- statement,
503
- confidence,
504
- importance,
505
- fingerprint,
506
- supersedes,
507
- overrideConfidence,
508
- supersedesRejected,
509
- });
510
- }
511
-
512
- return deduplicateItems(items);
513
- }
514
-
515
- /**
516
- * Fire conversation starters generation when journal memories were created.
517
- * Wrapped in try/catch so failures never propagate to the caller.
518
- */
519
- function triggerConversationStartersIfNeeded(
520
- count: number,
521
- scopeId: string,
522
- ): void {
523
- if (count <= 0) return;
524
- try {
525
- maybeEnqueueConversationStartersJob(scopeId);
526
- } catch (err) {
527
- log.warn(
528
- { err: err instanceof Error ? err.message : String(err) },
529
- "Failed to check conversation starters cadence",
530
- );
531
- }
532
- }
533
-
534
- // ── Public API ─────────────────────────────────────────────────────────
535
-
536
- export async function extractAndUpsertMemoryItemsForMessage(
537
- messageId: string,
538
- scopeId?: string,
539
- conversationId?: string,
540
- ): Promise<number> {
541
- const db = getDb();
542
- const message = db
543
- .select({
544
- id: messages.id,
545
- role: messages.role,
546
- content: messages.content,
547
- createdAt: messages.createdAt,
548
- conversationId: messages.conversationId,
549
- })
550
- .from(messages)
551
- .where(eq(messages.id, messageId))
552
- .get();
553
-
554
- if (!message) return 0;
555
-
556
- // Fetch up to 6 preceding messages from the same conversation for
557
- // disambiguation context (e.g. resolving "that framework" or "yes, do it").
558
- const effectiveConversationId = conversationId ?? message.conversationId;
559
- const precedingMessages = effectiveConversationId
560
- ? db
561
- .select({ role: messages.role, content: messages.content })
562
- .from(messages)
563
- .where(
564
- and(
565
- eq(messages.conversationId, effectiveConversationId),
566
- sql`${messages.createdAt} < ${message.createdAt}`,
567
- ),
568
- )
569
- .orderBy(desc(messages.createdAt))
570
- .limit(6)
571
- .all()
572
- .reverse()
573
- : [];
574
-
575
- const effectiveScopeId = scopeId ?? "default";
576
-
577
- // Directly create journal memories from any journal files written during
578
- // this message, bypassing LLM extraction (which would summarize/rewrite them).
579
- // This must run before the extraction guards (semantic density, useLLM, etc.)
580
- // because journal disk scanning is independent of LLM extraction.
581
- let journalUpserted = 0;
582
- if (message.role === "assistant") {
583
- journalUpserted = upsertJournalMemoriesFromDisk(
584
- message.createdAt,
585
- effectiveScopeId,
586
- messageId,
587
- );
588
- }
589
-
590
- const text = extractTextFromStoredMessageContent(message.content);
591
- if (text.length === 0) {
592
- triggerConversationStartersIfNeeded(journalUpserted, effectiveScopeId);
593
- return journalUpserted;
594
- }
595
-
596
- const config = getConfig();
597
- const extractionConfig = config.memory.extraction;
598
-
599
- // Resolve the guardian's persona to provide personality-aware extraction
600
- // context. Currently uses the guardian persona for all conversations —
601
- // non-guardian conversations are rare and the guardian's persona provides
602
- // better extraction context than none.
603
- const userPersona = resolveGuardianPersona();
604
-
605
- if (!extractionConfig.useLLM) {
606
- triggerConversationStartersIfNeeded(journalUpserted, effectiveScopeId);
607
- return journalUpserted;
608
- }
609
-
610
- const extracted = await extractItemsWithLLM(
611
- text,
612
- extractionConfig,
613
- effectiveScopeId,
614
- message.role,
615
- precedingMessages,
616
- userPersona,
617
- );
618
-
619
- if (extracted.length === 0) {
620
- triggerConversationStartersIfNeeded(journalUpserted, effectiveScopeId);
621
- return journalUpserted;
622
- }
623
-
624
- // Guard: re-check after the async LLM call. The event loop yields during
625
- // extractItemsWithLLM, so another task could have marked the conversation
626
- // as failed in the meantime. Bail before writing to the DB.
627
- if (conversationId && isConversationFailed(conversationId)) {
628
- log.info(
629
- { messageId, conversationId },
630
- "Skipping upsert — conversation marked failed during extraction",
631
- );
632
- triggerConversationStartersIfNeeded(journalUpserted, effectiveScopeId);
633
- return journalUpserted;
634
- }
635
-
636
- let upserted = 0;
637
- for (const item of extracted) {
638
- const now = Date.now();
639
- const seenAt = message.createdAt;
640
- const existing = db
641
- .select()
642
- .from(memoryItems)
643
- .where(
644
- and(
645
- eq(memoryItems.fingerprint, item.fingerprint),
646
- eq(memoryItems.scopeId, effectiveScopeId),
647
- ),
648
- )
649
- .get();
650
-
651
- let memoryItemId: string;
652
- let effectiveStatus: string = "active";
653
- if (existing) {
654
- memoryItemId = existing.id;
655
- effectiveStatus = "active";
656
- // Preserve sourceType for tool-sourced items — extraction should not
657
- // demote items the user explicitly saved.
658
- const effectiveSourceType =
659
- existing.sourceType === "tool" ? "tool" : "extraction";
660
-
661
- // Dual-write verificationState alongside sourceType for client compat.
662
- // Promote from assistant_inferred → user_reported when re-seen from user.
663
- const effectiveVerificationState =
664
- message.role === "user" || existing.verificationState === "user_reported"
665
- ? "user_reported"
666
- : existing.verificationState === "user_confirmed"
667
- ? "user_confirmed"
668
- : "assistant_inferred";
669
-
670
- db.update(memoryItems)
671
- .set({
672
- status: effectiveStatus,
673
- confidence: clampUnitInterval(
674
- Math.max(existing.confidence, item.confidence),
675
- ),
676
- importance: clampUnitInterval(
677
- Math.max(existing.importance ?? 0, item.importance),
678
- ),
679
- lastSeenAt: Math.max(existing.lastSeenAt, seenAt),
680
- sourceType: effectiveSourceType,
681
- sourceMessageRole: message.role,
682
- verificationState: effectiveVerificationState,
683
- })
684
- .where(eq(memoryItems.id, existing.id))
685
- .run();
686
- } else {
687
- memoryItemId = uuid();
688
- db.insert(memoryItems)
689
- .values({
690
- id: memoryItemId,
691
- kind: item.kind,
692
- subject: item.subject,
693
- statement: item.statement,
694
- status: "active",
695
- confidence: item.confidence,
696
- importance: item.importance,
697
- fingerprint: item.fingerprint,
698
- sourceType: "extraction",
699
- sourceMessageRole: message.role,
700
- // Dual-write verificationState for client compat
701
- verificationState:
702
- message.role === "user" ? "user_reported" : "assistant_inferred",
703
- scopeId: effectiveScopeId,
704
- firstSeenAt: message.createdAt,
705
- lastSeenAt: seenAt,
706
- lastUsedAt: null,
707
- supersedes: item.supersedes,
708
- overrideConfidence: item.overrideConfidence,
709
- })
710
- .run();
711
- upserted += 1;
712
- }
713
-
714
- // Handle LLM-directed supersession based on overrideConfidence.
715
- // Guard: skip if supersedes targets the current item (self-supersession on
716
- // fingerprint re-hit would incorrectly remove an active memory).
717
- if (
718
- item.supersedes &&
719
- item.supersedes !== memoryItemId &&
720
- item.overrideConfidence === "explicit" &&
721
- effectiveStatus === "active"
722
- ) {
723
- // Explicit supersession: mark old item as superseded and link both items
724
- const oldItem = db
725
- .select({ id: memoryItems.id })
726
- .from(memoryItems)
727
- .where(
728
- and(
729
- eq(memoryItems.id, item.supersedes),
730
- eq(memoryItems.scopeId, effectiveScopeId),
731
- eq(memoryItems.status, "active"),
732
- ),
733
- )
734
- .get();
735
-
736
- if (oldItem) {
737
- db.update(memoryItems)
738
- .set({
739
- status: "superseded",
740
- supersededBy: memoryItemId,
741
- })
742
- .where(eq(memoryItems.id, oldItem.id))
743
- .run();
744
-
745
- // Update new item's supersedes link
746
- db.update(memoryItems)
747
- .set({ supersedes: oldItem.id })
748
- .where(eq(memoryItems.id, memoryItemId))
749
- .run();
750
-
751
- // Remove superseded item from Qdrant vector index
752
- try {
753
- const qdrant = getQdrantClient();
754
- await withQdrantBreaker(() =>
755
- qdrant.deleteByTarget("item", oldItem.id),
756
- );
757
- } catch (err) {
758
- const errMsg = err instanceof Error ? err.message : String(err);
759
- log.warn(
760
- { err: errMsg, oldItemId: oldItem.id },
761
- "Failed to remove superseded item from Qdrant — will be cleaned up by index maintenance",
762
- );
763
- }
764
-
765
- log.debug(
766
- { newItemId: memoryItemId, oldItemId: oldItem.id },
767
- "Explicitly superseded memory item",
768
- );
769
- }
770
- } else if (item.supersedes && item.overrideConfidence === "tentative") {
771
- // Tentative: insert as active but don't supersede — both coexist
772
- log.debug(
773
- {
774
- newItemId: memoryItemId,
775
- supersedes: item.supersedes,
776
- overrideConfidence: "tentative",
777
- },
778
- "Tentative override — both items coexist",
779
- );
780
- } else if (item.supersedes && item.overrideConfidence === "inferred") {
781
- // Inferred: insert as active, don't supersede, log for observability
782
- log.debug(
783
- {
784
- newItemId: memoryItemId,
785
- supersedes: item.supersedes,
786
- overrideConfidence: "inferred",
787
- },
788
- "Inferred override — both items coexist (weak signal)",
789
- );
790
- }
791
-
792
- // Fallback subject-match supersession: only when the LLM did not
793
- // explicitly handle supersession for this item. Skip items whose
794
- // supersedes ID was rejected (hallucinated) — they should coexist,
795
- // not trigger subject-based replacement.
796
- if (
797
- !item.supersedes &&
798
- !item.supersedesRejected &&
799
- SUPERSEDE_KINDS.has(item.kind) &&
800
- effectiveStatus === "active"
801
- ) {
802
- db.update(memoryItems)
803
- .set({ status: "superseded" })
804
- .where(
805
- and(
806
- eq(memoryItems.kind, item.kind),
807
- eq(memoryItems.subject, item.subject),
808
- eq(memoryItems.status, "active"),
809
- eq(memoryItems.scopeId, effectiveScopeId),
810
- sql`${memoryItems.id} <> ${memoryItemId}`,
811
- ),
812
- )
813
- .run();
814
- }
815
-
816
- db.insert(memoryItemSources)
817
- .values({
818
- memoryItemId,
819
- messageId,
820
- evidence: item.statement,
821
- createdAt: now,
822
- })
823
- .onConflictDoNothing()
824
- .run();
825
-
826
- enqueueMemoryJob("embed_item", { itemId: memoryItemId });
827
- }
828
-
829
- upserted += journalUpserted;
830
-
831
- log.debug(
832
- { messageId, extracted: extracted.length, upserted },
833
- "Extracted memory items from message",
834
- );
835
-
836
- // Trigger conversation starters generation when new items are upserted
837
- triggerConversationStartersIfNeeded(upserted, effectiveScopeId);
838
-
839
- return upserted;
840
- }
841
-
842
- // ── Helpers ────────────────────────────────────────────────────────────
843
-
844
- export function deduplicateItems(items: ExtractedItem[]): ExtractedItem[] {
845
- const seen = new Set<string>();
846
- const unique: ExtractedItem[] = [];
847
- for (const item of items) {
848
- if (seen.has(item.fingerprint)) continue;
849
- seen.add(item.fingerprint);
850
- unique.push(item);
851
- }
852
- return unique;
853
- }
854
-
855
- /** Parse a score value, returning `fallback` for null, undefined, empty strings, and non-finite numbers. */
856
- export function parseScore(value: unknown, fallback: number): number {
857
- if (value == null || value === "") return fallback;
858
- const n = Number(value);
859
- return Number.isFinite(n) ? n : fallback;
860
- }