@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,6 +1,9 @@
1
1
  /**
2
2
  * Route handlers for memory item CRUD endpoints.
3
3
  *
4
+ * Queries memory_graph_nodes and maps results to the client's
5
+ * MemoryItemPayload shape for backwards compatibility.
6
+ *
4
7
  * GET /v1/memory-items — list memory items (with filtering, search, sort, pagination)
5
8
  * GET /v1/memory-items/:id — get a single memory item
6
9
  * POST /v1/memory-items — create a new memory item
@@ -8,8 +11,17 @@
8
11
  * DELETE /v1/memory-items/:id — delete a memory item and its embeddings
9
12
  */
10
13
 
11
- import { and, asc, count, desc, eq, inArray, like, ne, or } from "drizzle-orm";
12
- import { v4 as uuid } from "uuid";
14
+ import {
15
+ and,
16
+ asc,
17
+ count,
18
+ desc,
19
+ eq,
20
+ inArray,
21
+ like,
22
+ ne,
23
+ notInArray,
24
+ } from "drizzle-orm";
13
25
  import { z } from "zod";
14
26
 
15
27
  import { getConfig } from "../../config/loader.js";
@@ -19,15 +31,23 @@ import {
19
31
  generateSparseEmbedding,
20
32
  getMemoryBackendStatus,
21
33
  } from "../../memory/embedding-backend.js";
22
- import { computeMemoryFingerprint } from "../../memory/fingerprint.js";
34
+ import {
35
+ createNode,
36
+ deleteNode,
37
+ getNode,
38
+ updateNode,
39
+ } from "../../memory/graph/store.js";
40
+ import type {
41
+ Fidelity,
42
+ ImageRef,
43
+ MemoryNode,
44
+ MemoryType,
45
+ NewNode,
46
+ } from "../../memory/graph/types.js";
23
47
  import { enqueueMemoryJob } from "../../memory/jobs-store.js";
24
48
  import { withQdrantBreaker } from "../../memory/qdrant-circuit-breaker.js";
25
49
  import { getQdrantClient } from "../../memory/qdrant-client.js";
26
- import {
27
- conversations,
28
- memoryEmbeddings,
29
- memoryItems,
30
- } from "../../memory/schema.js";
50
+ import { conversations, memoryGraphNodes } from "../../memory/schema.js";
31
51
  import { getLogger } from "../../util/logger.js";
32
52
  import { httpError } from "../http-errors.js";
33
53
  import type { RouteContext, RouteDefinition } from "../http-router.js";
@@ -38,23 +58,20 @@ const log = getLogger("memory-item-routes");
38
58
  // Constants
39
59
  // ---------------------------------------------------------------------------
40
60
 
41
- const VALID_KINDS = [
42
- "identity",
43
- "preference",
44
- "project",
45
- "decision",
46
- "constraint",
47
- "event",
48
- "capability",
49
- "journal",
50
- ] as const;
51
-
52
- type MemoryItemKind = (typeof VALID_KINDS)[number];
61
+ const VALID_TYPES: MemoryType[] = [
62
+ "episodic",
63
+ "semantic",
64
+ "procedural",
65
+ "emotional",
66
+ "prospective",
67
+ "behavioral",
68
+ "narrative",
69
+ "shared",
70
+ ];
53
71
 
54
72
  const VALID_SORT_FIELDS = [
55
73
  "lastSeenAt",
56
74
  "importance",
57
- "accessCount",
58
75
  "kind",
59
76
  "firstSeenAt",
60
77
  ] as const;
