@vellumai/assistant 0.6.0 → 0.6.2

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 (358) hide show
  1. package/AGENTS.md +4 -0
  2. package/ARCHITECTURE.md +68 -15
  3. package/Dockerfile +2 -2
  4. package/bun.lock +6 -2
  5. package/docker-entrypoint.sh +42 -1
  6. package/docs/architecture/integrations.md +1 -1
  7. package/docs/architecture/memory.md +21 -24
  8. package/node_modules/@vellumai/ces-contracts/src/handles.ts +7 -9
  9. package/openapi.yaml +539 -4
  10. package/package.json +5 -1
  11. package/src/__tests__/anthropic-provider.test.ts +160 -95
  12. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  13. package/src/__tests__/app-executors.test.ts +47 -1
  14. package/src/__tests__/app-source-watcher.test.ts +159 -0
  15. package/src/__tests__/assistant-event-hub.test.ts +30 -0
  16. package/src/__tests__/checker.test.ts +138 -172
  17. package/src/__tests__/cli-command-risk-guard.test.ts +1 -1
  18. package/src/__tests__/config-schema.test.ts +5 -0
  19. package/src/__tests__/context-overflow-approval.test.ts +5 -5
  20. package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -6
  21. package/src/__tests__/conversation-agent-loop.test.ts +4 -51
  22. package/src/__tests__/conversation-analysis-routes.test.ts +169 -0
  23. package/src/__tests__/conversation-directories-parse.test.ts +105 -0
  24. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  25. package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
  26. package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
  27. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
  28. package/src/__tests__/conversation-wipe.test.ts +2 -6
  29. package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
  30. package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
  31. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  32. package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
  33. package/src/__tests__/credential-execution-approval-bridge.test.ts +0 -2
  34. package/src/__tests__/date-context.test.ts +76 -210
  35. package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
  36. package/src/__tests__/file-list-tool.test.ts +219 -0
  37. package/src/__tests__/first-greeting.test.ts +1 -1
  38. package/src/__tests__/heartbeat-service.test.ts +180 -3
  39. package/src/__tests__/identity-routes.test.ts +328 -0
  40. package/src/__tests__/init-feature-flag-overrides.test.ts +167 -0
  41. package/src/__tests__/injection-block.test.ts +24 -0
  42. package/src/__tests__/inline-command-runner.test.ts +7 -5
  43. package/src/__tests__/install-skill-routing.test.ts +7 -6
  44. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +15 -14
  45. package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
  46. package/src/__tests__/llm-context-normalization.test.ts +18 -18
  47. package/src/__tests__/llm-context-route-provider.test.ts +101 -0
  48. package/src/__tests__/llm-request-log-turn-query.test.ts +162 -0
  49. package/src/__tests__/log-export-workspace.test.ts +257 -100
  50. package/src/__tests__/managed-credential-catalog-cli.test.ts +12 -14
  51. package/src/__tests__/mcp-abort-signal.test.ts +5 -0
  52. package/src/__tests__/mcp-client-auth.test.ts +5 -0
  53. package/src/__tests__/memory-recall-log-store.test.ts +132 -0
  54. package/src/__tests__/migration-export-streaming.test.ts +304 -0
  55. package/src/__tests__/migration-import-commit-http.test.ts +11 -10
  56. package/src/__tests__/mock-fetch.ts +87 -0
  57. package/src/__tests__/navigate-settings-tab.test.ts +14 -1
  58. package/src/__tests__/notification-broadcaster.test.ts +65 -0
  59. package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
  60. package/src/__tests__/onboarding-template-contract.test.ts +63 -14
  61. package/src/__tests__/parser.test.ts +32 -0
  62. package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
  63. package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
  64. package/src/__tests__/permission-mode-sse.test.ts +418 -0
  65. package/src/__tests__/permission-mode-store.test.ts +277 -0
  66. package/src/__tests__/permission-mode.test.ts +101 -0
  67. package/src/__tests__/pkb-autoinject.test.ts +96 -0
  68. package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
  69. package/src/__tests__/profiler-routes.test.ts +502 -0
  70. package/src/__tests__/profiler-run-store.test.ts +441 -0
  71. package/src/__tests__/proxy-approval-callback.test.ts +4 -75
  72. package/src/__tests__/registry.test.ts +1 -1
  73. package/src/__tests__/require-fresh-approval.test.ts +0 -2
  74. package/src/__tests__/sandbox-diagnostics.test.ts +1 -32
  75. package/src/__tests__/sandbox-host-parity.test.ts +5 -4
  76. package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
  77. package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
  78. package/src/__tests__/search-skills-unified.test.ts +4 -3
  79. package/src/__tests__/send-endpoint-busy.test.ts +42 -3
  80. package/src/__tests__/set-permission-mode.test.ts +274 -0
  81. package/src/__tests__/skill-load-feature-flag.test.ts +12 -0
  82. package/src/__tests__/skill-memory.test.ts +2 -783
  83. package/src/__tests__/strip-memory-injections.test.ts +187 -0
  84. package/src/__tests__/subagent-detail.test.ts +84 -0
  85. package/src/__tests__/subagent-disposal.test.ts +308 -0
  86. package/src/__tests__/subagent-manager-notify.test.ts +19 -10
  87. package/src/__tests__/subagent-notify-parent.test.ts +390 -0
  88. package/src/__tests__/subagent-role-registry.test.ts +108 -0
  89. package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
  90. package/src/__tests__/subagent-tools.test.ts +464 -4
  91. package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
  92. package/src/__tests__/task-memory-cleanup.test.ts +12 -12
  93. package/src/__tests__/terminal-sandbox.test.ts +1 -1
  94. package/src/__tests__/terminal-tools.test.ts +16 -29
  95. package/src/__tests__/test-preload.ts +18 -0
  96. package/src/__tests__/tool-domain-event-publisher.test.ts +0 -1
  97. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -8
  98. package/src/__tests__/tool-executor.test.ts +4 -27
  99. package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
  100. package/src/__tests__/top-level-renderer.test.ts +10 -13
  101. package/src/__tests__/transport-hints-queue.test.ts +77 -0
  102. package/src/__tests__/trust-store.test.ts +4 -4
  103. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +116 -2
  104. package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
  105. package/src/__tests__/workspace-migration-030-seed-pkb-autoinject.test.ts +168 -0
  106. package/src/__tests__/workspace-policy.test.ts +2 -7
  107. package/src/agent/loop.ts +6 -29
  108. package/src/approvals/guardian-request-resolvers.ts +24 -0
  109. package/src/avatar/traits-png-sync.ts +3 -3
  110. package/src/channels/types.ts +5 -0
  111. package/src/cli/__tests__/run-assistant-command.ts +56 -0
  112. package/src/cli/__tests__/unknown-command.test.ts +33 -0
  113. package/src/cli/commands/__tests__/email-download.test.ts +245 -0
  114. package/src/cli/commands/__tests__/email-list.test.ts +192 -0
  115. package/src/cli/commands/__tests__/email-register.test.ts +186 -0
  116. package/src/cli/commands/__tests__/email-send.test.ts +291 -0
  117. package/src/cli/commands/__tests__/email-status.test.ts +181 -0
  118. package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
  119. package/src/cli/commands/__tests__/routes.test.ts +562 -0
  120. package/src/cli/commands/conversations.ts +1 -8
  121. package/src/cli/commands/default-action.ts +68 -1
  122. package/src/cli/commands/email.ts +584 -835
  123. package/src/cli/commands/memory.ts +1 -34
  124. package/src/cli/commands/notifications.ts +7 -2
  125. package/src/cli/commands/oauth/__tests__/connect.test.ts +27 -0
  126. package/src/cli/commands/oauth/connect.ts +25 -5
  127. package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
  128. package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
  129. package/src/cli/commands/platform/__tests__/status.test.ts +1 -1
  130. package/src/cli/commands/routes.ts +396 -0
  131. package/src/cli/commands/skills.ts +130 -20
  132. package/src/cli/program.ts +11 -2
  133. package/src/cli.ts +1 -120
  134. package/src/config/assistant-feature-flags.ts +59 -55
  135. package/src/config/bundled-skills/app-builder/SKILL.md +91 -5
  136. package/src/config/bundled-skills/gmail/SKILL.md +13 -8
  137. package/src/config/bundled-skills/gmail/TOOLS.json +1 -1
  138. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -1
  139. package/src/config/bundled-skills/messaging/SKILL.md +7 -0
  140. package/src/config/bundled-skills/schedule/SKILL.md +22 -2
  141. package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
  142. package/src/config/bundled-skills/settings/TOOLS.json +1 -1
  143. package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
  144. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
  145. package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
  146. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +8 -3
  147. package/src/config/bundled-skills/slack/SKILL.md +2 -0
  148. package/src/config/bundled-skills/subagent/SKILL.md +43 -3
  149. package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
  150. package/src/config/env-registry.ts +63 -0
  151. package/src/config/feature-flag-registry.json +17 -1
  152. package/src/config/schema.ts +8 -0
  153. package/src/config/schemas/filing.ts +51 -0
  154. package/src/config/schemas/heartbeat.ts +15 -12
  155. package/src/config/schemas/memory-lifecycle.ts +12 -0
  156. package/src/config/schemas/security.ts +14 -0
  157. package/src/config/schemas/services.ts +8 -0
  158. package/src/credential-execution/approval-bridge.ts +0 -1
  159. package/src/credential-execution/managed-catalog.ts +3 -7
  160. package/src/daemon/app-source-watcher.ts +93 -0
  161. package/src/daemon/config-watcher.ts +85 -3
  162. package/src/daemon/context-overflow-approval.ts +0 -1
  163. package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
  164. package/src/daemon/conversation-agent-loop.ts +179 -65
  165. package/src/daemon/conversation-attachments.ts +0 -1
  166. package/src/daemon/conversation-history.ts +4 -19
  167. package/src/daemon/conversation-lifecycle.ts +8 -14
  168. package/src/daemon/conversation-messaging.ts +3 -0
  169. package/src/daemon/conversation-process.ts +30 -8
  170. package/src/daemon/conversation-queue-manager.ts +8 -0
  171. package/src/daemon/conversation-runtime-assembly.ts +359 -308
  172. package/src/daemon/conversation-surfaces.ts +65 -0
  173. package/src/daemon/conversation-tool-setup.ts +44 -17
  174. package/src/daemon/conversation-workspace.ts +1 -2
  175. package/src/daemon/conversation.ts +19 -3
  176. package/src/daemon/date-context.ts +26 -53
  177. package/src/daemon/first-greeting.ts +1 -1
  178. package/src/daemon/handlers/conversations.ts +5 -7
  179. package/src/daemon/handlers/shared.test.ts +143 -0
  180. package/src/daemon/handlers/shared.ts +70 -5
  181. package/src/daemon/handlers/skills.ts +11 -18
  182. package/src/daemon/lifecycle.ts +220 -158
  183. package/src/daemon/message-types/conversations.ts +29 -6
  184. package/src/daemon/message-types/messages.ts +9 -2
  185. package/src/daemon/message-types/notifications.ts +12 -0
  186. package/src/daemon/message-types/schedules.ts +1 -0
  187. package/src/daemon/message-types/settings.ts +18 -0
  188. package/src/daemon/profiler-run-store.ts +557 -0
  189. package/src/daemon/server.ts +87 -10
  190. package/src/daemon/shutdown-handlers.ts +5 -0
  191. package/src/daemon/tool-side-effects.ts +23 -3
  192. package/src/daemon/transport-hints.ts +33 -0
  193. package/src/export/transcript-formatter.ts +148 -0
  194. package/src/filing/filing-service.ts +228 -0
  195. package/src/heartbeat/heartbeat-service.ts +96 -7
  196. package/src/index.ts +1 -1
  197. package/src/mcp/client.ts +6 -0
  198. package/src/mcp/mcp-oauth-provider.ts +149 -27
  199. package/src/memory/admin.ts +33 -32
  200. package/src/memory/app-store.ts +69 -0
  201. package/src/memory/conversation-bootstrap.ts +1 -1
  202. package/src/memory/conversation-crud.ts +151 -117
  203. package/src/memory/conversation-directories.ts +39 -0
  204. package/src/memory/conversation-group-migration.ts +66 -6
  205. package/src/memory/conversation-queries.ts +58 -12
  206. package/src/memory/conversation-title-service.ts +1 -0
  207. package/src/memory/db-init.ts +182 -376
  208. package/src/memory/embedding-local.ts +1 -1
  209. package/src/memory/graph/bootstrap.ts +75 -66
  210. package/src/memory/graph/capability-seed.ts +167 -17
  211. package/src/memory/graph/consolidation.ts +38 -4
  212. package/src/memory/graph/conversation-graph-memory.ts +133 -104
  213. package/src/memory/graph/extraction-job.ts +9 -4
  214. package/src/memory/graph/extraction.ts +66 -23
  215. package/src/memory/graph/graph-memory-state-store.ts +37 -0
  216. package/src/memory/graph/graph-search.ts +29 -15
  217. package/src/memory/graph/injection.ts +38 -8
  218. package/src/memory/graph/inspect.ts +12 -3
  219. package/src/memory/graph/retriever.ts +365 -262
  220. package/src/memory/graph/store.test.ts +48 -0
  221. package/src/memory/graph/store.ts +150 -11
  222. package/src/memory/graph/tool-handlers.ts +84 -209
  223. package/src/memory/graph/tools.ts +8 -52
  224. package/src/memory/graph/types.ts +24 -0
  225. package/src/memory/group-crud.ts +25 -9
  226. package/src/memory/job-handlers/cleanup.ts +44 -1
  227. package/src/memory/jobs-store.ts +70 -60
  228. package/src/memory/jobs-worker.ts +44 -28
  229. package/src/memory/llm-request-log-store.ts +96 -12
  230. package/src/memory/memory-recall-log-store.ts +49 -5
  231. package/src/memory/migrations/203-drop-memory-items-tables.ts +33 -1
  232. package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
  233. package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
  234. package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
  235. package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
  236. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
  237. package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
  238. package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
  239. package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
  240. package/src/memory/migrations/index.ts +8 -0
  241. package/src/memory/migrations/registry.ts +8 -0
  242. package/src/memory/schema/conversations.ts +14 -0
  243. package/src/memory/schema/infrastructure.ts +8 -1
  244. package/src/memory/schema/memory-core.ts +0 -51
  245. package/src/memory/schema/memory-graph.ts +15 -0
  246. package/src/memory/task-memory-cleanup.ts +30 -11
  247. package/src/messaging/provider.ts +1 -1
  248. package/src/notifications/broadcaster.ts +6 -0
  249. package/src/notifications/conversation-pairing.ts +12 -4
  250. package/src/notifications/copy-composer.ts +86 -0
  251. package/src/notifications/decision-engine.ts +35 -0
  252. package/src/notifications/emit-signal.ts +14 -0
  253. package/src/notifications/signal.ts +11 -0
  254. package/src/oauth/platform-connection.test.ts +2 -2
  255. package/src/oauth/seed-providers.ts +1 -0
  256. package/src/permissions/checker.ts +15 -4
  257. package/src/permissions/defaults.ts +7 -8
  258. package/src/permissions/permission-mode-store.ts +180 -0
  259. package/src/permissions/permission-mode.ts +31 -0
  260. package/src/permissions/prompter.ts +0 -2
  261. package/src/permissions/workspace-policy.ts +9 -0
  262. package/src/platform/client.ts +1 -1
  263. package/src/prompts/system-prompt.ts +59 -7
  264. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
  265. package/src/prompts/templates/BOOTSTRAP.md +76 -162
  266. package/src/prompts/templates/HEARTBEAT.md +3 -1
  267. package/src/prompts/templates/SOUL.md +30 -9
  268. package/src/prompts/templates/UPDATES.md +8 -0
  269. package/src/providers/anthropic/client.ts +107 -219
  270. package/src/runtime/assistant-event-hub.ts +22 -0
  271. package/src/runtime/auth/route-policy.ts +23 -0
  272. package/src/runtime/auth/token-service.ts +8 -0
  273. package/src/runtime/http-server.ts +32 -2
  274. package/src/runtime/http-types.ts +12 -1
  275. package/src/runtime/migrations/vbundle-builder.ts +389 -3
  276. package/src/runtime/migrations/vbundle-importer.ts +8 -6
  277. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
  278. package/src/runtime/routes/app-management-routes.ts +1 -11
  279. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
  280. package/src/runtime/routes/archive-utils.ts +29 -0
  281. package/src/runtime/routes/avatar-routes.ts +2 -9
  282. package/src/runtime/routes/btw-routes.ts +14 -1
  283. package/src/runtime/routes/conversation-analysis-routes.ts +185 -0
  284. package/src/runtime/routes/conversation-management-routes.ts +1 -14
  285. package/src/runtime/routes/conversation-query-routes.ts +49 -3
  286. package/src/runtime/routes/conversation-routes.ts +270 -44
  287. package/src/runtime/routes/group-routes.ts +22 -8
  288. package/src/runtime/routes/heartbeat-routes.ts +4 -10
  289. package/src/runtime/routes/identity-routes.ts +53 -18
  290. package/src/runtime/routes/llm-context-normalization.ts +14 -10
  291. package/src/runtime/routes/log-export/AGENTS.md +104 -0
  292. package/src/runtime/routes/log-export/__tests__/workspace-allowlist-error-contract.test.ts +103 -0
  293. package/src/runtime/routes/log-export/__tests__/workspace-allowlist.test.ts +716 -0
  294. package/src/runtime/routes/log-export/workspace-allowlist.ts +458 -0
  295. package/src/runtime/routes/log-export-routes.ts +41 -278
  296. package/src/runtime/routes/memory-item-routes.test.ts +168 -233
  297. package/src/runtime/routes/migration-routes.ts +18 -7
  298. package/src/runtime/routes/profiler-routes.ts +350 -0
  299. package/src/runtime/routes/schedule-routes.ts +27 -12
  300. package/src/runtime/routes/settings-routes.ts +95 -8
  301. package/src/runtime/routes/subagents-routes.ts +28 -7
  302. package/src/runtime/routes/user-route-dispatcher.ts +223 -0
  303. package/src/runtime/routes/user-routes.ts +41 -0
  304. package/src/runtime/routes/workspace-routes.ts +0 -1
  305. package/src/schedule/schedule-store.ts +30 -0
  306. package/src/schedule/scheduler.ts +45 -18
  307. package/src/skills/catalog-install.ts +10 -2
  308. package/src/skills/inline-command-runner.ts +12 -14
  309. package/src/skills/managed-store.ts +2 -2
  310. package/src/skills/skill-memory.ts +1 -293
  311. package/src/subagent/index.ts +13 -3
  312. package/src/subagent/manager.ts +308 -29
  313. package/src/subagent/types.ts +68 -0
  314. package/src/tasks/task-runner.ts +4 -4
  315. package/src/tools/apps/executors.ts +29 -4
  316. package/src/tools/filesystem/list.ts +93 -0
  317. package/src/tools/permission-checker.ts +78 -18
  318. package/src/tools/registry.ts +4 -0
  319. package/src/tools/schedule/create.ts +3 -0
  320. package/src/tools/schedule/list.ts +1 -0
  321. package/src/tools/schedule/update.ts +6 -0
  322. package/src/tools/secret-detection-handler.ts +0 -1
  323. package/src/tools/shared/filesystem/errors.ts +5 -0
  324. package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
  325. package/src/tools/shared/filesystem/types.ts +17 -0
  326. package/src/tools/shared/shell-output.ts +31 -2
  327. package/src/tools/skills/sandbox-runner.ts +3 -6
  328. package/src/tools/subagent/abort.ts +12 -2
  329. package/src/tools/subagent/message.ts +9 -2
  330. package/src/tools/subagent/notify-parent.ts +79 -0
  331. package/src/tools/subagent/read.ts +29 -8
  332. package/src/tools/subagent/resolve.ts +21 -0
  333. package/src/tools/subagent/spawn.ts +2 -0
  334. package/src/tools/subagent/status.ts +11 -1
  335. package/src/tools/system/avatar-generator.ts +3 -3
  336. package/src/tools/system/register.ts +23 -0
  337. package/src/tools/system/set-permission-mode.ts +103 -0
  338. package/src/tools/terminal/parser.ts +30 -5
  339. package/src/tools/terminal/safe-env.ts +16 -1
  340. package/src/tools/terminal/sandbox-diagnostics.ts +4 -4
  341. package/src/tools/terminal/sandbox.ts +4 -1
  342. package/src/tools/terminal/shell.ts +3 -5
  343. package/src/tools/tool-manifest.ts +6 -0
  344. package/src/tools/types.ts +2 -3
  345. package/src/util/logger.ts +1 -1
  346. package/src/util/platform.ts +50 -17
  347. package/src/watcher/provider-types.ts +1 -1
  348. package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
  349. package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
  350. package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
  351. package/src/workspace/migrations/029-seed-pkb.ts +85 -0
  352. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +73 -0
  353. package/src/workspace/migrations/registry.ts +6 -0
  354. package/src/workspace/top-level-renderer.ts +5 -9
  355. package/src/__tests__/cli-memory.test.ts +0 -377
  356. package/src/__tests__/clipboard.test.ts +0 -88
  357. package/src/cli/cli-memory.ts +0 -179
  358. package/src/util/clipboard.ts +0 -34
