@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
@@ -6,7 +6,7 @@
6
6
  // retrieval mode based on conversation state.
7
7
  // ---------------------------------------------------------------------------
8
8
 
9
- import { and, desc, eq, ne } from "drizzle-orm";
9
+ import { and, desc, eq, inArray, ne, notInArray } from "drizzle-orm";
10
10
 
11
11
  import type { AssistantConfig } from "../../config/types.js";
12
12
  import { estimateTextTokens } from "../../context/token-estimator.js";
@@ -20,22 +20,22 @@ import { getLogger } from "../../util/logger.js";
20
20
  import { getDb } from "../db.js";
21
21
  import { memorySummaries } from "../schema.js";
22
22
  import { conversations } from "../schema/conversations.js";
23
+ import {
24
+ loadGraphMemoryState,
25
+ saveGraphMemoryState,
26
+ } from "./graph-memory-state-store.js";
23
27
  import {
24
28
  assembleContextBlock,
25
29
  assembleInjectionBlock,
26
30
  InContextTracker,
31
+ type InContextTrackerSnapshot,
27
32
  MAX_CONTEXT_LOAD_IMAGES,
28
33
  MAX_PER_TURN_IMAGES,
29
- MAX_REFRESH_IMAGES,
30
34
  type ResolvedImage,
31
35
  resolveInjectionImages,
32
36
  } from "./injection.js";
33
- import {
34
- loadContextMemory,
35
- REFRESH_INTERVAL_TURNS,
36
- refreshContextMemory,
37
- retrieveForTurn,
38
- } from "./retriever.js";
37
+ import { loadContextMemory, retrieveForTurn } from "./retriever.js";
38
+ import type { RetrievalMetrics } from "./types.js";
39
39
 
40
40
  const log = getLogger("graph-conversation-memory");
41
41
 
@@ -51,10 +51,9 @@ const ESTIMATED_IMAGE_TOKENS = 1000;
51
51
  */