@@ -62,31 +79,82 @@ const VALID_SORT_FIELDS = [
62
79
  type SortField = (typeof VALID_SORT_FIELDS)[number];
63
80
 
64
81
  const SORT_COLUMN_MAP = {
65
- lastSeenAt: memoryItems.lastSeenAt,
66
- importance: memoryItems.importance,
67
- accessCount: memoryItems.accessCount,
68
- kind: memoryItems.kind,
69
- firstSeenAt: memoryItems.firstSeenAt,
82
+ lastSeenAt: memoryGraphNodes.lastAccessed,
83
+ importance: memoryGraphNodes.significance,
84
+ kind: memoryGraphNodes.type,
85
+ firstSeenAt: memoryGraphNodes.created,
70
86
  } as const;
71
87
 
72
88
  // ---------------------------------------------------------------------------
73
89
  // Helpers
74
90
  // ---------------------------------------------------------------------------
75
91
 
76
- function isValidKind(value: string): value is MemoryItemKind {
77
- return (VALID_KINDS as readonly string[]).includes(value);
92
+ function isValidType(value: string): value is MemoryType {
93
+ return (VALID_TYPES as string[]).includes(value);
78
94
  }
79
95
 
80
96
  function isValidSortField(value: string): value is SortField {
81
97
  return (VALID_SORT_FIELDS as readonly string[]).includes(value);
82
98
  }
83
99
 
100
+ /**
101
+ * Split graph node content into subject (first line) and statement (rest).
102
+ * Playbooks store JSON in statement; other nodes use plain prose.
103
+ */
104
+ function splitContent(content: string): { subject: string; statement: string } {
105
+ const newlineIdx = content.indexOf("\n");
106
+ if (newlineIdx === -1) {
107
+ return { subject: content, statement: content };
108
+ }
109
+ return {
110
+ subject: content.slice(0, newlineIdx).trim(),
111
+ statement: content.slice(newlineIdx + 1).trim(),
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Map a graph node to the client's MemoryItemPayload shape.
117
+ */
118
+ function nodeToPayload(
119
+ node: MemoryNode,
120
+ scopeLabel: string | null = null,
121
+ ): Record<string, unknown> {
122
+ const { subject, statement } = splitContent(node.content);
123
+ return {
124
+ id: node.id,
125
+ kind: node.type,
126
+ subject,
127
+ statement,
128
+ status: node.fidelity === "gone" ? "superseded" : "active",
129
+ confidence: node.confidence,
130
+ importance: node.significance,
131
+ eventDate: node.eventDate,
132
+ firstSeenAt: node.created,
133
+ lastSeenAt: node.lastAccessed,
134
+
135
+ // Graph-specific fields
136
+ fidelity: node.fidelity,
137
+ sourceType: node.sourceType,
138
+ narrativeRole: node.narrativeRole,
139
+ partOfStory: node.partOfStory,
140
+ reinforcementCount: node.reinforcementCount,
141
+ stability: node.stability,
142
+ emotionalCharge: node.emotionalCharge,
143
+
144
+ scopeId: node.scopeId,
145
+ scopeLabel,
146
+
147
+ // Legacy fields — not applicable to graph nodes
148
+ accessCount: null,
149
+ verificationState: null,
150
+ lastUsedAt: null,
151
+ supersedes: null,
152
+ supersededBy: null,
153
+ };
154
+ }
155
+
84
156
  /**
85
157
  * Resolve a `scopeLabel` for a memory item based on its `scopeId`.
86
- *
87
- * - `"default"` → `null`
88
- * - `"private:<conversationId>"` → `"Private · <title>"` when the conversation
89
- * has a title, or `"Private"` when it doesn't (or the conversation was deleted).
90
158
  */
91
159
  function resolveScopeLabel(
92
160
  scopeId: string,
@@ -103,7 +171,6 @@ function resolveScopeLabel(
103
171
 
104
172
  /**
105
173
  * Batch-fetch conversation titles for a set of private-scoped memory items.
106
- * Returns a Map from conversation ID → title (or null).
107
174
  */
108
175
  function buildConversationTitleMap(
109
176
  db: ReturnType<typeof getDb>,
@@ -133,20 +200,6 @@ function buildConversationTitleMap(
133
200
  // Semantic search constants
134
201
  // ---------------------------------------------------------------------------
135
202
 
136
- /**
137
- * Maximum number of candidate IDs fetched from Qdrant for semantic search.
138
- *
139
- * Kind-filtering and pagination happen *after* this fetch, so if a broad query
140
- * matches more items than this ceiling, results beyond it are silently excluded
141
- * — kind-filtered counts may be under-reported and offsets past this value are
142
- * unreachable. The limit exists to bound Qdrant query cost and the size of the
143
- * subsequent `IN (...)` SQL clauses (SQLite's SQLITE_MAX_VARIABLE_NUMBER is
144
- * 32,766 in Bun's bundled build, so 10k stays well within that).
145
- *
146
- * If this becomes a real bottleneck for large memory stores, consider pushing
147
- * kind/status filters into Qdrant payload filtering so the fetch limit only
148
- * needs to cover the final result set.
149
- */
150
203
  const SEMANTIC_SEARCH_FETCH_CEILING = 10_000;
151
204
 
152
205
  // ---------------------------------------------------------------------------
@@ -154,15 +207,14 @@ const SEMANTIC_SEARCH_FETCH_CEILING = 10_000;
154
207
  // ---------------------------------------------------------------------------
155
208
 
156
209
  /**
157
- * Attempt hybrid semantic search for memory items via Qdrant.
158
- * Returns ordered item IDs + total count on success, or `null` when
210
+ * Hybrid semantic search for graph nodes via Qdrant.
211
+ * Returns ordered node IDs + total count on success, or `null` when
159
212
  * the embedding backend / Qdrant is unavailable (caller falls back to SQL).
160
213
  */
161
- async function searchItemsSemantic(
214
+ async function searchNodesSemantic(
162
215
  query: string,
163
216
  fetchLimit: number,
164
217
  kindFilter: string | null,
165
- statusFilter: string,
166
218
  ): Promise<{ ids: string[]; total: number } | null> {
167
219
  try {
168
220
  const config = getConfig();
@@ -176,13 +228,10 @@ async function searchItemsSemantic(
176
228
  const sparse = generateSparseEmbedding(query);
177
229
  const sparseVector = { indices: sparse.indices, values: sparse.values };
178
230
 
179
- // Build Qdrant filter — items only, exclude sentinel
231
+ // Filter to graph_node target_type, exclude gone nodes
180
232
  const mustConditions: Array<Record<string, unknown>> = [
181
- { key: "target_type", match: { value: "item" } },
233
+ { key: "target_type", match: { value: "graph_node" } },
182
234
  ];
183
- if (statusFilter && statusFilter !== "all") {
184
- mustConditions.push({ key: "status", match: { value: statusFilter } });
185
- }
186
235
  if (kindFilter) {
187
236
  mustConditions.push({ key: "kind", match: { value: kindFilter } });
188
237
  }
@@ -204,11 +253,6 @@ async function searchItemsSemantic(
204
253
  );
205
254
 
206
255
  const ids = results.map((r) => r.payload.target_id);
207
-
208
- // Use the vector search result count as the pagination total.
209
- // A DB-wide COUNT would include items with no embedding yet (lagging) and
210
- // items irrelevant to the search query, inflating the total and causing
211
- // clients to paginate into empty pages.
212
256
  return { ids, total: ids.length };
213
257
  } catch (err) {
214
258
  log.warn({ err }, "Semantic memory search failed, falling back to SQL");
@@ -229,10 +273,10 @@ export async function handleListMemoryItems(url: URL): Promise<Response> {
229
273
  const limitParam = Number(url.searchParams.get("limit") ?? 100);
230
274
  const offsetParam = Number(url.searchParams.get("offset") ?? 0);
231
275
 
232
- if (kindParam && !isValidKind(kindParam)) {
276
+ if (kindParam && !isValidType(kindParam)) {
233
277
  return httpError(
234
278
  "BAD_REQUEST",
235
- `Invalid kind "${kindParam}". Must be one of: ${VALID_KINDS.join(", ")}`,
279
+ `Invalid kind "${kindParam}". Must be one of: ${VALID_TYPES.join(", ")}`,
236
280
  400,
237
281
  );
238
282
  }
@@ -255,57 +299,62 @@ export async function handleListMemoryItems(url: URL): Promise<Response> {
255
299
 
256
300
  const db = getDb();
257
301
 
302
+ // Build fidelity filter based on status param
303
+ const fidelityFilter =
304
+ statusParam === "all"
305
+ ? undefined
306
+ : statusParam === "inactive"
307
+ ? eq(memoryGraphNodes.fidelity, "gone")
308
+ : notInArray(memoryGraphNodes.fidelity, ["gone"]);
309
+
258
310
  // ── Semantic search path ────────────────────────────────────────────
259
- // When a search query is present, try Qdrant hybrid search first.
260
- // Falls back to SQL LIKE when embeddings / Qdrant are unavailable.
261
311
  if (searchParam) {
262
- // Search WITHOUT kind filter so we can compute cross-kind counts.
263
- // Kind filtering is applied post-hoc while preserving relevance order.
264
- const semanticResult = await searchItemsSemantic(
312
+ const semanticResult = await searchNodesSemantic(
265
313
  searchParam,
266
314
  SEMANTIC_SEARCH_FETCH_CEILING,
267
315
  null,
268
- statusParam,
269
316
  );
270
317
 
271
318
  if (semanticResult && semanticResult.ids.length > 0) {
272
- // Compute kindCounts from all semantic matches (no kind filter).
273
- // Status filter is applied as defense-in-depth against stale Qdrant payloads.
274
- const kindCountConditions = [inArray(memoryItems.id, semanticResult.ids)];
275
- if (statusParam && statusParam !== "all") {
276
- kindCountConditions.push(eq(memoryItems.status, statusParam));
277
- }
319
+ // Compute kindCounts from all semantic matches
320
+ const kindCountConditions = [
321
+ inArray(memoryGraphNodes.id, semanticResult.ids),
322
+ ];
323
+ if (fidelityFilter) kindCountConditions.push(fidelityFilter);
324
+
278
325
  const kindCountRows = db
279
- .select({ kind: memoryItems.kind, count: count() })
280
- .from(memoryItems)
326
+ .select({ kind: memoryGraphNodes.type, count: count() })
327
+ .from(memoryGraphNodes)
281
328
  .where(and(...kindCountConditions))
282
- .groupBy(memoryItems.kind)
329
+ .groupBy(memoryGraphNodes.type)
283
330
  .all();
284
331
  const semanticKindCounts: Record<string, number> = {};
285
332
  for (const row of kindCountRows) {
286
333
  semanticKindCounts[row.kind] = row.count;
287
334
  }
288
335
 
289
- // Apply kind filter while preserving semantic relevance ordering.
290
- // Status filter is applied here too for defense-in-depth consistency.
336
+ // Apply kind + fidelity filter while preserving semantic relevance ordering
291
337
  let filteredIds = semanticResult.ids;
292
- if (kindParam) {
293
- const kindFilterConditions = [
294
- inArray(memoryItems.id, semanticResult.ids),
295
- eq(memoryItems.kind, kindParam),
338
+ {
339
+ const filterConditions = [
340
+ inArray(memoryGraphNodes.id, semanticResult.ids),
296
341
  ];
297
- if (statusParam && statusParam !== "all") {
298
- kindFilterConditions.push(eq(memoryItems.status, statusParam));
342
+ if (kindParam) {
343
+ filterConditions.push(eq(memoryGraphNodes.type, kindParam));
344
+ }
345
+ if (fidelityFilter) filterConditions.push(fidelityFilter);
346
+
347
+ if (filterConditions.length > 1) {
348
+ const validIdSet = new Set(
349
+ db
350
+ .select({ id: memoryGraphNodes.id })
351
+ .from(memoryGraphNodes)
352
+ .where(and(...filterConditions))
353
+ .all()
354
+ .map((r) => r.id),
355
+ );
356
+ filteredIds = semanticResult.ids.filter((id) => validIdSet.has(id));
299
357
  }
300
- const kindIdSet = new Set(
301
- db
302
- .select({ id: memoryItems.id })
303
- .from(memoryItems)
304
- .where(and(...kindFilterConditions))
305
- .all()
306
- .map((r) => r.id),
307
- );
308
- filteredIds = semanticResult.ids.filter((id) => kindIdSet.has(id));
309
358
  }
310
359
 
311
360
  const total = filteredIds.length;
@@ -319,19 +368,15 @@ export async function handleListMemoryItems(url: URL): Promise<Response> {
319
368
  });
320
369
  }
321
370
 
322
- // Re-apply DB-side filters as defense-in-depth against stale Qdrant
323
- // payloads leaking deleted/mismatched rows.
324
- const hydrationConditions = [inArray(memoryItems.id, pageIds)];
325
- if (statusParam && statusParam !== "all") {
326
- hydrationConditions.push(eq(memoryItems.status, statusParam));
327
- }
328
- if (kindParam) {
329
- hydrationConditions.push(eq(memoryItems.kind, kindParam));
330
- }
371
+ // Hydrate nodes from DB
372
+ const hydrationConditions = [inArray(memoryGraphNodes.id, pageIds)];
373
+ if (fidelityFilter) hydrationConditions.push(fidelityFilter);
374
+ if (kindParam)
375
+ hydrationConditions.push(eq(memoryGraphNodes.type, kindParam));
331
376
 
332
377
  const rows = db
333
378
  .select()
334
- .from(memoryItems)
379
+ .from(memoryGraphNodes)
335
380
  .where(and(...hydrationConditions))
336
381
  .all();
337
382
 
@@ -341,44 +386,34 @@ export async function handleListMemoryItems(url: URL): Promise<Response> {
341
386
 
342
387
  const titleMap = buildConversationTitleMap(
343
388
  db,
344
- rows.map((i) => i.scopeId),
389
+ rows.map((r) => r.scopeId),
345
390
  );
346
- const enrichedItems = rows.map((item) => ({
347
- ...item,
348
- scopeLabel: resolveScopeLabel(item.scopeId, titleMap),
349
- }));
350
-
351
- return Response.json({
352
- items: enrichedItems,
353
- total,
354
- kindCounts: semanticKindCounts,
391
+ const items = rows.map((row) => {
392
+ const node = rowToNode(row);
393
+ return nodeToPayload(node, resolveScopeLabel(node.scopeId, titleMap));
355
394
  });
395
+
396
+ return Response.json({ items, total, kindCounts: semanticKindCounts });
356
397
  }
357
- // semanticResult was null (Qdrant unavailable) or empty — fall through to SQL
398
+ // Fall through to SQL path
358
399
  }
359
400
 
360
401
  // ── Kind counts for SQL path ───────────────────────────────────────
361
- // Respects status/search filters but NOT kind filter, so the sidebar
362
- // can show totals for every kind simultaneously.
363
402
  const kindCountConditions = [];
364
- if (statusParam && statusParam !== "all") {
365
- kindCountConditions.push(eq(memoryItems.status, statusParam));
366
- }
403
+ if (fidelityFilter) kindCountConditions.push(fidelityFilter);
367
404
  if (searchParam) {
368
405
  kindCountConditions.push(
369
- or(
370
- like(memoryItems.subject, `%${searchParam}%`),
371
- like(memoryItems.statement, `%${searchParam}%`),
372
- )!,
406
+ like(memoryGraphNodes.content, `%${searchParam}%`),
373
407
  );
374
408
  }
375
409
  const kindCountWhere =
376
410
  kindCountConditions.length > 0 ? and(...kindCountConditions) : undefined;
411
+
377
412
  const sqlKindCountRows = db
378
- .select({ kind: memoryItems.kind, count: count() })
379
- .from(memoryItems)
413
+ .select({ kind: memoryGraphNodes.type, count: count() })
414
+ .from(memoryGraphNodes)
380
415
  .where(kindCountWhere)
381
- .groupBy(memoryItems.kind)
416
+ .groupBy(memoryGraphNodes.type)
382
417
  .all();
383
418
  const kindCounts: Record<string, number> = {};
384
419
  for (const row of sqlKindCountRows) {
@@ -387,19 +422,10 @@ export async function handleListMemoryItems(url: URL): Promise<Response> {
387
422
 
388
423
  // ── SQL path (default or fallback) ──────────────────────────────────
389
424
  const conditions = [];
390
- if (statusParam && statusParam !== "all") {
391
- conditions.push(eq(memoryItems.status, statusParam));
392
- }
393
- if (kindParam) {
394
- conditions.push(eq(memoryItems.kind, kindParam));
395
- }
425
+ if (fidelityFilter) conditions.push(fidelityFilter);
426
+ if (kindParam) conditions.push(eq(memoryGraphNodes.type, kindParam));
396
427
  if (searchParam) {
397
- conditions.push(
398
- or(
399
- like(memoryItems.subject, `%${searchParam}%`),
400
- like(memoryItems.statement, `%${searchParam}%`),
401
- )!,
402
- );
428
+ conditions.push(like(memoryGraphNodes.content, `%${searchParam}%`));
403
429
  }
404
430
 
405
431
  const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
@@ -407,7 +433,7 @@ export async function handleListMemoryItems(url: URL): Promise<Response> {
407
433
  // Count query
408
434
  const countResult = db
409
435
  .select({ count: count() })
410
- .from(memoryItems)
436
+ .from(memoryGraphNodes)
411
437
  .where(whereClause)
412
438
  .get();
413
439
  const total = countResult?.count ?? 0;
@@ -416,26 +442,25 @@ export async function handleListMemoryItems(url: URL): Promise<Response> {
416
442
  const sortColumn = SORT_COLUMN_MAP[sortParam];
417
443
  const orderFn = orderParam === "asc" ? asc : desc;
418
444
 
419
- const items = db
445
+ const rows = db
420
446
  .select()
421
- .from(memoryItems)
447
+ .from(memoryGraphNodes)
422
448
  .where(whereClause)
423
449
  .orderBy(orderFn(sortColumn))
424
450
  .limit(limitParam)
425
451
  .offset(offsetParam)
426
452
  .all();
427
453
 
428
- // Resolve scope labels for private-scoped items
429
454
  const titleMap = buildConversationTitleMap(
430
455
  db,
431
- items.map((i) => i.scopeId),
456
+ rows.map((r) => r.scopeId),
432
457
  );
433
- const enrichedItems = items.map((item) => ({
434
- ...item,
435
- scopeLabel: resolveScopeLabel(item.scopeId, titleMap),
436
- }));
458
+ const items = rows.map((row) => {
459
+ const node = rowToNode(row);
460
+ return nodeToPayload(node, resolveScopeLabel(node.scopeId, titleMap));
461
+ });
437
462
 
438
- return Response.json({ items: enrichedItems, total, kindCounts });
463
+ return Response.json({ items, total, kindCounts });
439
464
  }
440
465
 
441
466
  // ---------------------------------------------------------------------------
@@ -444,51 +469,17 @@ export async function handleListMemoryItems(url: URL): Promise<Response> {
444
469
 
445
470
  export function handleGetMemoryItem(ctx: RouteContext): Response {
446
471
  const { id } = ctx.params;
447
- const db = getDb();
448
472
 
449
- const item = db
450
- .select()
451
- .from(memoryItems)
452
- .where(eq(memoryItems.id, id))
453
- .get();
454
-
455
- if (!item) {
473
+ const node = getNode(id);
474
+ if (!node) {
456
475
  return httpError("NOT_FOUND", "Memory item not found", 404);
457
476
  }
458
477
 
459
- let supersedesSubject: string | undefined;
460
- let supersededBySubject: string | undefined;
461
-
462
- if (item.supersedes) {
463
- const superseded = db
464
- .select({ subject: memoryItems.subject })
465
- .from(memoryItems)
466
- .where(eq(memoryItems.id, item.supersedes))
467
- .get();
468
- supersedesSubject = superseded?.subject;
469
- }
470
-
471
- if (item.supersededBy) {
472
- const superseding = db
473
- .select({ subject: memoryItems.subject })
474
- .from(memoryItems)
475
- .where(eq(memoryItems.id, item.supersededBy))
476
- .get();
477
- supersededBySubject = superseding?.subject;
478
- }
479
-
480
- // Resolve scope label
481
- const titleMap = buildConversationTitleMap(db, [item.scopeId]);
482
- const scopeLabel = resolveScopeLabel(item.scopeId, titleMap);
478
+ const db = getDb();
479
+ const titleMap = buildConversationTitleMap(db, [node.scopeId]);
480
+ const scopeLabel = resolveScopeLabel(node.scopeId, titleMap);
483
481
 
484
- return Response.json({
485
- item: {
486
- ...item,
487
- scopeLabel,
488
- ...(supersedesSubject !== undefined ? { supersedesSubject } : {}),
489
- ...(supersededBySubject !== undefined ? { supersededBySubject } : {}),
490
- },
491
- });
482
+ return Response.json({ item: nodeToPayload(node, scopeLabel) });
492
483
  }
493
484
 
494
485
  // ---------------------------------------------------------------------------
@@ -507,25 +498,14 @@ export async function handleCreateMemoryItem(
507
498
 
508
499
  const { kind, subject, statement, importance } = body;
509
500
 
510
- // Validate kind
511
- if (typeof kind !== "string" || !isValidKind(kind)) {
501
+ if (typeof kind !== "string" || !isValidType(kind)) {
512
502
  return httpError(
513
503
  "BAD_REQUEST",
514
- `kind is required and must be one of: ${VALID_KINDS.join(", ")}`,
504
+ `kind is required and must be one of: ${VALID_TYPES.join(", ")}`,
515
505
  400,
516
506
  );
517
507
  }
518
508
 
519
- // Validate subject
520
- if (typeof subject !== "string" || subject.trim().length === 0) {
521
- return httpError(
522
- "BAD_REQUEST",
523
- "subject is required and must be a non-empty string",
524
- 400,
525
- );
526
- }
527
-
528
- // Validate statement
529
509
  if (typeof statement !== "string" || statement.trim().length === 0) {
530
510
  return httpError(
531
511
  "BAD_REQUEST",
@@ -534,27 +514,21 @@ export async function handleCreateMemoryItem(
534
514
  );
535
515
  }
536
516
 
537
- const trimmedSubject = subject.trim();
517
+ const trimmedSubject = typeof subject === "string" ? subject.trim() : "";
538
518
  const trimmedStatement = statement.trim();
519
+ const content = trimmedSubject
520
+ ? `${trimmedSubject}\n${trimmedStatement}`
521
+ : trimmedStatement;
539
522
 
540
- const scopeId = "default";
541
- const fingerprint = computeMemoryFingerprint(
542
- scopeId,
543
- kind,
544
- trimmedSubject,
545
- trimmedStatement,
546
- );
547
-
523
+ // Check for duplicate content
548
524
  const db = getDb();
549
-
550
- // Check for existing item with same fingerprint + scopeId
551
525
  const existing = db
552
- .select()
553
- .from(memoryItems)
526
+ .select({ id: memoryGraphNodes.id })
527
+ .from(memoryGraphNodes)
554
528
  .where(
555
529
  and(
556
- eq(memoryItems.fingerprint, fingerprint),
557
- eq(memoryItems.scopeId, scopeId),
530
+ eq(memoryGraphNodes.content, content),
531
+ ne(memoryGraphNodes.fidelity, "gone"),
558
532
  ),
559
533
  )
560
534
  .get();
@@ -567,44 +541,43 @@ export async function handleCreateMemoryItem(
567
541
  );
568
542
  }
569
543
 
570
- const id = uuid();
571
544
  const now = Date.now();
545
+ const newNode: NewNode = {
546
+ content,
547
+ type: kind as MemoryType,
548
+ created: now,
549
+ lastAccessed: now,
550
+ lastConsolidated: now,
551
+ eventDate: null,
552
+ emotionalCharge: {
553
+ valence: 0,
554
+ intensity: 0.1,
555
+ decayCurve: "linear",
556
+ decayRate: 0.05,
557
+ originalIntensity: 0.1,
558
+ },
559
+ fidelity: "vivid",
560
+ confidence: 0.95,
561
+ significance: importance ?? 0.8,
562
+ stability: 14,
563
+ reinforcementCount: 0,
564
+ lastReinforced: now,
565
+ sourceConversations: [],
566
+ sourceType: "direct",
567
+ narrativeRole: null,
568
+ partOfStory: null,
569
+ imageRefs: null,
570
+ scopeId: "default",
571
+ };
572
572
 
573
- db.insert(memoryItems)
574
- .values({
575
- id,
576
- kind,
577
- subject: trimmedSubject,
578
- statement: trimmedStatement,
579
- status: "active",
580
- confidence: 0.95,
581
- importance: importance ?? 0.8,
582
- fingerprint,
583
- sourceType: "tool",
584
- verificationState: "user_confirmed",
585
- scopeId,
586
- firstSeenAt: now,
587
- lastSeenAt: now,
588
- lastUsedAt: null,
589
- overrideConfidence: "explicit",
590
- })
591
- .run();
592
-
593
- enqueueMemoryJob("embed_item", { itemId: id });
594
-
595
- // Fetch the inserted row to return it
596
- const insertedRow = db
597
- .select()
598
- .from(memoryItems)
599
- .where(eq(memoryItems.id, id))
600
- .get();
573
+ const created = createNode(newNode);
574
+ enqueueMemoryJob("embed_graph_node", { nodeId: created.id });
601
575
 
602
- // Enrich with scopeLabel for API consistency
603
- const titleMap = buildConversationTitleMap(db, [scopeId]);
604
- const scopeLabel = resolveScopeLabel(scopeId, titleMap);
576
+ const titleMap = buildConversationTitleMap(db, [created.scopeId]);
577
+ const scopeLabel = resolveScopeLabel(created.scopeId, titleMap);
605
578
 
606
579
  return Response.json(
607
- { item: { ...insertedRow, scopeLabel } },
580
+ { item: nodeToPayload(created, scopeLabel) },
608
581
  { status: 201 },
609
582
  );
610
583
  }
@@ -617,114 +590,82 @@ export async function handleUpdateMemoryItem(
617
590
  ctx: RouteContext,
618
591
  ): Promise<Response> {
619
592
  const { id } = ctx.params;
593
+
594
+ const existing = getNode(id);
595
+ if (!existing) {
596
+ return httpError("NOT_FOUND", "Memory item not found", 404);
597
+ }
598
+
620
599
  const body = (await ctx.req.json()) as {
621
600
  subject?: string;
622
601
  statement?: string;
623
602
  kind?: string;
624
603
  status?: string;
625
604
  importance?: number;
626
- sourceType?: string;
627
- verificationState?: string;
628
605
  };
629
606
 
630
- const db = getDb();
631
-
632
- const existing = db
633
- .select()
634
- .from(memoryItems)
635
- .where(eq(memoryItems.id, id))
636
- .get();
637
-
638
- if (!existing) {
639
- return httpError("NOT_FOUND", "Memory item not found", 404);
640
- }
641
-
642
- // Build the update set with only provided fields
643
- const set: Record<string, unknown> = {
644
- lastSeenAt: Date.now(),
607
+ const changes: Partial<Omit<MemoryNode, "id">> = {
608
+ lastAccessed: Date.now(),
645
609
  };
646
610
 
647
- if (body.subject !== undefined) {
648
- if (typeof body.subject !== "string") {
649
- return httpError("BAD_REQUEST", "subject must be a string", 400);
650
- }
651
- set.subject = body.subject.trim();
652
- }
653
- if (body.statement !== undefined) {
654
- if (typeof body.statement !== "string") {
655
- return httpError("BAD_REQUEST", "statement must be a string", 400);
611
+ // Rebuild content if subject or statement changed
612
+ const { subject: existingSubject, statement: existingStatement } =
613
+ splitContent(existing.content);
614
+ const newSubject =
615
+ body.subject !== undefined ? body.subject.trim() : existingSubject;
616
+ const newStatement =
617
+ body.statement !== undefined ? body.statement.trim() : existingStatement;
618
+
619
+ let contentChanged = false;
620
+ if (body.subject !== undefined || body.statement !== undefined) {
621
+ const newContent = newSubject
622
+ ? `${newSubject}\n${newStatement}`
623
+ : newStatement;
624
+ if (newContent !== existing.content) {
625
+ changes.content = newContent;
626
+ contentChanged = true;
656
627
  }
657
- set.statement = body.statement.trim();
658
628
  }
629
+
659
630
  if (body.kind !== undefined) {
660
- if (!isValidKind(body.kind)) {
631
+ if (!isValidType(body.kind)) {
661
632
  return httpError(
662
633
  "BAD_REQUEST",
663
- `Invalid kind "${body.kind}". Must be one of: ${VALID_KINDS.join(", ")}`,
634
+ `Invalid kind "${body.kind}". Must be one of: ${VALID_TYPES.join(", ")}`,
664
635
  400,
665
636
  );
666
637
  }
667
- set.kind = body.kind;
668
- }
669
- if (body.status !== undefined) {
670
- set.status = body.status;
671
- }
672
- if (body.importance !== undefined) {
673
- set.importance = body.importance;
674
- }
675
- if (body.sourceType !== undefined) {
676
- set.sourceType = body.sourceType;
638
+ changes.type = body.kind as MemoryType;
677
639
  }
678
640
 
679
- // Accept verificationState from clients that haven't migrated to sourceType yet.
680
- // Map verificationState sourceType for forward compat, and write both fields.
681
- if (body.verificationState !== undefined) {
682
- set.verificationState = body.verificationState;
683
- // Map verificationState to sourceType if sourceType wasn't explicitly provided
684
- if (body.sourceType === undefined) {
685
- set.sourceType =
686
- body.verificationState === "user_confirmed" ? "tool" : "extraction";
641
+ if (body.status !== undefined) {
642
+ // Map client status to fidelity
643
+ if (body.status === "superseded" || body.status === "inactive") {
644
+ changes.fidelity = "gone";
645
+ } else if (body.status === "active") {
646
+ changes.fidelity = "vivid";
687
647
  }
688
648
  }
689
- // If sourceType was set (either directly or via mapping), also write verificationState
690
- if (body.sourceType !== undefined && body.verificationState === undefined) {
691
- set.verificationState =
692
- body.sourceType === "tool"
693
- ? "user_confirmed"
694
- : existing.verificationState === "user_reported"
695
- ? "user_reported"
696
- : "assistant_inferred";
697
- }
698
-
699
- // If subject, statement, or kind changed, recompute fingerprint
700
- const contentChanged =
701
- body.subject !== undefined ||
702
- body.statement !== undefined ||
703
- body.kind !== undefined;
704
649
 
705
- if (contentChanged) {
706
- const newSubject = (set.subject as string | undefined) ?? existing.subject;
707
- const newStatement =
708
- (set.statement as string | undefined) ?? existing.statement;
709
- const newKind = (set.kind as string | undefined) ?? existing.kind;
710
- const scopeId = existing.scopeId;
711
-
712
- const fingerprint = computeMemoryFingerprint(
713
- scopeId,
714
- newKind,
715
- newSubject,
716
- newStatement,
717
- );
650
+ if (body.importance !== undefined) {
651
+ changes.significance = body.importance;
652
+ }
718
653
 
719
- // Check for collision (exclude self)
654
+ // Check for content collision when content changed OR when reactivating a
655
+ // gone item (which could duplicate an existing active item's content).
656
+ const reactivating =
657
+ changes.fidelity === "vivid" && existing.fidelity === "gone";
658
+ if (contentChanged || reactivating) {
659
+ const contentToCheck = changes.content ?? existing.content;
660
+ const db = getDb();
720
661
  const collision = db
721
- .select({ id: memoryItems.id })
722
- .from(memoryItems)
662
+ .select({ id: memoryGraphNodes.id })
663
+ .from(memoryGraphNodes)
723
664
  .where(
724
665
  and(
725
- eq(memoryItems.fingerprint, fingerprint),
726
- eq(memoryItems.scopeId, scopeId),
727
- ne(memoryItems.id, id),
666
+ eq(memoryGraphNodes.content, contentToCheck),
667
+ ne(memoryGraphNodes.id, id),
668
+ ne(memoryGraphNodes.fidelity, "gone"),
728
669
  ),
729
670
  )
730
671
  .get();
@@ -736,36 +677,25 @@ export async function handleUpdateMemoryItem(
736
677
  409,
737
678
  );
738
679
  }
739
-
740
- set.fingerprint = fingerprint;
741
680
  }
742
681
 
743
- db.update(memoryItems).set(set).where(eq(memoryItems.id, id)).run();
682
+ updateNode(id, changes);
744
683
 
745
- // If statement changed, enqueue embed job
746
- if (body.statement !== undefined) {
747
- enqueueMemoryJob("embed_item", { itemId: id });
684
+ if (contentChanged) {
685
+ enqueueMemoryJob("embed_graph_node", { nodeId: id });
748
686
  }
749
687
 
750
- // Fetch and return the updated row
751
- const updatedRow = db
752
- .select()
753
- .from(memoryItems)
754
- .where(eq(memoryItems.id, id))
755
- .get();
688
+ // Fetch updated node
689
+ const updated = getNode(id);
690
+ if (!updated) {
691
+ return httpError("NOT_FOUND", "Memory item not found after update", 404);
692
+ }
756
693
 
757
- // Enrich with scopeLabel for API consistency
758
- const patchTitleMap = buildConversationTitleMap(db, [
759
- updatedRow?.scopeId ?? existing.scopeId,
760
- ]);
761
- const patchScopeLabel = resolveScopeLabel(
762
- updatedRow?.scopeId ?? existing.scopeId,
763
- patchTitleMap,
764
- );
694
+ const db = getDb();
695
+ const titleMap = buildConversationTitleMap(db, [updated.scopeId]);
696
+ const scopeLabel = resolveScopeLabel(updated.scopeId, titleMap);
765
697
 
766
- return Response.json({
767
- item: { ...updatedRow, scopeLabel: patchScopeLabel },
768
- });
698
+ return Response.json({ item: nodeToPayload(updated, scopeLabel) });
769
699
  }
770
700
 
771
701
  // ---------------------------------------------------------------------------
@@ -776,34 +706,57 @@ export async function handleDeleteMemoryItem(
776
706
  ctx: RouteContext,
777
707
  ): Promise<Response> {
778
708
  const { id } = ctx.params;
779
- const db = getDb();
780
-
781
- const existing = db
782
- .select()
783
- .from(memoryItems)
784
- .where(eq(memoryItems.id, id))
785
- .get();
786
709
 
710
+ const existing = getNode(id);
787
711
  if (!existing) {
788
712
  return httpError("NOT_FOUND", "Memory item not found", 404);
789
713
  }
790
714
 
791
- // Delete embeddings for this item
792
- db.delete(memoryEmbeddings)
793
- .where(
794
- and(
795
- eq(memoryEmbeddings.targetType, "item"),
796
- eq(memoryEmbeddings.targetId, id),
797
- ),
798
- )
799
- .run();
715
+ // Hard-delete the node (cascades to edges and triggers via FK)
716
+ deleteNode(id);
800
717
 
801
- // Delete the item (cascades memoryItemSources)
802
- db.delete(memoryItems).where(eq(memoryItems.id, id)).run();
718
+ // Clean up Qdrant vectors asynchronously
719
+ enqueueMemoryJob("delete_qdrant_vectors", {
720
+ targetType: "graph_node",
721
+ targetId: id,
722
+ });
803
723
 
804
724
  return new Response(null, { status: 204 });
805
725
  }
806
726
 
727
+ // ---------------------------------------------------------------------------
728
+ // Row → MemoryNode helper (inline version of store's rowToNode)
729
+ // ---------------------------------------------------------------------------
730
+
731
+ function rowToNode(row: typeof memoryGraphNodes.$inferSelect): MemoryNode {
732
+ return {
733
+ id: row.id,
734
+ content: row.content,
735
+ type: row.type as MemoryType,
736
+ created: row.created,
737
+ lastAccessed: row.lastAccessed,
738
+ lastConsolidated: row.lastConsolidated,
739
+ eventDate: row.eventDate ?? null,
740
+ emotionalCharge: JSON.parse(row.emotionalCharge),
741
+ fidelity: row.fidelity as Fidelity,
742
+ confidence: row.confidence,
743
+ significance: row.significance,
744
+ stability: row.stability,
745
+ reinforcementCount: row.reinforcementCount,
746
+ lastReinforced: row.lastReinforced,
747
+ sourceConversations: JSON.parse(row.sourceConversations) as string[],
748
+ sourceType: row.sourceType as
749
+ | "direct"
750
+ | "inferred"
751
+ | "observed"
752
+ | "told-by-other",
753
+ narrativeRole: row.narrativeRole,
754
+ partOfStory: row.partOfStory,
755
+ imageRefs: row.imageRefs ? (JSON.parse(row.imageRefs) as ImageRef[]) : null,
756
+ scopeId: row.scopeId,
757
+ };
758
+ }
759
+
807
760
  // ---------------------------------------------------------------------------
808
761
  // Route definitions
809
762
  // ---------------------------------------------------------------------------
@@ -865,14 +818,13 @@ export function memoryItemRouteDefinitions(): RouteDefinition[] {
865
818
  method: "GET",
866
819
  policyKey: "memory-items",
867
820
  summary: "Get a memory item",
868
- description:
869
- "Return a single memory item by ID with supersession metadata.",
821
+ description: "Return a single memory item by ID with graph metadata.",
870
822
  tags: ["memory"],
871
823
  responseBody: z.object({
872
824
  item: z
873
825
  .object({})
874
826
  .passthrough()
875
- .describe("Memory item with scopeLabel and supersession info"),
827
+ .describe("Memory item with scopeLabel and graph metadata"),
876
828
  }),
877
829
  handler: (ctx) => handleGetMemoryItem(ctx),
878
830
  },
@@ -880,13 +832,16 @@ export function memoryItemRouteDefinitions(): RouteDefinition[] {
880
832
  endpoint: "memory-items",
881
833
  method: "POST",
882
834
  summary: "Create a memory item",
883
- description: "Create a new memory item and enqueue embedding.",
835
+ description: "Create a new memory graph node and enqueue embedding.",
884
836
  tags: ["memory"],
885
837
  requestBody: z.object({
886
838
  kind: z
887
839
  .string()
888
- .describe("Memory kind (identity, preference, project, etc.)"),
889
- subject: z.string().describe("Subject line"),
840
+ .describe("Memory type (episodic, semantic, procedural, etc.)"),
841
+ subject: z
842
+ .string()
843
+ .describe("Subject line (first line of content)")
844
+ .optional(),
890
845
  statement: z.string().describe("Statement content"),
891
846
  importance: z
892
847
  .number()
@@ -903,16 +858,14 @@ export function memoryItemRouteDefinitions(): RouteDefinition[] {
903
858
  method: "PATCH",
904
859
  policyKey: "memory-items",
905
860
  summary: "Update a memory item",
906
- description: "Partially update fields on an existing memory item.",
861
+ description: "Partially update fields on an existing memory graph node.",
907
862
  tags: ["memory"],
908
863
  requestBody: z.object({
909
- subject: z.string(),
910
- statement: z.string(),
911
- kind: z.string(),
912
- status: z.string(),
913
- importance: z.number(),
914
- sourceType: z.string(),
915
- verificationState: z.string(),
864
+ subject: z.string().optional(),
865
+ statement: z.string().optional(),
866
+ kind: z.string().optional(),
867
+ status: z.string().optional(),
868
+ importance: z.number().optional(),
916
869
  }),
917
870
  responseBody: z.object({
918
871
  item: z.object({}).passthrough().describe("Updated memory item"),
@@ -924,7 +877,7 @@ export function memoryItemRouteDefinitions(): RouteDefinition[] {
924
877
  method: "DELETE",
925
878
  policyKey: "memory-items",
926
879
  summary: "Delete a memory item",
927
- description: "Delete a memory item and its embeddings.",
880
+ description: "Delete a memory graph node and its embeddings.",
928
881
  tags: ["memory"],
929
882
  responseBody: z.object({
930
883
  ok: z.boolean(),