@@ -13,18 +13,18 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
13
13
  import { join } from "node:path";
14
14
 
15
15
  import { and, asc, ne, sql } from "drizzle-orm";
16
+ import { v4 as uuid } from "uuid";
16
17
 
17
18
  import { getConfig } from "../../config/loader.js";
18
19
  import { getLogger } from "../../util/logger.js";
19
20
  import { getWorkspaceDir } from "../../util/platform.js";
20
21
  import { getMemoryCheckpoint, setMemoryCheckpoint } from "../checkpoints.js";
21
- import { getDb, rawAll } from "../db.js";
22
+ import { getDb, rawAll, rawGet, rawRun } from "../db.js";
22
23
  import { enqueueMemoryJob, hasActiveJobOfType } from "../jobs-store.js";
23
24
  import { initQdrantClient } from "../qdrant-client.js";
24
25
  import { conversations, memoryGraphNodes, memorySegments } from "../schema.js";
25
26
  import { runGraphExtraction } from "./extraction.js";
26
- import { countNodes, createNode } from "./store.js";
27
- import type { NewNode } from "./types.js";
27
+ import { countNodes } from "./store.js";
28
28
 
29
29
  const log = getLogger("graph-bootstrap");
30
30
 
@@ -61,7 +61,7 @@ export interface BootstrapResult {
61
61
  * if interrupted, re-run and it picks up where it left off.
62
62
  */
63
63
  export async function bootstrapFromHistory(
64
- options?: BootstrapOptions
64
+ options?: BootstrapOptions,
65
65
  ): Promise<BootstrapResult> {
66
66
  const start = Date.now();
67
67
  const scopeId = options?.scopeId ?? "default";
@@ -106,7 +106,7 @@ export async function bootstrapFromHistory(
106
106
 
107
107
  log.info(
108
108
  { total: allConversations.length },
109
- "Starting graph bootstrap from historical conversations"
109
+ "Starting graph bootstrap from historical conversations",
110
110
  );
111
111
 
112
112
  // Resume from checkpoint
@@ -148,7 +148,7 @@ export async function bootstrapFromHistory(
148
148
  skipQdrant: true, // Use DB query for candidates (no Qdrant dependency)
149
149
  conversationTimestamp: conv.createdAt, // Use actual conversation time
150
150
  embedInline: true, // Embed synchronously so nodes are searchable immediately
151
- }
151
+ },
152
152
  );
153
153
 
154
154
  result.totalNodesCreated += extractionResult.nodesCreated;
@@ -171,14 +171,14 @@ export async function bootstrapFromHistory(
171
171
  nodes: nodeCount,
172
172
  elapsed: `${((Date.now() - start) / 1000).toFixed(1)}s`,
173
173
  },
174
- "Bootstrap progress"
174
+ "Bootstrap progress",
175
175
  );