52
52
  export class ConversationGraphMemory {
53
53
  readonly tracker = new InContextTracker();
54
- private turnCount = 0;
55
54
  private initialized = false;
56
- private lastCompactedAt: number | null = null;
57
55
  private needsReload = false;
56
+ private stateRestored = false;
58
57
  private scopeId: string;
59
58
  private conversationId: string;
60
59
  private lastInjectedBlock: string | null = null;
@@ -66,6 +65,65 @@ export class ConversationGraphMemory {
66
65
  this.conversationId = conversationId;
67
66
  }
68
67
 
68
+ /**
69
+ * Persist tracker state to the database so it survives eviction.
70
+ * Called during conversation disposal.
71
+ */
72
+ persistState(): void {
73
+ if (!this.initialized) return;
74
+ try {
75
+ const snapshot: InContextTrackerSnapshot & {
76
+ initialized: boolean;
77
+ needsReload: boolean;
78
+ } = {
79
+ initialized: this.initialized,
80
+ needsReload: this.needsReload,
81
+ ...this.tracker.toJSON(),
82
+ };
83
+ saveGraphMemoryState(this.conversationId, JSON.stringify(snapshot));
84
+ } catch (err) {
85
+ log.warn(
86
+ { err: err instanceof Error ? err.message : String(err) },
87
+ "Failed to persist graph memory state (non-fatal)",
88
+ );
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Restore tracker state from the database after eviction + recreation.
94
+ * On failure or missing row, silently falls back to full context-load.
95
+ */
96
+ restoreState(): void {
97
+ if (this.stateRestored) return;
98
+ try {
99
+ const json = loadGraphMemoryState(this.conversationId);
100
+ if (!json) return;
101
+
102
+ const snapshot = JSON.parse(json) as InContextTrackerSnapshot & {
103
+ initialized: boolean;
104
+ needsReload?: boolean;
105
+ };
106
+ this.initialized = snapshot.initialized;
107
+ this.needsReload = snapshot.needsReload ?? false;
108
+ this.tracker.restoreFrom(snapshot);
109
+ this.stateRestored = true;
110
+
111
+ log.info(
112
+ {
113
+ conversationId: this.conversationId,
114
+ turn: snapshot.currentTurn,
115
+ inContextCount: snapshot.inContext.length,
116
+ },
117
+ "Restored graph memory state after eviction",
118
+ );
119
+ } catch (err) {
120
+ log.warn(
121
+ { err: err instanceof Error ? err.message : String(err) },
122
+ "Failed to restore graph memory state — will do full context load",
123
+ );
124
+ }
125
+ }
126
+
69
127
  /**
70
128
  * Fetch the most recent conversation summaries (excluding the current
71
129
  * conversation, which won't have one yet at context-load time).
@@ -93,7 +151,15 @@ export class ConversationGraphMemory {
93
151
  conversations,
94
152
  eq(memorySummaries.scopeKey, conversations.id),
95
153
  )
96
- .where(and(baseWhere, ne(conversations.conversationType, "background")))
154
+ .where(
155
+ and(
156
+ baseWhere,
157
+ notInArray(conversations.conversationType, [
158
+ "background",
159
+ "scheduled",
160
+ ]),
161
+ ),
162
+ )
97
163
  .orderBy(desc(memorySummaries.updatedAt))
98
164
  .limit(3)
99
165
  .all();
@@ -102,7 +168,7 @@ export class ConversationGraphMemory {
102
168
  return userRows.map((r) => r.summary);
103
169
  }
104
170
 
105
- // Fill remaining slots with at most 1 background conversation
171
+ // Fill remaining slots with at most 1 background/scheduled conversation
106
172
  const remaining = Math.min(1, 3 - userRows.length);
107
173
  const bgRows = db
108
174
  .select({ summary: memorySummaries.summary })
@@ -111,7 +177,15 @@ export class ConversationGraphMemory {
111
177
  conversations,
112
178
  eq(memorySummaries.scopeKey, conversations.id),
113
179
  )
114
- .where(and(baseWhere, eq(conversations.conversationType, "background")))
180
+ .where(
181
+ and(
182
+ baseWhere,
183
+ inArray(conversations.conversationType, [
184
+ "background",
185
+ "scheduled",
186
+ ]),
187
+ ),
188
+ )
115
189
  .orderBy(desc(memorySummaries.updatedAt))
116
190
  .limit(remaining)
117
191
  .all();
@@ -133,7 +207,6 @@ export class ConversationGraphMemory {
133
207
  // so we conservatively clear everything and reload.
134
208
  this.tracker.evictCompactedTurns(this.tracker.getTurn());
135
209
  this.needsReload = true;
136
- this.lastCompactedAt = Date.now();
137
210
  log.info(
138
211
  { compactedMessageCount },
139
212
  "Compaction detected — will reload context on next turn",
@@ -184,7 +257,6 @@ export class ConversationGraphMemory {
184
257
  *
185
258
  * Dispatches to the appropriate retrieval mode:
186
259
  * - Turn 1 (or after compaction): full context load
187
- * - Every 5 turns: periodic refresh
188
260
  * - Every other turn: per-turn injection
189
261
  *
190
262
  * Returns augmented messages with memory context prepended to the last
@@ -199,11 +271,12 @@ export class ConversationGraphMemory {
199
271
  runMessages: Message[];
200
272
  injectedTokens: number;
201
273
  latencyMs: number;
202
- mode: "context-load" | "refresh" | "per-turn" | "none";
274
+ mode: "context-load" | "per-turn" | "none";
203
275
  /** The raw text content of the injected block (without XML wrapper), or null if nothing was injected. */
204
276
  injectedBlockText: string | null;
277
+ /** Retrieval pipeline metrics (null for noop/error paths). */
278
+ metrics: RetrievalMetrics | null;
205
279
  }> {
206
- this.turnCount++;
207
280
  this.tracker.advanceTurn();
208
281
 
209
282
  const noopResult = {
@@ -212,6 +285,7 @@ export class ConversationGraphMemory {
212
285
  latencyMs: 0,
213
286
  mode: "none" as const,
214
287
  injectedBlockText: null as string | null,
288
+ metrics: null as RetrievalMetrics | null,
215
289
  };
216
290
 
217
291
  // Gate: skip for empty/tool-result-only messages — unless we need to
@@ -245,10 +319,6 @@ export class ConversationGraphMemory {
245
319
  );
246
320
  }
247
321
 
248
- if (this.turnCount % REFRESH_INTERVAL_TURNS === 0) {
249
- return await this.runRefresh(messages, config, abortSignal);
250
- }
251
-
252
322
  return await this.runPerTurn(messages, config, abortSignal);
253
323
  } catch (err) {
254
324
  log.warn(
@@ -290,6 +360,7 @@ export class ConversationGraphMemory {
290
360
  latencyMs: result.latencyMs,
291
361
  mode: "context-load" as const,
292
362
  injectedBlockText: null,
363
+ metrics: result.metrics,
293
364
  };
294
365
  }
295
366
 
@@ -308,6 +379,7 @@ export class ConversationGraphMemory {
308
379
  latencyMs: result.latencyMs,
309
380
  mode: "context-load" as const,
310
381
  injectedBlockText: null,
382
+ metrics: result.metrics,
311
383
  };
312
384
  }
313
385
 
@@ -339,80 +411,7 @@ export class ConversationGraphMemory {
339
411
  latencyMs: result.latencyMs,
340
412
  mode: "context-load" as const,
341
413
  injectedBlockText: contextBlock,
342
- };
343
- }
344
-
345
- private async runRefresh(
346
- messages: Message[],
347
- config: AssistantConfig,
348
- signal: AbortSignal,
349
- ) {
350
- // Build recent turns text from the last ~6 messages
351
- const recentTurns = messages
352
- .slice(-6)
353
- .map((m) => {
354
- const textBlocks = m.content.filter(
355
- (b): b is Extract<typeof b, { type: "text" }> => b.type === "text",
356
- );
357
- if (textBlocks.length === 0) return "";
358
- return `[${m.role}]: ${textBlocks.map((b) => b.text).join(" ")}`;
359
- })
360
- .filter((t) => t.length > 0)
361
- .join("\n\n");
362
-
363
- const result = await refreshContextMemory({
364
- recentTurnsText: recentTurns,
365
- scopeId: this.scopeId,
366
- config,
367
- tracker: this.tracker,
368
- signal,
369
- });
370
-
371
- if (result.nodes.length === 0) {
372
- this.lastInjectedBlock = null;
373
- this.lastInjectedNodeIds = [];
374
- this.lastInjectedImages = new Map();
375
- return {
376
- runMessages: messages,
377
- injectedTokens: 0,
378
- latencyMs: result.latencyMs,
379
- mode: "refresh" as const,
380
- injectedBlockText: null,
381
- };
382
- }
383
-
384
- // Track new nodes
385
- this.tracker.add(result.nodes.map((n) => n.node.id));
386
-
387
- const injectionBlock = assembleInjectionBlock(result.nodes);
388
- if (!injectionBlock) {
389
- return {
390
- runMessages: messages,
391
- injectedTokens: 0,
392
- latencyMs: result.latencyMs,
393
- mode: "refresh" as const,
394
- injectedBlockText: null,
395
- };
396
- }
397
-
398
- // Resolve images from scored nodes
399
- const images = await resolveInjectionImages(
400
- result.nodes,
401
- MAX_REFRESH_IMAGES,
402
- );
403
-
404
- this.lastInjectedBlock = injectionBlock;
405
- this.lastInjectedNodeIds = result.nodes.map((n) => n.node.id);
406
- this.lastInjectedImages = images;
407
-
408
- return {
409
- runMessages: injectMemoryBlock(messages, injectionBlock, images),
410
- injectedTokens:
411
- estimateTextTokens(injectionBlock) +
412
- images.size * ESTIMATED_IMAGE_TOKENS,
413
- latencyMs: result.latencyMs,
414
- mode: "refresh" as const,
415
- injectedBlockText: injectionBlock,
414
+ metrics: result.metrics,
416
415
  };
417
416
  }
418
417
 
@@ -466,6 +465,7 @@ export class ConversationGraphMemory {
466
465
  latencyMs: result.latencyMs,
467
466
  mode: "per-turn" as const,
468
467
  injectedBlockText: null,
468
+ metrics: result.metrics,
469
469
  };
470
470
  }