176
176
  }
177
177
  } catch (err) {
178
178
  const errMsg = err instanceof Error ? err.message : String(err);
179
179
  log.warn(
180
180
  { conversationId: conv.id, err: errMsg },
181
- "Failed to extract conversation, continuing"
181
+ "Failed to extract conversation, continuing",
182
182
  );
183
183
  result.errors.push({ conversationId: conv.id, error: errMsg });
184
184
 
@@ -199,7 +199,7 @@ export async function bootstrapFromHistory(
199
199
  errors: result.errors.length,
200
200
  elapsedMs: result.elapsedMs,
201
201
  },
202
- "Graph bootstrap complete"
202
+ "Graph bootstrap complete",
203
203
  );
204
204
 
205
205
  return result;
@@ -209,7 +209,7 @@ export async function bootstrapFromHistory(
209
209
  * Also extract from journal files on disk.
210
210
  */
211
211
  export async function bootstrapFromJournal(
212
- scopeId: string = "default"
212
+ scopeId: string = "default",
213
213
  ): Promise<{ extracted: number; errors: number }> {
214
214
  const config = getConfig();
215
215
  const journalDir = join(getWorkspaceDir(), "journal");
@@ -227,7 +227,7 @@ export async function bootstrapFromJournal(
227
227
  (f) =>
228
228
  f.endsWith(".md") &&
229
229
  !f.startsWith(".") &&
230
- f.toLowerCase() !== "readme.md"
230
+ f.toLowerCase() !== "readme.md",
231
231
  );
232
232
  } catch {
233
233
  continue;
@@ -248,7 +248,7 @@ export async function bootstrapFromJournal(
248
248
  } catch (err) {
249
249
  log.warn(
250
250
  { file, slug, err: err instanceof Error ? err.message : String(err) },
251
- "Failed to extract journal entry"
251
+ "Failed to extract journal entry",
252
252
  );
253
253
  errors++;
254
254
  }
@@ -289,8 +289,8 @@ function parseJournalDate(filename: string): number {
289
289
 
290
290
  return new Date(
291
291
  `${year}-${month}-${day}T${String(hours).padStart(2, "0")}:${String(
292
- minutes
293
- ).padStart(2, "0")}:00`
292
+ minutes,
293
+ ).padStart(2, "0")}:00`,
294
294
  ).getTime();
295
295
  }
296
296
 
@@ -320,8 +320,8 @@ export function maybeEnqueueGraphBootstrap(): void {
320
320
  .where(
321
321
  and(
322
322
  ne(memoryGraphNodes.type, "procedural"),
323
- sql`${memoryGraphNodes.fidelity} != 'gone'`
324
- )
323
+ sql`${memoryGraphNodes.fidelity} != 'gone'`,
324
+ ),
325
325
  )
326
326
  .get()?.count ?? 0;
327
327
 
@@ -343,7 +343,7 @@ export function maybeEnqueueGraphBootstrap(): void {
343
343
 
344
344
  log.info(
345
345
  { segmentCount, hasJournalFiles },
346
- "Graph empty with historical data — enqueueing bootstrap"
346
+ "Graph empty with historical data — enqueueing bootstrap",
347
347
  );
348
348
  enqueueMemoryJob("graph_bootstrap", {});
349
349
  }
@@ -379,6 +379,11 @@ const KIND_TO_PREFIX: Record<string, string> = {
379
379
  *
380
380
  * Idempotent: uses a checkpoint to run only once. Skips items whose
381
381
  * sourceKey already exists in the graph.
382
+ *
383
+ * Uses raw SQL for the INSERT to avoid coupling to the evolving Drizzle
384
+ * schema. ORM-based inserts include every column in the schema definition,
385
+ * so adding a column in a later migration would cause this migration to
386
+ * fail with "table has no column named …" on upgrade paths.
382
387
  */
383
388
  export function migrateToolCreatedItems(): void {
384
389
  if (getMemoryCheckpoint(MIGRATE_ITEMS_CHECKPOINT)) return;
@@ -392,12 +397,15 @@ export function migrateToolCreatedItems(): void {
392
397
  `SELECT id, kind, subject, statement, confidence, importance, scope_id, first_seen_at
393
398
  FROM memory_items
394
399
  WHERE kind IN (${placeholders}) AND status = 'active'`,
395
- ...kinds
400
+ ...kinds,
396
401
  );
397
- } catch {
402
+ } catch (err) {
398
403
  // Table may not exist (fresh install) — nothing to migrate
399
- setMemoryCheckpoint(MIGRATE_ITEMS_CHECKPOINT, "done");
400
- return;
404
+ if (err instanceof Error && err.message.includes("no such table")) {
405
+ setMemoryCheckpoint(MIGRATE_ITEMS_CHECKPOINT, "done");
406
+ return;
407
+ }
408
+ throw err;
401
409
  }
402
410
 
403
411
  if (rows.length === 0) {
@@ -405,7 +413,6 @@ export function migrateToolCreatedItems(): void {
405
413
  return;
406
414
  }
407
415
 
408
- const db = getDb();
409
416
  let migrated = 0;
410
417
 
411
418
  for (const row of rows) {
@@ -413,55 +420,57 @@ export function migrateToolCreatedItems(): void {
413
420
  if (!prefix) continue;
414
421
 
415
422
  // Build content in the format the new tools expect
416
- const content =
417
- row.kind === "playbook"
418
- ? `${row.subject}\n${row.statement}`
419
- : `${row.subject}: ${row.statement}`;
423
+ const content = `${row.subject}\n${row.statement}`;
420
424
 
421
425
  // Check if already migrated (sourceKey exists in graph)
422
426
  const sourceKey = `${prefix}${row.id}`;
423
- const existing = db
424
- .select({ id: memoryGraphNodes.id })
425
- .from(memoryGraphNodes)
426
- .where(
427
- sql`${memoryGraphNodes.sourceConversations} LIKE ${
428
- "%" + sourceKey + "%"
429
- }`
430
- )
431
- .get();
427
+ const existing = rawGet<{ id: string }>(
428
+ `SELECT id FROM memory_graph_nodes WHERE source_conversations LIKE ?`,
429
+ `%${sourceKey}%`,
430
+ );
432
431
  if (existing) continue;
433
432
 
434
433
  const now = Date.now();
435
- const node: NewNode = {
434
+ const id = uuid();
435
+ const emotionalCharge = JSON.stringify({
436
+ valence: 0,
437
+ intensity: 0.1,
438
+ decayCurve: "linear",
439
+ decayRate: 0.05,
440
+ originalIntensity: 0.1,
441
+ });
442
+
443
+ rawRun(
444
+ `INSERT INTO memory_graph_nodes (
445
+ id, content, type, created, last_accessed, last_consolidated,
446
+ event_date, emotional_charge, fidelity, confidence, significance,
447
+ stability, reinforcement_count, last_reinforced,
448
+ source_conversations, source_type, narrative_role, part_of_story,
449
+ image_refs, scope_id
450
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
451
+ id,
436
452
  content,
437
- type: "semantic",
438
- created: row.first_seen_at || now,
439
- lastAccessed: now,
440
- lastConsolidated: now,
441
- eventDate: null,
442
- emotionalCharge: {
443
- valence: 0,
444
- intensity: 0.1,
445
- decayCurve: "linear",
446
- decayRate: 0.05,
447
- originalIntensity: 0.1,
448
- },
449
- fidelity: "vivid",
450
- confidence: row.confidence,
451
- significance: row.importance,
452
- stability: 14,
453
- reinforcementCount: 0,
454
- lastReinforced: now,
455
- sourceConversations: [sourceKey],
456
- sourceType: "direct",
457
- narrativeRole: null,
458
- partOfStory: null,
459
- imageRefs: null,
460
- scopeId: row.scope_id || "default",
461
- };
462
-
463
- const created = createNode(node);
464
- enqueueMemoryJob("embed_graph_node", { nodeId: created.id });
453
+ "semantic",
454
+ row.first_seen_at || now,
455
+ now,
456
+ now,
457
+ null,
458
+ emotionalCharge,
459
+ "vivid",
460
+ row.confidence,
461
+ row.importance,
462
+ 14,
463
+ 0,
464
+ now,
465
+ JSON.stringify([sourceKey]),
466
+ "direct",
467
+ null,
468
+ null,
469
+ null,
470
+ row.scope_id || "default",
471
+ );
472
+
473
+ enqueueMemoryJob("embed_graph_node", { nodeId: id });
465
474
  migrated++;
466
475
  }
467
476
 
@@ -470,7 +479,7 @@ export function migrateToolCreatedItems(): void {
470
479
  if (migrated > 0) {
471
480
  log.info(
472
481
  { migrated, total: rows.length },
473
- "Migrated tool-created items to graph nodes"
482
+ "Migrated tool-created items to graph nodes",
474
483
  );
475
484
  }
476
485
  }
@@ -506,7 +515,7 @@ export async function cleanupStaleItemVectors(): Promise<void> {
506
515
  } catch (err) {
507
516
  log.warn(
508
517
  { err: err instanceof Error ? err.message : String(err) },
509
- "Failed to clean up stale item vectors — will retry on next startup"
518
+ "Failed to clean up stale item vectors — will retry on next startup",
510
519
  );
511
520
  }
512
521
  }
@@ -2,17 +2,22 @@
2
2
  // Memory Graph — Capability seeding for skills and CLI commands
3
3
  //
4
4
  // Creates graph nodes for skill/CLI capabilities so they participate in
5
- // semantic retrieval. Mirrors the old memoryItems-based seeding in
6
- // skill-memory.ts and cli-memory.ts.
5
+ // semantic retrieval.
7
6
  // ---------------------------------------------------------------------------
8
7
 
9
- import { and, eq, like } from "drizzle-orm";
8
+ import { and, eq, like, sql } from "drizzle-orm";
10
9
 