471
471
 
@@ -480,6 +480,7 @@ export class ConversationGraphMemory {
480
480
  latencyMs: result.latencyMs,
481
481
  mode: "per-turn" as const,
482
482
  injectedBlockText: null,
483
+ metrics: result.metrics,
483
484
  };
484
485
  }
485
486
 
@@ -501,6 +502,7 @@ export class ConversationGraphMemory {
501
502
  latencyMs: result.latencyMs,
502
503
  mode: "per-turn" as const,
503
504
  injectedBlockText: injectionBlock,
505
+ metrics: result.metrics,
504
506
  };
505
507
  }
506
508
  }
@@ -513,20 +515,26 @@ export class ConversationGraphMemory {
513
515
  * Remove all memory-injected blocks from the last user message.
514
516
  *
515
517
  * `injectMemoryBlock` always prepends blocks in this order:
516
- * 1. `<memory __injected>…</memory>` text block
517
- * 2. For each image: `<memory_image>…</memory_image>` text + `image` block
518
+ * 1. For each image: `<memory_image __injected>…` text + `image` + `</memory_image>` text (3-block group)
519
+ * 2. `<memory __injected>…</memory>` text block
518
520
  *
519
521
  * We strip all leading blocks that match this pattern so that
520
522
  * `reinjectCachedMemory` is idempotent — no duplicate images after compaction.
521
523
  */
522
- function stripExistingMemoryInjections(messages: Message[]): Message[] {
524
+ export function stripExistingMemoryInjections(messages: Message[]): Message[] {
523
525
  if (messages.length === 0) return messages;
524
526
  const last = messages[messages.length - 1];
525
527
  if (!last || last.role !== "user") return messages;
526
528
 
527
529
  // Walk from the front and skip all memory-injected blocks.
528
530
  // The injection prefix is always contiguous at the start of content.
531
+ // Memory-injected images use a 3-block pattern: opening <memory_image> text,
532
+ // image block, closing </memory_image> text (see injectMemoryBlock).
533
+ // Legacy 2-block pattern (no closing tag) is also handled for backward compat.
534
+ // Only strip image blocks that follow a marker — user-attached images must be preserved.
529
535
  let firstNonMemory = 0;
536
+ let prevWasMemoryImageMarker = false;
537
+ let prevWasInjectedImage = false;
530
538
  const content = last.content;
531
539
  while (firstNonMemory < content.length) {
532
540
  const block = content[firstNonMemory];
@@ -535,13 +543,27 @@ function stripExistingMemoryInjections(messages: Message[]): Message[] {
535
543
  block.text.startsWith("<memory __injected>\n")
536
544
  ) {
537
545
  firstNonMemory++;
546
+ prevWasMemoryImageMarker = false;
547
+ prevWasInjectedImage = false;
538
548
  } else if (
539
549
  block.type === "text" &&
540
- block.text.startsWith("<memory_image>")
550
+ block.text.startsWith("<memory_image")
541
551
  ) {
542
552
  firstNonMemory++;
543
- } else if (block.type === "image") {
553
+ prevWasMemoryImageMarker = true;
554
+ prevWasInjectedImage = false;
555
+ } else if (block.type === "image" && prevWasMemoryImageMarker) {
556
+ firstNonMemory++;
557
+ prevWasMemoryImageMarker = false;
558
+ prevWasInjectedImage = true;
559
+ } else if (
560
+ block.type === "text" &&
561
+ block.text === "</memory_image>" &&
562
+ prevWasInjectedImage
563
+ ) {
564
+ // Closing tag from the 3-block pattern — only strip after an injected image
544
565
  firstNonMemory++;
566
+ prevWasInjectedImage = false;
545
567
  } else {
546
568
  break;
547
569
  }
@@ -592,14 +614,12 @@ function injectMemoryBlock(
592
614
  const userTail = cleaned[cleaned.length - 1];
593
615
  if (!userTail || userTail.role !== "user") return messages;
594
616
 
595
- const blocks: ContentBlock[] = [
596
- { type: "text" as const, text: `<memory __injected>\n${text}\n</memory>` },
597
- ];
617
+ const blocks: ContentBlock[] = [];
598
618
 
599
619
  for (const [_nodeId, img] of images) {
600
620
  blocks.push({
601
621
  type: "text" as const,
602
- text: `<memory_image>${img.description}</memory_image>`,
622
+ text: `<memory_image __injected>\n${img.description}`,
603
623
  });
604
624
  blocks.push({
605
625
  type: "image" as const,
@@ -609,8 +629,17 @@ function injectMemoryBlock(
609
629
  data: img.base64Data,
610
630
  },
611
631
  } as ImageContent);
632
+ blocks.push({
633
+ type: "text" as const,
634
+ text: `</memory_image>`,
635
+ });
612
636
  }
613
637
 
638
+ blocks.push({
639
+ type: "text" as const,
640
+ text: `<memory __injected>\n${text}\n</memory>`,
641
+ });
642
+
614
643
  return [
615
644
  ...cleaned.slice(0, -1),
616
645
  { ...userTail, content: [...blocks, ...userTail.content] },
@@ -22,10 +22,10 @@ const log = getLogger("graph-extraction-job");
22
22
  * Checkpoint key: `graph_extract:<conversationId>:last_ts`
23
23
  * Value: epoch ms of the most recent message processed.
24
24
  *
25
- * This mirrors the old batch_extract pattern:
26
- * - Triggered by indexer after batchSize messages (default 10)
27
- * - Also triggered by indexer idle debounce (default 300s)
28
- * - Also triggered by conversation dispose (end of conversation)
25
+ * Trigger sources:
26
+ * - Indexer after batchSize messages (default 10)
27
+ * - Indexer idle debounce (default 300s)
28
+ * - Conversation dispose (end of conversation)
29
29
  */
30
30
  export async function graphExtractJob(
31
31
  job: MemoryJob,
@@ -40,9 +40,14 @@ export async function graphExtractJob(
40
40
  const lastTs = getMemoryCheckpoint(checkpointKey);
41
41
  const afterTimestamp = lastTs ? parseInt(lastTs, 10) : undefined;
42
42
 
43
+ const activeContextNodeIds = Array.isArray(job.payload.activeContextNodeIds)
44
+ ? (job.payload.activeContextNodeIds as string[])
45
+ : undefined;
46
+
43
47
  try {
44
48
  const result = await runGraphExtraction(conversationId, scopeId, config, {
45
49
  afterTimestamp,
50
+ activeContextNodeIds,
46
51
  });
47
52
 
48
53
  // Update checkpoint to the newest message actually processed — using
@@ -60,6 +60,7 @@ const EXTRACTION_SYSTEM_PROMPT_CHAR_BUDGET = 24_000;
60
60
  function buildGraphExtractionSystemPrompt(
61
61
  candidateNodes: Array<{ id: string; type: string; content: string }>,
62
62
  identityContext: string | null,
63
+ activeContextNodeIds?: Set<string>,
63
64
  ): string {
64
65
  const instructions = `You are the memory consolidation process for an AI assistant. A conversation just ended.
65
66
  Your job is to extract memories worth keeping and produce a structured diff.
@@ -70,6 +71,8 @@ Call the \`extract_graph_diff\` tool with the diff. Each node needs:
70
71
 
71
72
  - **content**: First-person prose — how the assistant naturally remembers this. Write naturally, not as a database entry. E.g. "He mentioned his mom used to make amazing Sunday dinners — he still misses them" not "User's mother cooked Sunday dinners."
72
73
 
74
+ Be concise — most memories should be 1-3 sentences capturing the essential detail and emotional weight. Don't narrate every nuance; write a vivid snapshot, not a journal entry. Higher-significance (0.7+) memories can use a short paragraph, but even those should stay focused.
75
+
73
76
  - **type**: Classify by WHAT the memory IS, not how it FEELS. Almost every memory has emotional weight — that goes in emotionalCharge, not the type.
74
77
 
75
78
  - **episodic**: A specific moment or event. "We stayed up until 4 AM debugging the pipeline." "The first time we deployed to production." Use this for things that HAPPENED.
@@ -135,7 +138,44 @@ Do NOT attach images that are incidental (screenshots of error messages fully de
135
138
 
136
139
  Write detailed descriptions — these are used for text-based retrieval when visual search isn't available.
137
140
 
138
- ## Candidate Nodes (existing memories)
141
+ ${(() => {
142
+ const reconsolidationNodes = activeContextNodeIds?.size
143
+ ? candidateNodes.filter((n) => activeContextNodeIds.has(n.id))
144
+ : [];
145
+ const otherCandidates = activeContextNodeIds?.size
146
+ ? candidateNodes.filter((n) => !activeContextNodeIds.has(n.id))
147
+ : candidateNodes;
148
+
149
+ const reconsolidationSection =
150
+ reconsolidationNodes.length > 0
151
+ ? `## Reconsolidation Window
152
+
153
+ These memories were ACTIVELY RECALLED during this conversation — the user and
154
+ assistant both saw them. Recalled memories are in a reconsolidation window and
155
+ should be the FIRST candidates for updating with new information.
156
+
157
+ When a recalled memory relates to what was discussed:
158
+ - Conversation CONFIRMS what the memory says → REINFORCE it
159
+ - Conversation adds new detail or nuance → UPDATE it with richer content
160
+ - Conversation reveals the memory is outdated or wrong → UPDATE it or create a superseding node
161
+ - Conversation is unrelated to this memory → leave it alone
162
+
163
+ STRONG PREFERENCE: Update a recalled memory rather than creating a new node that
164
+ partially overlaps. The recalled memory already has history, reinforcement count,
165
+ and edge connections — enriching it preserves that context graph.
166
+
167
+ ### Recalled memories
168
+ ${reconsolidationNodes.map((n) => `- [${n.id}] (${n.type}) ${n.content}`).join("\n")}
169
+
170
+ `
171
+ : "";
172
+
173
+ const candidateHeader =
174
+ reconsolidationNodes.length > 0
175
+ ? "## Other Candidate Nodes (existing memories not in this conversation)"
176
+ : "## Candidate Nodes (existing memories)";
177
+
178
+ const candidateSection = `${candidateHeader}
139
179
 
140
180
  Check these CAREFULLY for overlap before creating any new node:
141
181
 
@@ -153,7 +193,10 @@ Common duplicate mistakes to avoid:
153
193
  - Same fact restated in a later conversation → REINFORCE, don't create
154
194
  - An update to an existing situation (e.g. "project is now done") → UPDATE the existing node, don't create a parallel one
155
195
 
156
- ${candidateNodes.length > 0 ? `### Existing memories (candidates for connection/reinforcement)\n${candidateNodes.map((n) => `- [${n.id}] (${n.type}) ${n.content}`).join("\n")}` : "No existing memories found — this may be an early conversation."}
196
+ ${otherCandidates.length > 0 ? `### Existing memories (candidates for connection/reinforcement)\n${otherCandidates.map((n) => `- [${n.id}] (${n.type}) ${n.content}`).join("\n")}` : reconsolidationNodes.length > 0 ? "All existing memories are shown in the reconsolidation section above." : "No existing memories found — this may be an early conversation."}`;
197
+
198
+ return reconsolidationSection + candidateSection;
199
+ })()}
157
200
  `;
158
201
 
159
202
  let prompt = instructions;
@@ -401,7 +444,7 @@ interface RawUpdateNode {
401
444
  significance?: number;
402
445
  confidence?: number;
403
446
  fidelity?: string;
404
- event_date?: number;
447
+ event_date?: number | null;
405
448
  }
406
449
 
407
450
  interface RawNewEdge {
@@ -601,19 +644,18 @@ export function parseExtractionResponse(
601
644
  if (
602
645
  node.eventDate != null &&
603
646
  (!Array.isArray(raw.triggers) ||
604
- !raw.triggers.some(
605
- (t) => t.type === "event" && t.event_date != null,
606
- ))
647
+ !raw.triggers.some((t) => t.type === "event" && t.event_date != null))
607
648
  ) {
608
- // Remove any malformed event triggers (type=event but missing event_date)
609
- const malformedIdx = deferredTriggers.findIndex(
610
- (dt) =>
649
+ // Remove all malformed event triggers (type=event but missing event_date)
650
+ for (let i = deferredTriggers.length - 1; i >= 0; i--) {
651
+ const dt = deferredTriggers[i];
652
+ if (
611
653
  dt.newNodeIndex === nodeIndex &&
612
654
  dt.trigger.type === "event" &&
613
- dt.trigger.eventDate == null,
614
- );
615
- if (malformedIdx !== -1) {
616
- deferredTriggers.splice(malformedIdx, 1);
655
+ dt.trigger.eventDate == null
656
+ ) {
657
+ deferredTriggers.splice(i, 1);
658
+ }
617
659
  }
618
660
 
619
661
  deferredTriggers.push({
@@ -674,7 +716,8 @@ export function parseExtractionResponse(
674
716
  ["vivid", "clear", "faded", "gist"].includes(raw.fidelity)
675
717
  )
676
718
  changes.fidelity = raw.fidelity;
677
- if (raw.event_date !== undefined) changes.eventDate = parseEpochMs(raw.event_date);
719
+ if (raw.event_date !== undefined)
720
+ changes.eventDate = parseEpochMs(raw.event_date);
678
721
  if (Object.keys(changes).length > 0) {
679
722
  diff.updateNodes.push({ id: raw.id, changes });
680
723
  }
@@ -773,9 +816,7 @@ export async function runGraphExtraction(
773
816
  // from the multimodal message content blocks for candidate search.
774
817
  if (imageResult) {
775
818
  transcript = imageResult.message.content
776
- .filter(
777
- (b): b is { type: "text"; text: string } => b.type === "text",
778
- )
819
+ .filter((b): b is { type: "text"; text: string } => b.type === "text")
779
820
  .map((b) => b.text)
780
821
  .join("\n");
781
822
  }
@@ -818,9 +859,14 @@ export async function runGraphExtraction(
818
859
  userPersona: userPersona ?? undefined,
819
860
  });
820
861
 
862
+ const activeSet = opts?.activeContextNodeIds
863
+ ? new Set(opts.activeContextNodeIds)
864
+ : undefined;
865
+
821
866
  const systemPrompt = buildGraphExtractionSystemPrompt(
822
867
  candidateNodes.map((n) => ({ id: n.id, type: n.type, content: n.content })),
823
868
  identityContext,
869
+ activeSet,
824
870
  );
825
871
 
826
872
  // 5. Resolve conversation timestamp before the LLM call so we can include
@@ -904,7 +950,7 @@ export async function runGraphExtraction(
904
950
  }
905
951
 
906
952
  // 8. Apply the diff
907
- const result = applyDiff(diff);
953
+ const result = applyDiff(diff, { conversationId });
908
954
 
909
955
  // 9. Apply deferred edges and triggers using the created node IDs
910
956
  const createdNodeIds = result.createdNodeIds;
@@ -1164,11 +1210,8 @@ export function loadTranscriptWithImages(
1164
1210
  for (let i = 0; i < parsed.length; i++) {
1165
1211
  const block = parsed[i];
1166
1212
  if (block?.type === "text") {
1167
- const rawText =
1168
- typeof block.text === "string" ? block.text : "";
1169
- const text = prefixAdded
1170
- ? rawText
1171
- : `[${row.role}]: ${rawText}`;
1213
+ const rawText = typeof block.text === "string" ? block.text : "";
1214
+ const text = prefixAdded ? rawText : `[${row.role}]: ${rawText}`;
1172
1215
  prefixAdded = true;
1173
1216
  totalTextLength += text.length;
1174
1217
  contentBlocks.push({ type: "text", text });
@@ -0,0 +1,37 @@
1
+ import { eq } from "drizzle-orm";
2
+
3
+ import { getDb } from "../db.js";
4
+ import { conversationGraphMemoryState } from "../schema.js";
5
+
6
+ /**
7
+ * Persist graph memory state for a conversation (upsert).
8
+ */
9
+ export function saveGraphMemoryState(
10
+ conversationId: string,
11
+ stateJson: string,
12
+ ): void {
13
+ const db = getDb();
14
+ const now = Date.now();
15
+ db.insert(conversationGraphMemoryState)
16
+ .values({ conversationId, stateJson, createdAt: now, updatedAt: now })
17
+ .onConflictDoUpdate({
18
+ target: conversationGraphMemoryState.conversationId,
19
+ set: { stateJson, updatedAt: now },
20
+ })
21
+ .run();
22
+ }
23
+
24
+ /**
25
+ * Load graph memory state for a conversation, or null if none exists.
26
+ */
27
+ export function loadGraphMemoryState(
28
+ conversationId: string,
29
+ ): string | null {
30
+ const db = getDb();
31
+ const row = db
32
+ .select({ stateJson: conversationGraphMemoryState.stateJson })
33
+ .from(conversationGraphMemoryState)
34
+ .where(eq(conversationGraphMemoryState.conversationId, conversationId))
35
+ .get();
36
+ return row?.stateJson ?? null;
37
+ }