11
10
  import { buildCliProgram } from "../../cli/program.js";
11
+ import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
12
12
  import { getConfig } from "../../config/loader.js";
13
13
  import { resolveSkillStates } from "../../config/skill-state.js";
14
14
  import { loadSkillCatalog } from "../../config/skills.js";
15
15
  import {
16
+ getCachedCatalogSync,
17
+ getCatalog,
18
+ } from "../../skills/catalog-cache.js";
19
+ import {
20
+ fromCatalogSkill,
16
21
  fromSkillSummary,
17
22
  type SkillCapabilityInput,
18
23
  } from "../../skills/skill-memory.js";
@@ -24,6 +29,9 @@ import { createNode } from "./store.js";
24
29
 
25
30
  const log = getLogger("graph-capability-seed");
26
31
 
32
+ /** Default significance for capability nodes. */
33
+ const CAPABILITY_SIGNIFICANCE = 0.6;
34
+
27
35
  /** Stable prefix for capability node source tracking. */
28
36
  const SKILL_SOURCE_PREFIX = "capability:skill:";
29
37
  const CLI_SOURCE_PREFIX = "capability:cli:";
@@ -107,11 +115,38 @@ export function seedSkillGraphNodes(): void {
107
115
  seenKeys.add(`${SKILL_SOURCE_PREFIX}${summary.id}`);
108
116
  }
109
117
 
110
- // Always prune stale capability nodesthis cleans up nodes from
111
- // previously-seeded uninstalled skills that are no longer refreshed.
112
- // seenKeys already contains all locally-installed-and-enabled skills
113
- // which is sufficient to identify what should be kept.
114
- pruneStaleCapabilities(SKILL_SOURCE_PREFIX, seenKeys);
118
+ // Protect uninstalled catalog skills from pruning they are seeded
119
+ // asynchronously by seedUninstalledCatalogSkillMemories() and should
120
+ // not be marked as "gone" just because they aren't locally installed.
121
+ // When the catalog cache is cold (empty before the async fetch
122
+ // completes), we can only prune locally managed skills; full
123
+ // catalog-based pruning waits until the cache is populated.
124
+ const cachedCatalog = getCachedCatalogSync();
125
+ if (cachedCatalog.length === 0) {
126
+ // Catalog cache is cold — we can't enumerate remote catalog skills, so
127
+ // skip catalog-based pruning to avoid incorrectly marking valid
128
+ // uninstalled catalog nodes as gone. But still prune locally disabled
129
+ // skills so stale capability nodes don't linger after cold start.
130
+ log.info("Catalog cache is cold — pruning only locally disabled skills");
131
+ const disabled = resolved.filter((r) => r.state !== "enabled");
132
+ for (const { summary } of disabled) {
133
+ deleteSkillCapabilityNode(summary.id);
134
+ }
135
+ } else {
136
+ for (const entry of cachedCatalog) {
137
+ const flagKey = entry.metadata?.vellum?.["feature-flag"];
138
+ if (flagKey && !isAssistantFeatureFlagEnabled(flagKey, config))
139
+ continue;
140
+ seenKeys.add(`${SKILL_SOURCE_PREFIX}${entry.id}`);
141
+ }
142
+ pruneStaleCapabilities(SKILL_SOURCE_PREFIX, seenKeys);
143
+ }
144
+
145
+ // Clean up old-format capability nodes (skill:* and cli:*) that use the
146
+ // legacy "{prefix}:{id}\n..." content format. Mark them as gone so they
147
+ // stop appearing as duplicates. Idempotent — once cleaned, subsequent
148
+ // runs find nothing.
149
+ cleanupOldFormatCapabilityNodes();
115
150
  } catch (err) {
116
151
  log.warn({ err }, "Failed to seed skill graph nodes");
117
152
  }
@@ -121,9 +156,9 @@ export function seedSkillGraphNodes(): void {
121
156
  * Seed graph nodes for all CLI commands.
122
157
  * Prunes stale nodes whose commands are no longer registered.
123
158
  */
124
- export function seedCliGraphNodes(): void {
159
+ export async function seedCliGraphNodes(): Promise<void> {
125
160
  try {
126
- const program = buildCliProgram();
161
+ const program = await buildCliProgram();
127
162
 
128
163
  const seenKeys = new Set<string>();
129
164
  for (const cmd of program.commands) {
@@ -137,6 +172,34 @@ export function seedCliGraphNodes(): void {
137
172
  }
138
173
  }
139
174
 
175
+ /**
176
+ * Seed capability graph nodes for catalog skills that are not yet installed.
177
+ * This makes uninstalled skills discoverable via memory injection so the LLM
178
+ * can auto-install them via skill_load when relevant.
179
+ * Best-effort: errors are logged but never thrown.
180
+ */
181
+ export async function seedUninstalledCatalogSkillMemories(): Promise<void> {
182
+ try {
183
+ const fullCatalog = await getCatalog();
184
+ if (fullCatalog.length === 0) return;
185
+
186
+ const installedCatalog = loadSkillCatalog();
187
+ const installedIds = new Set(installedCatalog.map((s) => s.id));
188
+
189
+ const config = getConfig();
190
+ for (const entry of fullCatalog) {
191
+ if (installedIds.has(entry.id)) continue;
192
+
193
+ const flagKey = entry.metadata?.vellum?.["feature-flag"];
194
+ if (flagKey && !isAssistantFeatureFlagEnabled(flagKey, config)) continue;
195
+
196
+ upsertSkillCapabilityNode(entry.id, fromCatalogSkill(entry));
197
+ }
198
+ } catch (err) {
199
+ log.warn({ err }, "Failed to seed uninstalled catalog skill memories");
200
+ }
201
+ }
202
+
140
203
  // ---------------------------------------------------------------------------
141
204
  // Internal helpers
142
205
  // ---------------------------------------------------------------------------
@@ -172,7 +235,7 @@ function upsertCapabilityNode(sourceKey: string, content: string): void {
172
235
  .where(
173
236
  and(
174
237
  eq(memoryGraphNodes.scopeId, "default"),
175
- like(memoryGraphNodes.sourceConversations, `%${sourceKey}%`),
238
+ eq(memoryGraphNodes.sourceConversations, JSON.stringify([sourceKey])),
176
239
  ),
177
240
  )
178
241
  .get();
@@ -181,9 +244,15 @@ function upsertCapabilityNode(sourceKey: string, content: string): void {
181
244
 
182
245
  if (existing) {
183
246
  if (existing.content === content && existing.fidelity !== "gone") {
184
- // Same content — just touch lastAccessed
247
+ // Same content — just touch lastAccessed (and backfill lastConsolidated
248
+ // for nodes created before the fix so they don't decay immediately,
249
+ // and backfill significance for nodes created before the raise to 0.6)
250
+ const updates: Record<string, number> = { lastAccessed: now };
251
+ if (existing.lastConsolidated === 0) updates.lastConsolidated = now;
252
+ if (existing.significance < CAPABILITY_SIGNIFICANCE)
253
+ updates.significance = CAPABILITY_SIGNIFICANCE;
185
254
  db.update(memoryGraphNodes)
186
- .set({ lastAccessed: now })
255
+ .set(updates)
187
256
  .where(eq(memoryGraphNodes.id, existing.id))
188
257
  .run();
189
258
  return;
@@ -195,6 +264,7 @@ function upsertCapabilityNode(sourceKey: string, content: string): void {
195
264
  content,
196
265
  fidelity: "vivid",
197
266
  lastAccessed: now,
267
+ ...(existing.lastConsolidated === 0 ? { lastConsolidated: now } : {}),
198
268
  })
199
269
  .where(eq(memoryGraphNodes.id, existing.id))
200
270
  .run();
@@ -208,7 +278,7 @@ function upsertCapabilityNode(sourceKey: string, content: string): void {
208
278
  type: "procedural" as const,
209
279
  created: now,
210
280
  lastAccessed: now,
211
- lastConsolidated: 0,
281
+ lastConsolidated: now,
212
282
  eventDate: null,
213
283
  emotionalCharge: {
214
284
  valence: 0,
@@ -219,7 +289,7 @@ function upsertCapabilityNode(sourceKey: string, content: string): void {
219
289
  },
220
290
  fidelity: "vivid" as const,
221
291
  confidence: 1.0,
222
- significance: 0.3,
292
+ significance: CAPABILITY_SIGNIFICANCE,
223
293
  stability: 1000, // Effectively permanent — never decays
224
294
  reinforcementCount: 0,
225
295
  lastReinforced: now,
@@ -246,7 +316,7 @@ function deleteCapabilityNode(sourceKey: string): void {
246
316
  .where(
247
317
  and(
248
318
  eq(memoryGraphNodes.scopeId, "default"),
249
- like(memoryGraphNodes.sourceConversations, `%${sourceKey}%`),
319
+ eq(memoryGraphNodes.sourceConversations, JSON.stringify([sourceKey])),
250
320
  ),
251
321
  )
252
322
  .get();
@@ -256,6 +326,79 @@ function deleteCapabilityNode(sourceKey: string): void {
256
326
  .set({ fidelity: "gone", lastAccessed: Date.now() })
257
327
  .where(eq(memoryGraphNodes.id, existing.id))
258
328
  .run();
329
+ enqueueMemoryJob("delete_qdrant_vectors", {
330
+ targetType: "graph_node",
331
+ targetId: existing.id,
332
+ });
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Find and soft-delete old-format capability memory nodes (skill:* and cli:*).
338
+ *
339
+ * The legacy system stored content as "skill:{id}\n{statement}" or
340
+ * "cli:{command}\n{statement}". The current system uses prose format.
341
+ * This marks any remaining old-format nodes as gone so they no longer
342
+ * surface in retrieval.
343
+ */
344
+ function cleanupOldFormatCapabilityNodes(): void {
345
+ const db = getDb();
346
+ const now = Date.now();
347
+
348
+ // --- skill:* old-format nodes ---
349
+ const oldFormatNodes = db
350
+ .select()
351
+ .from(memoryGraphNodes)
352
+ .where(
353
+ and(
354
+ eq(memoryGraphNodes.type, "procedural"),
355
+ eq(memoryGraphNodes.scopeId, "default"),
356
+ sql`${memoryGraphNodes.fidelity} != 'gone'`,
357
+ sql`${memoryGraphNodes.content} LIKE 'skill:%'`,
358
+ ),
359
+ )
360
+ .all();
361
+
362
+ for (const node of oldFormatNodes) {
363
+ // Verify this is truly old-format: "skill:{id}\n..."
364
+ if (!/^skill:\S+\n/.test(node.content)) continue;
365
+
366
+ db.update(memoryGraphNodes)
367
+ .set({ fidelity: "gone", lastAccessed: now })
368
+ .where(eq(memoryGraphNodes.id, node.id))
369
+ .run();
370
+ enqueueMemoryJob("delete_qdrant_vectors", {
371
+ targetType: "graph_node",
372
+ targetId: node.id,
373
+ });
374
+ log.info({ nodeId: node.id }, "Cleaned up old-format skill memory node");
375
+ }
376
+
377
+ // --- cli:* old-format nodes ---
378
+ const oldCliNodes = db
379
+ .select()
380
+ .from(memoryGraphNodes)
381
+ .where(
382
+ and(
383
+ eq(memoryGraphNodes.type, "procedural"),
384
+ eq(memoryGraphNodes.scopeId, "default"),
385
+ sql`${memoryGraphNodes.fidelity} != 'gone'`,
386
+ sql`${memoryGraphNodes.content} LIKE 'cli:%'`,
387
+ ),
388
+ )
389
+ .all();
390
+
391
+ for (const node of oldCliNodes) {
392
+ if (!/^cli:\S+\n/.test(node.content)) continue;
393
+ db.update(memoryGraphNodes)
394
+ .set({ fidelity: "gone", lastAccessed: now })
395
+ .where(eq(memoryGraphNodes.id, node.id))
396
+ .run();
397
+ enqueueMemoryJob("delete_qdrant_vectors", {
398
+ targetType: "graph_node",
399
+ targetId: node.id,
400
+ });
401
+ log.info({ nodeId: node.id }, "Cleaned up old-format CLI memory node");
259
402
  }
260
403
  }
261
404
 
@@ -284,11 +427,18 @@ function pruneStaleCapabilities(prefix: string, activeKeys: Set<string>): void {
284
427
  const sources = JSON.parse(row.sourceConversations as string);
285
428
  const key = Array.isArray(sources) ? sources[0] : null;
286
429
  if (key && typeof key === "string" && !activeKeys.has(key)) {
287
- log.info({ sourceKey: key, nodeId: row.id }, "Pruning stale capability graph node");
430
+ log.info(
431
+ { sourceKey: key, nodeId: row.id },
432
+ "Pruning stale capability graph node",
433
+ );
288
434
  db.update(memoryGraphNodes)
289
435
  .set({ fidelity: "gone", lastAccessed: now })
290
436
  .where(eq(memoryGraphNodes.id, row.id))
291
437
  .run();
438
+ enqueueMemoryJob("delete_qdrant_vectors", {
439
+ targetType: "graph_node",
440
+ targetId: row.id,
441
+ });
292
442
  }
293
443
  } catch {
294
444
  // Skip malformed JSON
@@ -19,13 +19,16 @@ import {
19
19
  } from "../../providers/provider-send-message.js";
20
20
  import { BackendUnavailableError } from "../../util/errors.js";
21
21
  import { getLogger } from "../../util/logger.js";
22
+ import { getDb } from "../db.js";
22
23
  import { parseEpochMs } from "./extraction.js";
23
24
  import {
24
25
  createTrigger,
26
+ deduplicateParagraphs,
25
27
  deleteNode,
26
28
  getEdgesForNode,
27
29
  getTriggersForNode,
28
30
  queryNodes,
31
+ recordNodeEdit,
29
32
  updateNode,
30
33
  } from "./store.js";
31
34
  import type { MemoryNode } from "./types.js";
@@ -60,7 +63,11 @@ function buildConsolidationPrompt(
60
63
  ? ` eventDate=${new Date(n.eventDate).toISOString().split("T")[0]}`
61
64
  : "";
62
65
  const imageStr = n.hasImage ? " [has_image]" : "";
63
- return ` [${n.id}] type=${n.type} sig=${n.significance.toFixed(2)} fidelity=${n.fidelity} reinforced=${n.reinforcementCount}x age=${age}d${eventStr}${imageStr}\n ${n.content}`;
66
+ return ` [${n.id}] type=${n.type} sig=${n.significance.toFixed(
67
+ 2,
68
+ )} fidelity=${n.fidelity} reinforced=${
69
+ n.reinforcementCount
70
+ }x age=${age}d${eventStr}${imageStr}\n ${n.content}`;
64
71
  })
65
72
  .join("\n\n");
66
73
 
@@ -194,7 +201,9 @@ function getTopSignificanceNodes(
194
201
  scopeId,
195
202
  fidelityNot: ["gone"],
196
203
  minSignificance: 0.6,
197
- }).filter((n) => !isCapabilityNode(n)).slice(0, n);
204
+ })
205
+ .filter((n) => !isCapabilityNode(n))
206
+ .slice(0, n);
198
207
  }
199
208
 
200
209
  function getDecayedNodes(scopeId: string): MemoryNode[] {
@@ -202,7 +211,10 @@ function getDecayedNodes(scopeId: string): MemoryNode[] {
202
211
  scopeId,
203
212
  limit: 10000,
204
213
  });
205
- return all.filter((n) => (n.fidelity === "faded" || n.fidelity === "gist") && !isCapabilityNode(n));
214
+ return all.filter(
215
+ (n) =>
216
+ (n.fidelity === "faded" || n.fidelity === "gist") && !isCapabilityNode(n),
217
+ );
206
218
  }
207
219
 
208
220
  function getRandomSample(scopeId: string, n: number = 30): MemoryNode[] {
@@ -506,8 +518,30 @@ async function consolidateChunk(
506
518
 
507
519
  if (Object.keys(changes).length > 1) {
508
520
  // more than just lastConsolidated
509
- updateNode(update.id, changes);
521
+
522
+ // Wrap edit recording + node update in a transaction so they are atomic:
523
+ // if updateNode fails, the edit record is rolled back.
524
+ getDb().transaction(() => {
525
+ if (changes.content) {
526
+ const cleanContent = deduplicateParagraphs(changes.content);
527
+ const node = nodeMap.get(update.id);
528
+ if (node && node.content !== cleanContent) {
529
+ recordNodeEdit({
530
+ nodeId: update.id,
531
+ previousContent: node.content,
532
+ newContent: cleanContent,
533
+ source: "consolidation",
534
+ });
535
+ }
536
+ }
537
+
538
+ updateNode(update.id, changes);
539
+ });
510
540
  result.nodesUpdated++;
541
+ // Sync in-memory state with what updateNode actually wrote to the DB
542
+ // (updateNode deduplicates content before persisting)
543
+ if (changes.content)
544
+ changes.content = deduplicateParagraphs(changes.content);
511
545
  const node = nodeMap.get(update.id);
512
546
  if (node) Object.assign(node, changes);
513
547
  }