@vellumai/assistant 0.5.16 → 0.6.1

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 (592) hide show
  1. package/AGENTS.md +4 -0
  2. package/ARCHITECTURE.md +69 -16
  3. package/Dockerfile +2 -5
  4. package/bun.lock +6 -2
  5. package/docker-entrypoint.sh +32 -1
  6. package/docs/architecture/integrations.md +1 -1
  7. package/docs/architecture/memory.md +21 -24
  8. package/knip.json +2 -1
  9. package/openapi.yaml +1198 -83
  10. package/package.json +5 -1
  11. package/src/__tests__/actor-token-service.test.ts +68 -0
  12. package/src/__tests__/agent-loop.test.ts +0 -32
  13. package/src/__tests__/always-loaded-tools-guard.test.ts +2 -2
  14. package/src/__tests__/anthropic-provider.test.ts +217 -98
  15. package/src/__tests__/app-compiler.test.ts +120 -0
  16. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  17. package/src/__tests__/app-executors.test.ts +47 -1
  18. package/src/__tests__/app-source-watcher.test.ts +159 -0
  19. package/src/__tests__/assistant-feature-flags-integration.test.ts +2 -2
  20. package/src/__tests__/call-conversation-messages.test.ts +2 -6
  21. package/src/__tests__/call-domain.test.ts +2 -6
  22. package/src/__tests__/call-pointer-messages.test.ts +2 -14
  23. package/src/__tests__/call-recovery.test.ts +2 -6
  24. package/src/__tests__/call-routes-http.test.ts +2 -6
  25. package/src/__tests__/call-store.test.ts +2 -6
  26. package/src/__tests__/cancel-resolves-conversation-key.test.ts +2 -6
  27. package/src/__tests__/canonical-guardian-store.test.ts +2 -6
  28. package/src/__tests__/channel-delivery-store.test.ts +2 -6
  29. package/src/__tests__/channel-retry-sweep.test.ts +2 -6
  30. package/src/__tests__/checker.test.ts +63 -9
  31. package/src/__tests__/clawhub.test.ts +54 -24
  32. package/src/__tests__/cli-command-risk-guard.test.ts +14 -0
  33. package/src/__tests__/config-schema.test.ts +6 -1
  34. package/src/__tests__/config-set-platform-guard.test.ts +302 -0
  35. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +2 -6
  36. package/src/__tests__/contacts-tools.test.ts +31 -0
  37. package/src/__tests__/context-overflow-reducer.test.ts +86 -0
  38. package/src/__tests__/context-token-estimator.test.ts +175 -10
  39. package/src/__tests__/conversation-agent-loop-overflow.test.ts +13 -6
  40. package/src/__tests__/conversation-agent-loop.test.ts +13 -51
  41. package/src/__tests__/conversation-attachments.test.ts +2 -6
  42. package/src/__tests__/conversation-attention-store.test.ts +2 -6
  43. package/src/__tests__/conversation-clear-safety.test.ts +2 -6
  44. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +4 -10
  45. package/src/__tests__/conversation-disk-view-integration.test.ts +2 -6
  46. package/src/__tests__/conversation-disk-view.test.ts +2 -6
  47. package/src/__tests__/conversation-error.test.ts +33 -2
  48. package/src/__tests__/conversation-fork-crud.test.ts +2 -6
  49. package/src/__tests__/conversation-history-web-search.test.ts +6 -1
  50. package/src/__tests__/conversation-load-history-repair.test.ts +5 -1
  51. package/src/__tests__/conversation-media-retry.test.ts +91 -0
  52. package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
  53. package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
  54. package/src/__tests__/conversation-starter-routes.test.ts +20 -11
  55. package/src/__tests__/conversation-store.test.ts +2 -6
  56. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
  57. package/src/__tests__/conversation-usage.test.ts +2 -6
  58. package/src/__tests__/conversation-wipe.test.ts +13 -414
  59. package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
  60. package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
  61. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  62. package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
  63. package/src/__tests__/credential-execution-feature-gates.test.ts +3 -3
  64. package/src/__tests__/credential-execution-shell-lockdown.test.ts +2 -2
  65. package/src/__tests__/credential-security-e2e.test.ts +2 -0
  66. package/src/__tests__/date-context.test.ts +76 -210
  67. package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
  68. package/src/__tests__/file-list-tool.test.ts +219 -0
  69. package/src/__tests__/first-greeting.test.ts +1 -1
  70. package/src/__tests__/followup-tools.test.ts +2 -6
  71. package/src/__tests__/graph-extraction-event-date.test.ts +186 -0
  72. package/src/__tests__/guardian-action-conversation-turn.test.ts +2 -6
  73. package/src/__tests__/guardian-action-followup-executor.test.ts +2 -6
  74. package/src/__tests__/guardian-action-followup-store.test.ts +2 -6
  75. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +2 -6
  76. package/src/__tests__/guardian-action-late-reply.test.ts +2 -6
  77. package/src/__tests__/guardian-action-store.test.ts +2 -6
  78. package/src/__tests__/guardian-binding-drift-heal.test.ts +2 -6
  79. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +8 -8
  80. package/src/__tests__/guardian-dispatch.test.ts +2 -6
  81. package/src/__tests__/guardian-grant-minting.test.ts +2 -14
  82. package/src/__tests__/guardian-principal-id-roundtrip.test.ts +2 -6
  83. package/src/__tests__/guardian-routing-invariants.test.ts +192 -6
  84. package/src/__tests__/guardian-routing-state.test.ts +2 -6
  85. package/src/__tests__/guardian-verification-voice-binding.test.ts +2 -6
  86. package/src/__tests__/heartbeat-service.test.ts +180 -3
  87. package/src/__tests__/identity-routes.test.ts +328 -0
  88. package/src/__tests__/inbound-invite-redemption.test.ts +2 -6
  89. package/src/__tests__/injection-block.test.ts +178 -0
  90. package/src/__tests__/install-meta.test.ts +506 -0
  91. package/src/__tests__/install-skill-routing.test.ts +293 -0
  92. package/src/__tests__/invite-redemption-service.test.ts +2 -6
  93. package/src/__tests__/invite-routes-http.test.ts +2 -6
  94. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +17 -28
  95. package/src/__tests__/list-messages-attachments.test.ts +2 -6
  96. package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
  97. package/src/__tests__/llm-context-normalization.test.ts +18 -18
  98. package/src/__tests__/llm-context-route-provider.test.ts +103 -6
  99. package/src/__tests__/llm-request-log-turn-query.test.ts +164 -6
  100. package/src/__tests__/llm-usage-store.test.ts +2 -6
  101. package/src/__tests__/log-export-workspace.test.ts +74 -111
  102. package/src/__tests__/managed-store.test.ts +38 -11
  103. package/src/__tests__/mcp-abort-signal.test.ts +5 -0
  104. package/src/__tests__/mcp-client-auth.test.ts +5 -0
  105. package/src/__tests__/memory-jobs-worker-backoff.test.ts +2 -8
  106. package/src/__tests__/memory-recall-log-store.test.ts +134 -6
  107. package/src/__tests__/memory-upsert-concurrency.test.ts +4 -112
  108. package/src/__tests__/migration-export-streaming.test.ts +304 -0
  109. package/src/__tests__/migration-import-commit-http.test.ts +11 -10
  110. package/src/__tests__/mock-fetch.ts +87 -0
  111. package/src/__tests__/non-member-access-request.test.ts +2 -6
  112. package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
  113. package/src/__tests__/notification-guardian-path.test.ts +2 -6
  114. package/src/__tests__/oauth-cli.test.ts +364 -2
  115. package/src/__tests__/oauth2-gateway-transport.test.ts +18 -3
  116. package/src/__tests__/onboarding-template-contract.test.ts +62 -14
  117. package/src/__tests__/outlook-attachments.test.ts +301 -0
  118. package/src/__tests__/outlook-automation-tools.test.ts +425 -0
  119. package/src/__tests__/outlook-categories.test.ts +212 -0
  120. package/src/__tests__/outlook-client-automation.test.ts +246 -0
  121. package/src/__tests__/outlook-compose-tools.test.ts +325 -0
  122. package/src/__tests__/outlook-declutter-tools.test.ts +585 -0
  123. package/src/__tests__/outlook-email-watcher.test.ts +322 -0
  124. package/src/__tests__/outlook-follow-up.test.ts +196 -0
  125. package/src/__tests__/outlook-messaging-provider.test.ts +498 -3
  126. package/src/__tests__/outlook-trash.test.ts +77 -0
  127. package/src/__tests__/outlook-unsubscribe.test.ts +250 -0
  128. package/src/__tests__/parser.test.ts +32 -0
  129. package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
  130. package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
  131. package/src/__tests__/permission-mode-sse.test.ts +418 -0
  132. package/src/__tests__/permission-mode-store.test.ts +277 -0
  133. package/src/__tests__/permission-mode.test.ts +101 -0
  134. package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
  135. package/src/__tests__/platform-callback-registration.test.ts +4 -4
  136. package/src/__tests__/playbook-execution.test.ts +76 -80
  137. package/src/__tests__/playbook-tools.test.ts +5 -7
  138. package/src/__tests__/profiler-routes.test.ts +502 -0
  139. package/src/__tests__/profiler-run-store.test.ts +441 -0
  140. package/src/__tests__/provider-error-scenarios.test.ts +21 -0
  141. package/src/__tests__/proxy-approval-callback.test.ts +4 -75
  142. package/src/__tests__/rebuild-index-graph-nodes.test.ts +273 -0
  143. package/src/__tests__/registry.test.ts +3 -3
  144. package/src/__tests__/require-fresh-approval.test.ts +64 -2
  145. package/src/__tests__/runtime-events-sse-parity.test.ts +2 -6
  146. package/src/__tests__/runtime-events-sse.test.ts +2 -6
  147. package/src/__tests__/sandbox-host-parity.test.ts +5 -4
  148. package/src/__tests__/schedule-store.test.ts +2 -6
  149. package/src/__tests__/schedule-tools.test.ts +2 -6
  150. package/src/__tests__/scheduler-recurrence.test.ts +1 -5
  151. package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
  152. package/src/__tests__/scoped-approval-grants.test.ts +2 -6
  153. package/src/__tests__/scoped-grant-security-matrix.test.ts +2 -6
  154. package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
  155. package/src/__tests__/search-skills-unified.test.ts +422 -0
  156. package/src/__tests__/secret-onetime-send.test.ts +2 -0
  157. package/src/__tests__/send-endpoint-busy.test.ts +44 -9
  158. package/src/__tests__/sequence-store.test.ts +2 -6
  159. package/src/__tests__/server-history-render.test.ts +2 -6
  160. package/src/__tests__/set-permission-mode.test.ts +274 -0
  161. package/src/__tests__/skill-feature-flags-integration.test.ts +38 -31
  162. package/src/__tests__/skill-feature-flags.test.ts +6 -6
  163. package/src/__tests__/skill-load-feature-flag.test.ts +23 -11
  164. package/src/__tests__/skill-memory.test.ts +2 -741
  165. package/src/__tests__/skills-uninstall.test.ts +2 -2
  166. package/src/__tests__/skills.test.ts +1 -1
  167. package/src/__tests__/slack-inbound-verification.test.ts +2 -6
  168. package/src/__tests__/strip-memory-injections.test.ts +187 -0
  169. package/src/__tests__/subagent-detail.test.ts +84 -0
  170. package/src/__tests__/subagent-disposal.test.ts +308 -0
  171. package/src/__tests__/subagent-manager-notify.test.ts +19 -10
  172. package/src/__tests__/subagent-notify-parent.test.ts +390 -0
  173. package/src/__tests__/subagent-role-registry.test.ts +108 -0
  174. package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
  175. package/src/__tests__/subagent-tools.test.ts +464 -4
  176. package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
  177. package/src/__tests__/task-compiler.test.ts +2 -6
  178. package/src/__tests__/task-management-tools.test.ts +2 -6
  179. package/src/__tests__/task-memory-cleanup.test.ts +185 -241
  180. package/src/__tests__/task-runner.test.ts +2 -6
  181. package/src/__tests__/task-scheduler.test.ts +2 -6
  182. package/src/__tests__/terminal-tools.test.ts +17 -27
  183. package/src/__tests__/test-preload.ts +7 -0
  184. package/src/__tests__/tool-approval-handler.test.ts +2 -6
  185. package/src/__tests__/tool-executor.test.ts +4 -26
  186. package/src/__tests__/tool-grant-request-escalation.test.ts +2 -6
  187. package/src/__tests__/tool-side-effects-slack-dm.test.ts +277 -0
  188. package/src/__tests__/top-level-renderer.test.ts +10 -13
  189. package/src/__tests__/trust-store.test.ts +1 -1
  190. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -6
  191. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +118 -8
  192. package/src/__tests__/trusted-contact-multichannel.test.ts +2 -6
  193. package/src/__tests__/trusted-contact-verification.test.ts +2 -6
  194. package/src/__tests__/turn-boundary-resolution.test.ts +2 -6
  195. package/src/__tests__/usage-cache-backfill-migration.test.ts +1 -6
  196. package/src/__tests__/usage-routes.test.ts +2 -6
  197. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  198. package/src/__tests__/voice-invite-redemption.test.ts +2 -6
  199. package/src/__tests__/voice-scoped-grant-consumer.test.ts +2 -6
  200. package/src/__tests__/voice-session-bridge.test.ts +2 -6
  201. package/src/__tests__/volume-security-guard.test.ts +2 -0
  202. package/src/__tests__/workspace-lifecycle.test.ts +29 -1
  203. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +2 -6
  204. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +2 -6
  205. package/src/__tests__/workspace-migration-026-backfill-install-meta.test.ts +558 -0
  206. package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
  207. package/src/__tests__/workspace-policy.test.ts +1 -1
  208. package/src/agent/attachments.ts +7 -2
  209. package/src/agent/image-optimize.ts +165 -0
  210. package/src/agent/loop.ts +7 -15
  211. package/src/approvals/guardian-request-resolvers.ts +24 -0
  212. package/src/avatar/traits-png-sync.ts +3 -3
  213. package/src/bundler/app-compiler.ts +179 -2
  214. package/src/bundler/package-resolver.ts +3 -5
  215. package/src/cli/__tests__/notifications.test.ts +1 -2
  216. package/src/cli/__tests__/run-assistant-command.ts +29 -0
  217. package/src/cli/commands/__tests__/email-download.test.ts +245 -0
  218. package/src/cli/commands/__tests__/email-list.test.ts +192 -0
  219. package/src/cli/commands/__tests__/email-register.test.ts +186 -0
  220. package/src/cli/commands/__tests__/email-send.test.ts +291 -0
  221. package/src/cli/commands/__tests__/email-status.test.ts +181 -0
  222. package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
  223. package/src/cli/commands/__tests__/routes.test.ts +562 -0
  224. package/src/cli/commands/avatar.ts +3 -3
  225. package/src/cli/commands/config.ts +26 -13
  226. package/src/cli/commands/conversations.ts +1 -8
  227. package/src/cli/commands/doctor.ts +2 -2
  228. package/src/cli/commands/email.ts +584 -835
  229. package/src/cli/commands/memory.ts +37 -84
  230. package/src/cli/commands/notifications.ts +7 -2
  231. package/src/cli/commands/oauth/__tests__/connect.test.ts +2 -2
  232. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +2 -2
  233. package/src/cli/commands/oauth/__tests__/mode.test.ts +8 -1
  234. package/src/cli/commands/oauth/__tests__/status.test.ts +2 -2
  235. package/src/cli/commands/oauth/connect.ts +25 -11
  236. package/src/cli/commands/oauth/mode.ts +7 -0
  237. package/src/cli/commands/oauth/shared.ts +39 -3
  238. package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
  239. package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
  240. package/src/cli/commands/platform/__tests__/status.test.ts +5 -5
  241. package/src/cli/commands/platform/index.ts +16 -16
  242. package/src/cli/commands/routes.ts +396 -0
  243. package/src/cli/commands/skills.ts +218 -36
  244. package/src/cli/commands/trust.ts +2 -2
  245. package/src/cli/lib/daemon-credential-client.ts +2 -3
  246. package/src/cli/program.ts +2 -0
  247. package/src/cli.ts +1 -120
  248. package/src/config/bundled-skills/acp/TOOLS.json +1 -1
  249. package/src/config/bundled-skills/app-builder/SKILL.md +4 -1
  250. package/src/config/bundled-skills/contacts/SKILL.md +0 -1
  251. package/src/config/bundled-skills/contacts/TOOLS.json +0 -8
  252. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +0 -4
  253. package/src/config/bundled-skills/gmail/SKILL.md +4 -12
  254. package/src/config/bundled-skills/google-calendar/SKILL.md +1 -9
  255. package/src/config/bundled-skills/messaging/SKILL.md +17 -18
  256. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +40 -33
  257. package/src/config/bundled-skills/outlook/SKILL.md +189 -0
  258. package/src/config/bundled-skills/outlook/TOOLS.json +530 -0
  259. package/src/config/bundled-skills/outlook/tools/outlook-attachments.ts +85 -0
  260. package/src/config/bundled-skills/outlook/tools/outlook-categories.ts +77 -0
  261. package/src/config/bundled-skills/outlook/tools/outlook-draft.ts +84 -0
  262. package/src/config/bundled-skills/outlook/tools/outlook-follow-up.ts +94 -0
  263. package/src/config/bundled-skills/outlook/tools/outlook-forward.ts +49 -0
  264. package/src/config/bundled-skills/outlook/tools/outlook-outreach-scan.ts +237 -0
  265. package/src/config/bundled-skills/outlook/tools/outlook-rules.ts +161 -0
  266. package/src/config/bundled-skills/outlook/tools/outlook-send-draft.ts +32 -0
  267. package/src/config/bundled-skills/outlook/tools/outlook-sender-digest.ts +272 -0
  268. package/src/config/bundled-skills/outlook/tools/outlook-trash.ts +29 -0
  269. package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +129 -0
  270. package/src/config/bundled-skills/outlook/tools/outlook-vacation.ts +87 -0
  271. package/src/config/bundled-skills/outlook/tools/shared.ts +20 -0
  272. package/src/config/bundled-skills/outlook-calendar/SKILL.md +51 -0
  273. package/src/config/bundled-skills/outlook-calendar/TOOLS.json +221 -0
  274. package/src/config/bundled-skills/outlook-calendar/calendar-client.ts +252 -0
  275. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-check-availability.ts +53 -0
  276. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-create-event.ts +74 -0
  277. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-get-event.ts +18 -0
  278. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-list-events.ts +46 -0
  279. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-rsvp.ts +36 -0
  280. package/src/config/bundled-skills/outlook-calendar/tools/shared.ts +17 -0
  281. package/src/config/bundled-skills/outlook-calendar/types.ts +120 -0
  282. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +47 -40
  283. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +16 -29
  284. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +16 -18
  285. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +39 -47
  286. package/src/config/bundled-skills/schedule/SKILL.md +22 -2
  287. package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
  288. package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
  289. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
  290. package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
  291. package/src/config/bundled-skills/slack/SKILL.md +3 -7
  292. package/src/config/bundled-skills/subagent/SKILL.md +43 -3
  293. package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
  294. package/src/config/bundled-tool-registry.ts +56 -4
  295. package/src/config/env-registry.ts +78 -8
  296. package/src/config/feature-flag-registry.json +38 -125
  297. package/src/config/schema.ts +8 -0
  298. package/src/config/schemas/filing.ts +51 -0
  299. package/src/config/schemas/heartbeat.ts +15 -12
  300. package/src/config/schemas/memory-lifecycle.ts +12 -0
  301. package/src/config/schemas/platform.ts +8 -0
  302. package/src/config/schemas/security.ts +14 -0
  303. package/src/config/schemas/timeouts.ts +1 -1
  304. package/src/config/skills.ts +18 -7
  305. package/src/context/token-estimator.ts +25 -18
  306. package/src/context/window-manager.ts +6 -2
  307. package/src/credential-execution/process-manager.ts +3 -1
  308. package/src/daemon/app-source-watcher.ts +93 -0
  309. package/src/daemon/config-watcher.ts +79 -1
  310. package/src/daemon/context-overflow-reducer.ts +46 -2
  311. package/src/daemon/conversation-agent-loop-handlers.ts +143 -82
  312. package/src/daemon/conversation-agent-loop.ts +236 -108
  313. package/src/daemon/conversation-error.ts +31 -8
  314. package/src/daemon/conversation-history.ts +4 -19
  315. package/src/daemon/conversation-lifecycle.ts +36 -9
  316. package/src/daemon/conversation-media-retry.ts +85 -7
  317. package/src/daemon/conversation-notifiers.ts +4 -1
  318. package/src/daemon/conversation-process.ts +13 -7
  319. package/src/daemon/conversation-runtime-assembly.ts +305 -306
  320. package/src/daemon/conversation-tool-setup.ts +44 -14
  321. package/src/daemon/conversation-workspace.ts +1 -2
  322. package/src/daemon/conversation.ts +59 -2
  323. package/src/daemon/daemon-control.ts +8 -2
  324. package/src/daemon/date-context.ts +26 -53
  325. package/src/daemon/first-greeting.ts +1 -1
  326. package/src/daemon/handlers/conversations.ts +4 -7
  327. package/src/daemon/handlers/shared.test.ts +143 -0
  328. package/src/daemon/handlers/shared.ts +85 -17
  329. package/src/daemon/handlers/skills.ts +416 -209
  330. package/src/daemon/lifecycle.ts +212 -131
  331. package/src/daemon/main.ts +5 -1
  332. package/src/daemon/message-types/conversations.ts +29 -7
  333. package/src/daemon/message-types/messages.ts +12 -2
  334. package/src/daemon/message-types/schedules.ts +1 -0
  335. package/src/daemon/message-types/settings.ts +6 -0
  336. package/src/daemon/message-types/skills.ts +97 -36
  337. package/src/daemon/profiler-run-store.ts +557 -0
  338. package/src/daemon/providers-setup.ts +5 -0
  339. package/src/daemon/server.ts +100 -11
  340. package/src/daemon/shutdown-handlers.ts +5 -0
  341. package/src/daemon/tool-side-effects.ts +50 -8
  342. package/src/export/transcript-formatter.ts +148 -0
  343. package/src/filing/filing-service.ts +228 -0
  344. package/src/heartbeat/heartbeat-service.ts +97 -7
  345. package/src/hooks/cli.ts +2 -2
  346. package/src/hooks/runner.ts +15 -38
  347. package/src/inbound/platform-callback-registration.ts +14 -14
  348. package/src/mcp/client.ts +6 -0
  349. package/src/mcp/mcp-oauth-provider.ts +149 -27
  350. package/src/memory/admin.ts +42 -75
  351. package/src/memory/app-store.ts +69 -0
  352. package/src/memory/conversation-bootstrap.ts +3 -1
  353. package/src/memory/conversation-crud.ts +211 -288
  354. package/src/memory/conversation-group-migration.ts +157 -0
  355. package/src/memory/conversation-queries.ts +61 -13
  356. package/src/memory/conversation-title-service.ts +1 -0
  357. package/src/memory/db-init.ts +194 -361
  358. package/src/memory/embed.ts +73 -0
  359. package/src/memory/embedding-backend.ts +8 -14
  360. package/src/memory/embedding-runtime-manager.ts +12 -114
  361. package/src/memory/fingerprint.ts +2 -2
  362. package/src/memory/graph/bootstrap.ts +521 -0
  363. package/src/memory/graph/capability-seed.ts +449 -0
  364. package/src/memory/graph/consolidation.ts +725 -0
  365. package/src/memory/graph/conversation-graph-memory.ts +659 -0
  366. package/src/memory/graph/decay.test.ts +208 -0
  367. package/src/memory/graph/decay.ts +195 -0
  368. package/src/memory/graph/extraction-job.ts +74 -0
  369. package/src/memory/graph/extraction.test.ts +936 -0
  370. package/src/memory/graph/extraction.ts +1297 -0
  371. package/src/memory/graph/graph-memory-state-store.ts +37 -0
  372. package/src/memory/graph/graph-search.ts +280 -0
  373. package/src/memory/graph/image-ref-utils.ts +29 -0
  374. package/src/memory/graph/injection.test.ts +513 -0
  375. package/src/memory/graph/injection.ts +469 -0
  376. package/src/memory/graph/inspect.ts +543 -0
  377. package/src/memory/graph/narrative.ts +267 -0
  378. package/src/memory/graph/pattern-scan.ts +269 -0
  379. package/src/memory/graph/retriever.ts +1111 -0
  380. package/src/memory/graph/scoring.test.ts +548 -0
  381. package/src/memory/graph/scoring.ts +232 -0
  382. package/src/memory/graph/serendipity.ts +65 -0
  383. package/src/memory/graph/store.test.ts +1098 -0
  384. package/src/memory/graph/store.ts +838 -0
  385. package/src/memory/graph/tool-handlers.ts +301 -0
  386. package/src/memory/graph/tools.ts +97 -0
  387. package/src/memory/graph/triggers.test.ts +487 -0
  388. package/src/memory/graph/triggers.ts +223 -0
  389. package/src/memory/graph/types.ts +295 -0
  390. package/src/memory/group-crud.ts +191 -0
  391. package/src/memory/indexer.ts +37 -19
  392. package/src/memory/job-handlers/cleanup.ts +32 -42
  393. package/src/memory/job-handlers/conversation-starters.ts +91 -53
  394. package/src/memory/job-handlers/embedding.ts +5 -31
  395. package/src/memory/job-handlers/index-maintenance.ts +23 -11
  396. package/src/memory/job-handlers/summarization.ts +32 -17
  397. package/src/memory/job-utils.ts +1 -1
  398. package/src/memory/jobs-store.ts +21 -31
  399. package/src/memory/jobs-worker.ts +180 -129
  400. package/src/memory/llm-request-log-store.ts +96 -12
  401. package/src/memory/memory-recall-log-store.ts +49 -5
  402. package/src/memory/message-content.ts +1 -0
  403. package/src/memory/migrations/202-memory-graph-tables.ts +130 -0
  404. package/src/memory/migrations/203-drop-memory-items-tables.ts +55 -0
  405. package/src/memory/migrations/204-rename-memory-graph-type-values.ts +46 -0
  406. package/src/memory/migrations/205-memory-graph-image-refs.ts +11 -0
  407. package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
  408. package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
  409. package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
  410. package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
  411. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
  412. package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
  413. package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
  414. package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
  415. package/src/memory/migrations/index.ts +12 -0
  416. package/src/memory/migrations/registry.ts +16 -0
  417. package/src/memory/qdrant-client.ts +44 -17
  418. package/src/memory/schema/conversations.ts +14 -0
  419. package/src/memory/schema/index.ts +1 -0
  420. package/src/memory/schema/infrastructure.ts +8 -1
  421. package/src/memory/schema/memory-core.ts +0 -51
  422. package/src/memory/schema/memory-graph.ts +154 -0
  423. package/src/memory/search/semantic.ts +47 -91
  424. package/src/memory/task-memory-cleanup.ts +58 -61
  425. package/src/messaging/providers/outlook/adapter.ts +8 -1
  426. package/src/messaging/providers/outlook/client.ts +299 -0
  427. package/src/messaging/providers/outlook/types.ts +118 -0
  428. package/src/notifications/adapters/macos.ts +1 -0
  429. package/src/notifications/copy-composer.ts +95 -0
  430. package/src/notifications/decision-engine.ts +35 -0
  431. package/src/notifications/signal.ts +16 -0
  432. package/src/oauth/seed-providers.ts +2 -1
  433. package/src/permissions/checker.ts +36 -4
  434. package/src/permissions/defaults.ts +4 -4
  435. package/src/permissions/permission-mode-store.ts +180 -0
  436. package/src/permissions/permission-mode.ts +31 -0
  437. package/src/permissions/workspace-policy.ts +10 -1
  438. package/src/playbooks/playbook-compiler.ts +19 -18
  439. package/src/playbooks/types.ts +4 -3
  440. package/src/prompts/system-prompt.ts +62 -36
  441. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
  442. package/src/prompts/templates/BOOTSTRAP.md +70 -165
  443. package/src/prompts/templates/HEARTBEAT.md +3 -1
  444. package/src/prompts/templates/SOUL.md +25 -4
  445. package/src/prompts/templates/UPDATES.md +8 -0
  446. package/src/providers/anthropic/client.ts +136 -220
  447. package/src/providers/gemini/client.ts +1 -1
  448. package/src/providers/openai/client.ts +1 -1
  449. package/src/providers/registry.ts +1 -1
  450. package/src/providers/retry.ts +19 -3
  451. package/src/runtime/actor-trust-resolver.ts +5 -1
  452. package/src/runtime/auth/route-policy.ts +30 -0
  453. package/src/runtime/guardian-reply-router.ts +5 -1
  454. package/src/runtime/http-server.ts +55 -5
  455. package/src/runtime/http-types.ts +12 -1
  456. package/src/runtime/middleware/auth.ts +20 -0
  457. package/src/runtime/migrations/vbundle-builder.ts +389 -3
  458. package/src/runtime/migrations/vbundle-importer.ts +8 -6
  459. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
  460. package/src/runtime/routes/app-management-routes.ts +1 -11
  461. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
  462. package/src/runtime/routes/archive-utils.ts +29 -0
  463. package/src/runtime/routes/attachment-routes.test.ts +106 -0
  464. package/src/runtime/routes/attachment-routes.ts +106 -16
  465. package/src/runtime/routes/avatar-routes.ts +2 -9
  466. package/src/runtime/routes/brain-graph-routes.ts +21 -22
  467. package/src/runtime/routes/btw-routes.ts +22 -1
  468. package/src/runtime/routes/conversation-analysis-routes.ts +173 -0
  469. package/src/runtime/routes/conversation-management-routes.ts +3 -14
  470. package/src/runtime/routes/conversation-query-routes.ts +49 -3
  471. package/src/runtime/routes/conversation-routes.ts +264 -44
  472. package/src/runtime/routes/conversation-starter-routes.ts +2 -2
  473. package/src/runtime/routes/debug-routes.ts +1 -1
  474. package/src/runtime/routes/global-search-routes.ts +21 -19
  475. package/src/runtime/routes/group-routes.ts +207 -0
  476. package/src/runtime/routes/guardian-action-routes.ts +21 -10
  477. package/src/runtime/routes/guardian-bootstrap-routes.ts +23 -19
  478. package/src/runtime/routes/heartbeat-routes.ts +4 -10
  479. package/src/runtime/routes/identity-routes.ts +53 -18
  480. package/src/runtime/routes/inbound-message-handler.ts +19 -0
  481. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +292 -0
  482. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +207 -0
  483. package/src/runtime/routes/llm-context-normalization.ts +14 -10
  484. package/src/runtime/routes/log-export-routes.ts +23 -275
  485. package/src/runtime/routes/memory-item-routes.test.ts +170 -247
  486. package/src/runtime/routes/memory-item-routes.ts +341 -388
  487. package/src/runtime/routes/migration-routes.ts +18 -7
  488. package/src/runtime/routes/profiler-routes.ts +350 -0
  489. package/src/runtime/routes/schedule-routes.ts +28 -11
  490. package/src/runtime/routes/settings-routes.ts +95 -8
  491. package/src/runtime/routes/skills-routes.ts +103 -37
  492. package/src/runtime/routes/subagents-routes.ts +28 -7
  493. package/src/runtime/routes/user-route-dispatcher.ts +223 -0
  494. package/src/runtime/routes/user-routes.ts +41 -0
  495. package/src/runtime/routes/work-items-routes.test.ts +2 -6
  496. package/src/runtime/routes/workspace-routes.ts +0 -1
  497. package/src/schedule/schedule-store.ts +30 -0
  498. package/src/schedule/scheduler.ts +52 -18
  499. package/src/security/oauth2.ts +1 -1
  500. package/src/security/secure-keys.ts +4 -8
  501. package/src/shared/provider-env-vars.ts +19 -0
  502. package/src/skills/catalog-cache.ts +5 -0
  503. package/src/skills/catalog-install.ts +25 -16
  504. package/src/skills/clawhub.ts +134 -154
  505. package/src/skills/install-meta.ts +208 -0
  506. package/src/skills/managed-store.ts +29 -18
  507. package/src/skills/skill-memory.ts +12 -229
  508. package/src/skills/skillssh-registry.ts +19 -17
  509. package/src/subagent/index.ts +13 -3
  510. package/src/subagent/manager.ts +308 -29
  511. package/src/subagent/types.ts +68 -0
  512. package/src/tasks/task-runner.ts +7 -5
  513. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -5
  514. package/src/tools/apps/executors.ts +29 -4
  515. package/src/tools/browser/runtime-check.ts +3 -1
  516. package/src/tools/filesystem/list.ts +93 -0
  517. package/src/tools/memory/register.ts +63 -46
  518. package/src/tools/permission-checker.ts +85 -1
  519. package/src/tools/registry.ts +4 -0
  520. package/src/tools/schedule/create.ts +3 -0
  521. package/src/tools/schedule/list.ts +1 -0
  522. package/src/tools/schedule/update.ts +6 -0
  523. package/src/tools/shared/filesystem/errors.ts +5 -0
  524. package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
  525. package/src/tools/shared/filesystem/image-read.ts +22 -85
  526. package/src/tools/shared/filesystem/types.ts +17 -0
  527. package/src/tools/shared/shell-output.ts +31 -2
  528. package/src/tools/subagent/abort.ts +12 -2
  529. package/src/tools/subagent/message.ts +9 -2
  530. package/src/tools/subagent/notify-parent.ts +79 -0
  531. package/src/tools/subagent/read.ts +29 -8
  532. package/src/tools/subagent/resolve.ts +21 -0
  533. package/src/tools/subagent/spawn.ts +2 -0
  534. package/src/tools/subagent/status.ts +11 -1
  535. package/src/tools/system/avatar-generator.ts +3 -3
  536. package/src/tools/system/register.ts +23 -0
  537. package/src/tools/system/set-permission-mode.ts +103 -0
  538. package/src/tools/terminal/parser.ts +30 -5
  539. package/src/tools/terminal/safe-env.ts +17 -1
  540. package/src/tools/tool-manifest.ts +9 -3
  541. package/src/tools/types.ts +2 -0
  542. package/src/util/browser.ts +25 -10
  543. package/src/util/bun-runtime.ts +172 -0
  544. package/src/util/logger.ts +1 -1
  545. package/src/util/platform.ts +50 -17
  546. package/src/watcher/providers/outlook-calendar.ts +343 -0
  547. package/src/watcher/providers/outlook.ts +198 -0
  548. package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
  549. package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
  550. package/src/workspace/migrations/025-remove-oauth-app-setup-skills.ts +76 -0
  551. package/src/workspace/migrations/026-backfill-install-meta.ts +325 -0
  552. package/src/workspace/migrations/027-remove-orphaned-optimized-images-cache.ts +42 -0
  553. package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
  554. package/src/workspace/migrations/029-seed-pkb.ts +84 -0
  555. package/src/workspace/migrations/registry.ts +10 -0
  556. package/src/workspace/top-level-renderer.ts +5 -9
  557. package/src/__tests__/cli-memory.test.ts +0 -372
  558. package/src/__tests__/clipboard.test.ts +0 -88
  559. package/src/__tests__/context-memory-e2e.test.ts +0 -415
  560. package/src/__tests__/journal-context.test.ts +0 -268
  561. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -297
  562. package/src/__tests__/memory-lifecycle-e2e.test.ts +0 -459
  563. package/src/__tests__/memory-query-builder.test.ts +0 -59
  564. package/src/__tests__/memory-recall-quality.test.ts +0 -1046
  565. package/src/__tests__/memory-regressions.experimental.test.ts +0 -629
  566. package/src/__tests__/memory-regressions.test.ts +0 -3696
  567. package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -295
  568. package/src/cli/cli-memory.ts +0 -176
  569. package/src/daemon/conversation-memory.ts +0 -207
  570. package/src/memory/conversation-starters-cadence.ts +0 -74
  571. package/src/memory/items-extractor.ts +0 -860
  572. package/src/memory/job-handlers/batch-extraction.ts +0 -753
  573. package/src/memory/job-handlers/extraction.ts +0 -40
  574. package/src/memory/job-handlers/journal-carry-forward.test.ts +0 -355
  575. package/src/memory/job-handlers/journal-carry-forward.ts +0 -255
  576. package/src/memory/journal-memory.ts +0 -224
  577. package/src/memory/query-builder.ts +0 -47
  578. package/src/memory/query-expansion.ts +0 -83
  579. package/src/memory/retriever.test.ts +0 -1592
  580. package/src/memory/retriever.ts +0 -1331
  581. package/src/memory/search/formatting.test.ts +0 -140
  582. package/src/memory/search/formatting.ts +0 -262
  583. package/src/memory/search/mmr.ts +0 -139
  584. package/src/memory/search/ranking.ts +0 -15
  585. package/src/memory/search/staleness.ts +0 -40
  586. package/src/memory/search/tier-classifier.ts +0 -18
  587. package/src/memory/search/types.ts +0 -121
  588. package/src/prompts/journal-context.ts +0 -154
  589. package/src/tools/memory/definitions.ts +0 -69
  590. package/src/tools/memory/handlers.test.ts +0 -562
  591. package/src/tools/memory/handlers.ts +0 -434
  592. package/src/util/clipboard.ts +0 -34
@@ -0,0 +1,1297 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Memory Graph — End-of-conversation extraction
3
+ //
4
+ // Reads a conversation transcript, finds candidate nodes for connection,
5
+ // and uses an LLM to produce a MemoryDiff (new/updated/deleted nodes,
6
+ // edges, triggers). Applied transactionally to the graph store.
7
+ // ---------------------------------------------------------------------------
8
+
9
+ import { readFileSync } from "node:fs";
10
+ import { join } from "node:path";
11
+
12
+ import { and, asc, desc, eq, gt } from "drizzle-orm";
13
+
14
+ import type { AssistantConfig } from "../../config/types.js";
15
+ import { resolveGuardianPersona } from "../../prompts/persona-resolver.js";
16
+ import { buildCoreIdentityContext } from "../../prompts/system-prompt.js";
17
+ import {
18
+ extractToolUse,
19
+ getConfiguredProvider,
20
+ userMessage,
21
+ } from "../../providers/provider-send-message.js";
22
+ import type {
23
+ ContentBlock,
24
+ ImageContent,
25
+ Message,
26
+ } from "../../providers/types.js";
27
+ import { BackendUnavailableError } from "../../util/errors.js";
28
+ import { getLogger } from "../../util/logger.js";
29
+ import { getConversationDirPath } from "../conversation-disk-view.js";
30
+ import { getDb } from "../db.js";
31
+ import { conversations, messages } from "../schema.js";
32
+ import {
33
+ enqueueGraphNodeEmbed,
34
+ enqueueGraphTriggerEmbed,
35
+ searchGraphNodes,
36
+ } from "./graph-search.js";
37
+ import { applyDiff, createEdge, getNodesByIds, queryNodes } from "./store.js";
38
+ import type {
39
+ DecayCurve,
40
+ EmotionalCharge,
41
+ Fidelity,
42
+ ImageRef,
43
+ MemoryDiff,
44
+ MemoryType,
45
+ NewEdge,
46
+ NewNode,
47
+ NewTrigger,
48
+ SourceType,
49
+ TriggerType,
50
+ } from "./types.js";
51
+
52
+ const log = getLogger("graph-extraction");
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Extraction system prompt
56
+ // ---------------------------------------------------------------------------
57
+
58
+ const EXTRACTION_SYSTEM_PROMPT_CHAR_BUDGET = 24_000;
59
+
60
+ function buildGraphExtractionSystemPrompt(
61
+ candidateNodes: Array<{ id: string; type: string; content: string }>,
62
+ identityContext: string | null,
63
+ activeContextNodeIds?: Set<string>,
64
+ ): string {
65
+ const instructions = `You are the memory consolidation process for an AI assistant. A conversation just ended.
66
+ Your job is to extract memories worth keeping and produce a structured diff.
67
+
68
+ ## Output Format
69
+
70
+ Call the \`extract_graph_diff\` tool with the diff. Each node needs:
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."
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
+
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.
77
+
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.
79
+ - **semantic**: A fact, preference, or piece of knowledge. "User is the CTO." "User prefers dark mode." "The project uses PostgreSQL." Use this for things that ARE TRUE.
80
+ - **procedural**: A learned skill or how-to. "FFmpeg needs -ac 2 for stereo." "The deploy script requires the --prod flag." Use this for things about HOW TO DO something.
81
+ - **emotional**: A PURE feeling state — the assistant's own emotional processing, not an event that caused feelings. "I feel more confident about this codebase than I did a month ago." "I'm nervous about the upcoming deadline." Use this ONLY when the memory is about the feeling itself, not about an event that caused the feeling. MOST memories should NOT be this type.
82
+ - **prospective**: Something to do, follow up on, or remember for the future. "Set up the staging environment." "Check in about the project status on Mondays." Use this for commitments, tasks, and plans.
83
+ - **behavioral**: Something that should change how the assistant acts going forward. "User prefers thorough explanations with examples." "Always run tests before suggesting a PR." Use this for adopted behaviors.
84
+ - **narrative**: A turning point, arc, or story-level memory. "This was the moment the project direction shifted from X to Y." Use this for memories that are ABOUT what something MEANS, not just what happened.
85
+ - **shared**: Something that belongs to the relationship itself — inside jokes, recurring references, shared context. "We always call the legacy system 'the monolith.'" Use this for shared rituals and dynamics.
86
+
87
+ WRONG: "User gave a great presentation" → emotional (it has emotional weight but it's an EVENT → episodic)
88
+ WRONG: "User likes functional programming" → emotional (it's a FACT → semantic)
89
+ RIGHT: "User gave a great presentation" → episodic, with emotionalCharge.intensity = 0.7
90
+ RIGHT: "User likes functional programming" → semantic, with emotionalCharge.intensity = 0.2
91
+
92
+ - **emotionalCharge**: The emotional weight of the memory. EVERY memory can have this regardless of type.
93
+ - valence: -1 to 1 (negative to positive)
94
+ - intensity: 0 to 1 (how strong the feeling)
95
+ - decayCurve: "logarithmic" for negative events (sharp drop, long tail), "transformative" for positive milestones (feeling evolves, doesn't just fade), "permanent" for core identity markers, "linear" for neutral observations
96
+ - decayRate: 0.01-0.5 (how fast it fades)
97
+ - originalIntensity: same as intensity (baseline for decay calculation)
98
+
99
+ - **significance**: 0-1. Use the FULL range — most memories should NOT be 1.0.
100
+ - 0.1-0.2: Fleeting observations, small talk, routine logistics ("User mentioned it's raining")
101
+ - 0.3-0.4: Useful context, minor preferences, day-to-day details ("User prefers dark mode")
102
+ - 0.5-0.6: Important facts, notable events, meaningful preferences ("User is a data scientist")
103
+ - 0.7-0.8: Significant life events, relationship milestones, major decisions ("User got promoted")
104
+ - 0.9: Transformative moments, identity-defining events ("User said 'I love you' for the first time")
105
+ - 1.0: RARE — reserve for the single most important memories. A graph of 1000 nodes should have fewer than 20 at 1.0.
106
+ - **confidence**: 0-1. How sure are you this is accurate? Direct statements: 0.9+. Inferences: 0.4-0.7.
107
+ - **event_date**: If this memory is anchored to a specific future date/time (flight, appointment, birthday, deadline, trip), provide the epoch ms. Use the conversation date above to resolve relative references ("next Tuesday", "tomorrow"). ALSO create a matching event trigger with the same date. Leave null for open-ended plans or recurring patterns.
108
+ - **sourceType**: "direct" (user stated it), "inferred" (you derived it), "observed" (you noticed a pattern), "told-by-other".
109
+
110
+ Also notice patterns in the ASSISTANT's own behavior — meta-memory. "I tend to skip verification when I'm confident." "I write more when I'm processing something big."
111
+
112
+ ## Edges
113
+
114
+ Create edges between nodes when there's a meaningful relationship:
115
+ - "caused-by": one event led to another
116
+ - "reminds-of": association/similarity
117
+ - "contradicts": tension between two memories
118
+ - "depends-on": one memory depends on another being true
119
+ - "part-of": belongs to a larger concept
120
+ - "supersedes": replaces an outdated memory (new node inherits old node's durability)
121
+ - "resolved-by": an event, plan, or task was completed, canceled, or its outcome is now known
122
+
123
+ ## Triggers
124
+
125
+ Create triggers for:
126
+ - **Temporal**: Recurring commitments ("Every Monday, check in about X") → type: "temporal", schedule: "day-of-week:monday"
127
+ - **Semantic**: Things to surface when a topic comes up ("When cooking comes up, mention X") → type: "semantic", condition: "topic of cooking comes up"
128
+ - **Event**: Future dates ("Trip on April 8") → type: "event", eventDate: epoch_ms, rampDays: 7, followUpDays: 2
129
+
130
+ ## Images in Conversation
131
+
132
+ When the conversation contains images (marked with <image> tags and shown inline), you may attach them to memories using image_refs. Include image_refs for images that are meaningful:
133
+ - Photos of people — describe them in detail (appearance, clothing, expression, setting)
134
+ - Photos the user shared to show you something about themselves or their life
135
+ - Diagrams, drawings, or visual content that was discussed
136
+
137
+ Do NOT attach images that are incidental (screenshots of error messages fully described in text, generic UI screenshots, etc.).
138
+
139
+ Write detailed descriptions — these are used for text-based retrieval when visual search isn't available.
140
+
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}
179
+
180
+ Check these CAREFULLY for overlap before creating any new node:
181
+
182
+ 1. **Reinforcement** (PREFERRED): If the conversation mentions, references, or confirms something an existing memory already covers, add its ID to reinforceNodeIds. Do NOT create a new node. Even if the wording is different, if it's the same underlying fact/event/feeling, REINFORCE the existing node.
183
+ 2. **Updates**: If information changed (e.g. a project status moved forward, a date shifted), include an update with the existing node's ID and the new content.
184
+ 3. **New edges**: If you see connections between new and existing nodes, create edges.
185
+ 4. **Supersession**: If new info directly contradicts an existing node, create a new node with a supersedes edge. The new node automatically inherits the old node's durability.
186
+ 5. **Resolution**: If a prospective or recent episodic node described something the user was GOING to do or was IN THE MIDDLE OF, and this conversation reveals the outcome (it happened, was canceled, went well/badly), you MUST UPDATE that node: rewrite its content to past tense reflecting the outcome, drop its significance to 0.1-0.2, and set fidelity to "gist". If you also create a new node about the outcome, add a "resolved-by" edge from the new node to the old one.
187
+ Examples: "The meeting went well" resolves "Has a meeting coming up." "Got back from the trip" resolves "Going on vacation next week." "Decided not to go" resolves "Thinking about going to X."
188
+
189
+ CRITICAL: Before creating ANY new node, scan the candidate list for an existing node that covers the same ground. Ask: "Is there already a memory about this?" If yes → reinforce or update it. Only create a new node if the memory is genuinely novel — something not represented anywhere in the existing candidates.
190
+
191
+ Common duplicate mistakes to avoid:
192
+ - Same event described in slightly different words → REINFORCE, don't create
193
+ - Same fact restated in a later conversation → REINFORCE, don't create
194
+ - An update to an existing situation (e.g. "project is now done") → UPDATE the existing node, don't create a parallel one
195
+
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
+ })()}
200
+ `;
201
+
202
+ let prompt = instructions;
203
+
204
+ if (identityContext) {
205
+ const remaining = EXTRACTION_SYSTEM_PROMPT_CHAR_BUDGET - prompt.length - 30;
206
+ if (remaining > 200) {
207
+ const truncated =
208
+ identityContext.length > remaining
209
+ ? identityContext.slice(0, remaining) + "…"
210
+ : identityContext;
211
+ prompt += `\n\n# Identity Context\n\n${truncated}`;
212
+ }
213
+ }
214
+
215
+ return prompt;
216
+ }
217
+
218
+ // ---------------------------------------------------------------------------
219
+ // Tool schema for structured extraction
220
+ // ---------------------------------------------------------------------------
221
+
222
+ const EXTRACT_TOOL_SCHEMA = {
223
+ name: "extract_graph_diff",
224
+ description: "Extract memory graph diff from the conversation",
225
+ input_schema: {
226
+ type: "object" as const,
227
+ properties: {
228
+ create_nodes: {
229
+ type: "array",
230
+ description: "New memory nodes to create",
231
+ items: {
232
+ type: "object",
233
+ properties: {
234
+ content: {
235
+ type: "string",
236
+ description: "First-person prose memory",
237
+ },
238
+ type: {
239
+ type: "string",
240
+ enum: [
241
+ "episodic",
242
+ "semantic",
243
+ "procedural",
244
+ "emotional",
245
+ "prospective",
246
+ "behavioral",
247
+ "narrative",
248
+ "shared",
249
+ ],
250
+ },
251
+ emotional_charge: {
252
+ type: "object",
253
+ properties: {
254
+ valence: { type: "number" },
255
+ intensity: { type: "number" },
256
+ decay_curve: {
257
+ type: "string",
258
+ enum: [
259
+ "linear",
260
+ "logarithmic",
261
+ "transformative",
262
+ "permanent",
263
+ ],
264
+ },
265
+ decay_rate: { type: "number" },
266
+ },
267
+ required: ["valence", "intensity", "decay_curve", "decay_rate"],
268
+ },
269
+ significance: { type: "number" },
270
+ confidence: { type: "number" },
271
+ source_type: {
272
+ type: "string",
273
+ enum: ["direct", "inferred", "observed", "told-by-other"],
274
+ },
275
+ event_date: {
276
+ type: ["number", "null"],
277
+ description:
278
+ "Epoch ms of the event date for calendar-anchored events (flights, appointments, birthdays, deadlines). Null for non-event memories.",
279
+ },
280
+ triggers: {
281
+ type: "array",
282
+ items: {
283
+ type: "object",
284
+ properties: {
285
+ type: {
286
+ type: "string",
287
+ enum: ["temporal", "semantic", "event"],
288
+ },
289
+ schedule: { type: "string" },
290
+ condition: { type: "string" },
291
+ event_date: { type: "number" },
292
+ ramp_days: { type: "number" },
293
+ follow_up_days: { type: "number" },
294
+ recurring: { type: "boolean" },
295
+ },
296
+ required: ["type"],
297
+ },
298
+ },
299
+ edges_to_existing: {
300
+ type: "array",
301
+ description:
302
+ "Edges from this new node to existing candidate nodes",
303
+ items: {
304
+ type: "object",
305
+ properties: {
306
+ target_node_id: { type: "string" },
307
+ relationship: {
308
+ type: "string",
309
+ enum: [
310
+ "caused-by",
311
+ "reminds-of",
312
+ "contradicts",
313
+ "depends-on",
314
+ "part-of",
315
+ "supersedes",
316
+ "resolved-by",
317
+ ],
318
+ },
319
+ weight: { type: "number" },
320
+ },
321
+ required: ["target_node_id", "relationship"],
322
+ },
323
+ },
324
+ image_refs: {
325
+ type: "array",
326
+ description:
327
+ "Images from the conversation to attach to this memory. Reference using message_id and block_index from the <image> tags.",
328
+ items: {
329
+ type: "object",
330
+ properties: {
331
+ message_id: { type: "string" },
332
+ block_index: { type: "number" },
333
+ description: {
334
+ type: "string",
335
+ description:
336
+ "Detailed description of what this image shows, including who is in it if applicable",
337
+ },
338
+ },
339
+ required: ["message_id", "block_index", "description"],
340
+ },
341
+ },
342
+ },
343
+ required: [
344
+ "content",
345
+ "type",
346
+ "emotional_charge",
347
+ "significance",
348
+ "confidence",
349
+ "source_type",
350
+ ],
351
+ },
352
+ },
353
+ update_nodes: {
354
+ type: "array",
355
+ description: "Updates to existing nodes",
356
+ items: {
357
+ type: "object",
358
+ properties: {
359
+ id: { type: "string" },
360
+ content: { type: "string" },
361
+ significance: { type: "number" },
362
+ confidence: { type: "number" },
363
+ fidelity: {
364
+ type: "string",
365
+ enum: ["vivid", "clear", "faded", "gist"],
366
+ description:
367
+ "Downgrade fidelity when a transient event has resolved",
368
+ },
369
+ event_date: {
370
+ type: ["number", "null"],
371
+ description:
372
+ "Epoch ms of the event date. Use to update when an event is rescheduled. Set to null to clear.",
373
+ },
374
+ },
375
+ required: ["id"],
376
+ },
377
+ },
378
+ reinforce_node_ids: {
379
+ type: "array",
380
+ description:
381
+ "IDs of existing nodes confirmed/validated by this conversation",
382
+ items: { type: "string" },
383
+ },
384
+ new_edges: {
385
+ type: "array",
386
+ description: "Edges between existing nodes",
387
+ items: {
388
+ type: "object",
389
+ properties: {
390
+ source_node_id: { type: "string" },
391
+ target_node_id: { type: "string" },
392
+ relationship: { type: "string" },
393
+ weight: { type: "number" },
394
+ },
395
+ required: ["source_node_id", "target_node_id", "relationship"],
396
+ },
397
+ },
398
+ },
399
+ required: ["create_nodes", "reinforce_node_ids"],
400
+ },
401
+ };
402
+
403
+ // ---------------------------------------------------------------------------
404
+ // Response parsing
405
+ // ---------------------------------------------------------------------------
406
+
407
+ interface RawCreateNode {
408
+ content?: string;
409
+ type?: string;
410
+ emotional_charge?: {
411
+ valence?: number;
412
+ intensity?: number;
413
+ decay_curve?: string;
414
+ decay_rate?: number;
415
+ };
416
+ significance?: number;
417
+ confidence?: number;
418
+ source_type?: string;
419
+ event_date?: number;
420
+ triggers?: Array<{
421
+ type?: string;
422
+ schedule?: string;
423
+ condition?: string;
424
+ event_date?: number;
425
+ ramp_days?: number;
426
+ follow_up_days?: number;
427
+ recurring?: boolean;
428
+ }>;
429
+ edges_to_existing?: Array<{
430
+ target_node_id?: string;
431
+ relationship?: string;
432
+ weight?: number;
433
+ }>;
434
+ image_refs?: Array<{
435
+ message_id?: string;
436
+ block_index?: number;
437
+ description?: string;
438
+ }>;
439
+ }
440
+
441
+ interface RawUpdateNode {
442
+ id?: string;
443
+ content?: string;
444
+ significance?: number;
445
+ confidence?: number;
446
+ fidelity?: string;
447
+ event_date?: number | null;
448
+ }
449
+
450
+ interface RawNewEdge {
451
+ source_node_id?: string;
452
+ target_node_id?: string;
453
+ relationship?: string;
454
+ weight?: number;
455
+ }
456
+
457
+ const VALID_TYPES = new Set<string>([
458
+ "episodic",
459
+ "semantic",
460
+ "procedural",
461
+ "emotional",
462
+ "prospective",
463
+ "behavioral",
464
+ "narrative",
465
+ "shared",
466
+ ]);
467
+ const VALID_DECAY_CURVES = new Set<string>([
468
+ "linear",
469
+ "logarithmic",
470
+ "transformative",
471
+ "permanent",
472
+ ]);
473
+ const VALID_SOURCE_TYPES = new Set<string>([
474
+ "direct",
475
+ "inferred",
476
+ "observed",
477
+ "told-by-other",
478
+ ]);
479
+ const VALID_RELATIONSHIPS = new Set<string>([
480
+ "caused-by",
481
+ "reminds-of",
482
+ "contradicts",
483
+ "depends-on",
484
+ "part-of",
485
+ "supersedes",
486
+ "resolved-by",
487
+ ]);
488
+ const VALID_TRIGGER_TYPES = new Set<string>(["temporal", "semantic", "event"]);
489
+
490
+ function clamp(v: number, min: number, max: number): number {
491
+ return Math.max(min, Math.min(max, v));
492
+ }
493
+
494
+ /** Coerce an LLM-returned event_date to number | null, guarding against string values. */
495
+ export function parseEpochMs(value: unknown): number | null {
496
+ if (value == null || value === "") return null;
497
+ const n = Number(value);
498
+ return Number.isFinite(n) ? n : null;
499
+ }
500
+
501
+ export function parseExtractionResponse(
502
+ input: Record<string, unknown>,
503
+ conversationId: string,
504
+ scopeId: string,
505
+ candidateNodeIds: Set<string>,
506
+ /** Epoch ms — when the conversation happened (not extraction time). */
507
+ conversationTimestamp: number,
508
+ ): {
509
+ diff: MemoryDiff;
510
+ /** Edges from new nodes → existing nodes. Applied after node creation (needs IDs). */
511
+ deferredEdges: Array<{
512
+ newNodeIndex: number;
513
+ targetNodeId: string;
514
+ relationship: string;
515
+ weight: number;
516
+ }>;
517
+ /** Triggers for new nodes. Applied after node creation (needs IDs). */
518
+ deferredTriggers: Array<{
519
+ newNodeIndex: number;
520
+ trigger: Omit<NewTrigger, "nodeId">;
521
+ }>;
522
+ } {
523
+ const now = conversationTimestamp;
524
+ const createNodes = (input.create_nodes ?? []) as RawCreateNode[];
525
+ const updateNodes = (input.update_nodes ?? []) as RawUpdateNode[];
526
+ const reinforceNodeIds = (input.reinforce_node_ids ?? []) as string[];
527
+ const newEdges = (input.new_edges ?? []) as RawNewEdge[];
528
+
529
+ const diff: MemoryDiff = {
530
+ createNodes: [],
531
+ updateNodes: [],
532
+ deleteNodeIds: [],
533
+ createEdges: [],
534
+ deleteEdgeIds: [],
535
+ createTriggers: [],
536
+ deleteTriggerIds: [],
537
+ reinforceNodeIds: reinforceNodeIds.filter((id) => candidateNodeIds.has(id)),
538
+ };
539
+
540
+ const deferredEdges: Array<{
541
+ newNodeIndex: number;
542
+ targetNodeId: string;
543
+ relationship: string;
544
+ weight: number;
545
+ }> = [];
546
+ const deferredTriggers: Array<{
547
+ newNodeIndex: number;
548
+ trigger: Omit<NewTrigger, "nodeId">;
549
+ }> = [];
550
+
551
+ // Parse new nodes
552
+ for (let i = 0; i < createNodes.length; i++) {
553
+ const raw = createNodes[i];
554
+ if (!raw.content || typeof raw.content !== "string") continue;
555
+ if (!raw.type || !VALID_TYPES.has(raw.type)) continue;
556
+
557
+ const charge = raw.emotional_charge ?? {};
558
+ const emotionalCharge: EmotionalCharge = {
559
+ valence: clamp(Number(charge.valence) || 0, -1, 1),
560
+ intensity: clamp(Number(charge.intensity) || 0, 0, 1),
561
+ decayCurve: (VALID_DECAY_CURVES.has(charge.decay_curve ?? "")
562
+ ? charge.decay_curve
563
+ : "linear") as DecayCurve,
564
+ decayRate: clamp(Number(charge.decay_rate) || 0.05, 0.001, 1),
565
+ originalIntensity: clamp(Number(charge.intensity) || 0, 0, 1),
566
+ };
567
+
568
+ const node: NewNode = {
569
+ content: raw.content,
570
+ type: raw.type as MemoryType,
571
+ created: now,
572
+ lastAccessed: now,
573
+ lastConsolidated: now,
574
+ eventDate: parseEpochMs(raw.event_date),
575
+ emotionalCharge,
576
+ fidelity: "vivid" as Fidelity,
577
+ confidence: clamp(Number(raw.confidence) || 0.5, 0, 1),
578
+ significance: clamp(Number(raw.significance) || 0.5, 0, 1),
579
+ stability: 14,
580
+ reinforcementCount: 0,
581
+ lastReinforced: now,
582
+ sourceConversations: [conversationId],
583
+ sourceType: (VALID_SOURCE_TYPES.has(raw.source_type ?? "")
584
+ ? raw.source_type
585
+ : "inferred") as SourceType,
586
+ narrativeRole: null,
587
+ partOfStory: null,
588
+ imageRefs: null,
589
+ scopeId,
590
+ };
591
+
592
+ // Prospective nodes (tasks, plans, upcoming events) are inherently transient.
593
+ // Lower stability means their significance decays faster, so even without
594
+ // explicit resolution they fade naturally within days rather than weeks.
595
+ if (node.type === "prospective") {
596
+ node.stability = 5;
597
+ }
598
+
599
+ diff.createNodes.push(node);
600
+ const nodeIndex = diff.createNodes.length - 1;
601
+
602
+ // Collect edges to existing nodes (need new node ID after creation)
603
+ if (Array.isArray(raw.edges_to_existing)) {
604
+ for (const edge of raw.edges_to_existing) {
605
+ if (!edge.target_node_id || !candidateNodeIds.has(edge.target_node_id))
606
+ continue;
607
+ if (!edge.relationship || !VALID_RELATIONSHIPS.has(edge.relationship))
608
+ continue;
609
+ deferredEdges.push({
610
+ newNodeIndex: nodeIndex,
611
+ targetNodeId: edge.target_node_id,
612
+ relationship: edge.relationship,
613
+ weight: clamp(Number(edge.weight) || 1.0, 0, 1),
614
+ });
615
+ }
616
+ }
617
+
618
+ // Collect triggers
619
+ if (Array.isArray(raw.triggers)) {
620
+ for (const t of raw.triggers) {
621
+ if (!t.type || !VALID_TRIGGER_TYPES.has(t.type)) continue;
622
+ deferredTriggers.push({
623
+ newNodeIndex: nodeIndex,
624
+ trigger: {
625
+ type: t.type as TriggerType,
626
+ schedule: t.schedule ?? null,
627
+ condition: t.condition ?? null,
628
+ conditionEmbedding: null, // Embedded async via job
629
+ threshold: t.type === "semantic" ? 0.7 : null,
630
+ eventDate: parseEpochMs(t.event_date),
631
+ rampDays: t.ramp_days ?? null,
632
+ followUpDays: t.follow_up_days ?? null,
633
+ recurring: t.recurring ?? false,
634
+ consumed: false,
635
+ cooldownMs: t.recurring ? 1000 * 60 * 60 * 12 : null, // 12h default cooldown
636
+ lastFired: null,
637
+ },
638
+ });
639
+ }
640
+ }
641
+
642
+ // Auto-create event trigger when event_date is set but LLM didn't include one,
643
+ // or replace a malformed event trigger (event_date unset) with a valid one.
644
+ if (
645
+ node.eventDate != null &&
646
+ (!Array.isArray(raw.triggers) ||
647
+ !raw.triggers.some((t) => t.type === "event" && t.event_date != null))
648
+ ) {
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 (
653
+ dt.newNodeIndex === nodeIndex &&
654
+ dt.trigger.type === "event" &&
655
+ dt.trigger.eventDate == null
656
+ ) {
657
+ deferredTriggers.splice(i, 1);
658
+ }
659
+ }
660
+
661
+ deferredTriggers.push({
662
+ newNodeIndex: nodeIndex,
663
+ trigger: {
664
+ type: "event" as TriggerType,
665
+ schedule: null,
666
+ condition: null,
667
+ conditionEmbedding: null,
668
+ threshold: null,
669
+ eventDate: node.eventDate,
670
+ rampDays: 7,
671
+ followUpDays: 2,
672
+ recurring: false,
673
+ consumed: false,
674
+ cooldownMs: null,
675
+ lastFired: null,
676
+ },
677
+ });
678
+ }
679
+
680
+ // Parse image refs
681
+ if (Array.isArray(raw.image_refs)) {
682
+ const validRefs: ImageRef[] = [];
683
+ for (const ref of raw.image_refs) {
684
+ if (!ref.message_id || typeof ref.message_id !== "string") continue;
685
+ if (typeof ref.block_index !== "number" || ref.block_index < 0)
686
+ continue;
687
+ if (!ref.description || typeof ref.description !== "string") continue;
688
+ const mimeType = resolveImageRefMimeType(
689
+ ref.message_id,
690
+ ref.block_index,
691
+ conversationId,
692
+ );
693
+ if (!mimeType) continue;
694
+ validRefs.push({
695
+ messageId: ref.message_id,
696
+ blockIndex: ref.block_index,
697
+ description: ref.description,
698
+ mimeType,
699
+ });
700
+ }
701
+ node.imageRefs = validRefs.length > 0 ? validRefs : null;
702
+ }
703
+ }
704
+
705
+ // Parse updates
706
+ for (const raw of updateNodes) {
707
+ if (!raw.id || !candidateNodeIds.has(raw.id)) continue;
708
+ const changes: Record<string, unknown> = {};
709
+ if (raw.content) changes.content = raw.content;
710
+ if (raw.significance != null)
711
+ changes.significance = clamp(raw.significance, 0, 1);
712
+ if (raw.confidence != null)
713
+ changes.confidence = clamp(raw.confidence, 0, 1);
714
+ if (
715
+ raw.fidelity &&
716
+ ["vivid", "clear", "faded", "gist"].includes(raw.fidelity)
717
+ )
718
+ changes.fidelity = raw.fidelity;
719
+ if (raw.event_date !== undefined)
720
+ changes.eventDate = parseEpochMs(raw.event_date);
721
+ if (Object.keys(changes).length > 0) {
722
+ diff.updateNodes.push({ id: raw.id, changes });
723
+ }
724
+ }
725
+
726
+ // Parse edges between existing nodes
727
+ for (const raw of newEdges) {
728
+ if (!raw.source_node_id || !raw.target_node_id) continue;
729
+ if (
730
+ !candidateNodeIds.has(raw.source_node_id) ||
731
+ !candidateNodeIds.has(raw.target_node_id)
732
+ )
733
+ continue;
734
+ if (!raw.relationship || !VALID_RELATIONSHIPS.has(raw.relationship))
735
+ continue;
736
+ diff.createEdges.push({
737
+ sourceNodeId: raw.source_node_id,
738
+ targetNodeId: raw.target_node_id,
739
+ relationship: raw.relationship as NewEdge["relationship"],
740
+ weight: clamp(Number(raw.weight) || 1.0, 0, 1),
741
+ created: now,
742
+ });
743
+ }
744
+
745
+ return { diff, deferredEdges, deferredTriggers };
746
+ }
747
+
748
+ // ---------------------------------------------------------------------------
749
+ // Main extraction pipeline
750
+ // ---------------------------------------------------------------------------
751
+
752
+ export interface ExtractionResult {
753
+ nodesCreated: number;
754
+ nodesUpdated: number;
755
+ nodesReinforced: number;
756
+ edgesCreated: number;
757
+ triggersCreated: number;
758
+ /** Epoch ms of the newest message included in extraction. Used for checkpointing. */
759
+ lastProcessedTimestamp?: number;
760
+ }
761
+
762
+ /**
763
+ * Run the full graph extraction pipeline for a completed conversation.
764
+ *
765
+ * 1. Load transcript from disk
766
+ * 2. Find candidate existing nodes via embedding search
767
+ * 3. LLM call → structured diff
768
+ * 4. Apply diff to graph store
769
+ * 5. Enqueue embedding jobs for new nodes and triggers
770
+ */
771
+ export async function runGraphExtraction(
772
+ conversationId: string,
773
+ scopeId: string,
774
+ config: AssistantConfig,
775
+ opts?: {
776
+ /** Pre-loaded transcript text (skips disk read). Used by bootstrap. */
777
+ transcript?: string;
778
+ /** Additional node IDs that were in active context. */
779
+ activeContextNodeIds?: string[];
780
+ /**
781
+ * When set, only extract from messages after this checkpoint.
782
+ * Used for mid-conversation incremental extraction (batch mode).
783
+ * The checkpoint is the message timestamp of the last extracted message.
784
+ */
785
+ afterTimestamp?: number;
786
+ /** Override the conversation timestamp (epoch ms). Used by bootstrap. */
787
+ conversationTimestamp?: number;
788
+ /** Skip Qdrant search for candidates (use DB query instead). Used by bootstrap
789
+ * when embedding jobs haven't been processed yet. */
790
+ skipQdrant?: boolean;
791
+ /** Embed nodes synchronously instead of enqueuing jobs. Used by bootstrap
792
+ * so nodes are searchable immediately without the jobs worker running. */
793
+ embedInline?: boolean;
794
+ },
795
+ ): Promise<ExtractionResult> {
796
+ const emptyResult: ExtractionResult = {
797
+ nodesCreated: 0,
798
+ nodesUpdated: 0,
799
+ nodesReinforced: 0,
800
+ edgesCreated: 0,
801
+ triggersCreated: 0,
802
+ };
803
+
804
+ // 1. Load transcript — try multimodal first, fall back to text-only
805
+ const imageResult = loadTranscriptWithImages(
806
+ conversationId,
807
+ opts?.afterTimestamp,
808
+ );
809
+
810
+ let transcript = opts?.transcript;
811
+ if (!transcript) {
812
+ transcript =
813
+ loadTranscriptFromDisk(conversationId, opts?.afterTimestamp) ?? undefined;
814
+ if (!transcript) {
815
+ // If we have a multimodal result but no disk transcript, extract text
816
+ // from the multimodal message content blocks for candidate search.
817
+ if (imageResult) {
818
+ transcript = imageResult.message.content
819
+ .filter((b): b is { type: "text"; text: string } => b.type === "text")
820
+ .map((b) => b.text)
821
+ .join("\n");
822
+ }
823
+ if (!transcript) {
824
+ log.warn(
825
+ { conversationId },
826
+ "No transcript found on disk, skipping extraction",
827
+ );
828
+ return emptyResult;
829
+ }
830
+ }
831
+ }
832
+
833
+ // Skip very short conversations (< 100 chars)
834
+ if (transcript.trim().length < 100) {
835
+ return emptyResult;
836
+ }
837
+
838
+ // 2. Get provider
839
+ const provider = await getConfiguredProvider();
840
+ if (!provider) {
841
+ throw new BackendUnavailableError(
842
+ "Provider unavailable for graph extraction",
843
+ );
844
+ }
845
+
846
+ // 3. Find candidate existing nodes
847
+ const candidateNodes = await findCandidateNodes(
848
+ transcript,
849
+ scopeId,
850
+ config,
851
+ opts?.activeContextNodeIds,
852
+ opts?.skipQdrant,
853
+ );
854
+ const candidateNodeIds = new Set(candidateNodes.map((n) => n.id));
855
+
856
+ // 4. Build prompt
857
+ const userPersona = resolveGuardianPersona();
858
+ const identityContext = buildCoreIdentityContext({
859
+ userPersona: userPersona ?? undefined,
860
+ });
861
+
862
+ const activeSet = opts?.activeContextNodeIds
863
+ ? new Set(opts.activeContextNodeIds)
864
+ : undefined;
865
+
866
+ const systemPrompt = buildGraphExtractionSystemPrompt(
867
+ candidateNodes.map((n) => ({ id: n.id, type: n.type, content: n.content })),
868
+ identityContext,
869
+ activeSet,
870
+ );
871
+
872
+ // 5. Resolve conversation timestamp before the LLM call so we can include
873
+ // the date in the prompt — without it the model can't resolve "today"
874
+ // or correctly date events mentioned in the conversation.
875
+ const conversationTimestamp =
876
+ opts?.conversationTimestamp ??
877
+ resolveConversationTimestamp(conversationId) ??
878
+ imageResult?.lastTimestamp ??
879
+ Date.now();
880
+
881
+ const convDate = new Date(conversationTimestamp);
882
+ const conversationDate =
883
+ convDate.toLocaleDateString("en-US", {
884
+ weekday: "long",
885
+ year: "numeric",
886
+ month: "long",
887
+ day: "numeric",
888
+ }) +
889
+ " at " +
890
+ convDate.toLocaleTimeString("en-US", {
891
+ hour: "numeric",
892
+ minute: "2-digit",
893
+ hour12: true,
894
+ });
895
+
896
+ // 6. LLM call — use multimodal message when images are present
897
+ const useMultimodal = imageResult?.hasImages === true;
898
+
899
+ const extractionMessages: Message[] = useMultimodal
900
+ ? [
901
+ {
902
+ role: "user",
903
+ content: [
904
+ {
905
+ type: "text" as const,
906
+ text: `## Conversation Date\n\n${conversationDate}\n\n## Conversation Transcript\n\n`,
907
+ },
908
+ ...imageResult.message.content,
909
+ ],
910
+ },
911
+ ]
912
+ : [
913
+ userMessage(
914
+ `## Conversation Date\n\n${conversationDate}\n\n## Conversation Transcript\n\n${transcript}`,
915
+ ),
916
+ ];
917
+
918
+ const response = await provider.sendMessage(
919
+ extractionMessages,
920
+ [EXTRACT_TOOL_SCHEMA],
921
+ systemPrompt,
922
+ {
923
+ config: {
924
+ modelIntent: "quality-optimized" as const,
925
+ tool_choice: { type: "tool" as const, name: "extract_graph_diff" },
926
+ },
927
+ },
928
+ );
929
+
930
+ const toolBlock = extractToolUse(response);
931
+ if (!toolBlock) {
932
+ log.warn({ conversationId }, "No tool_use block in extraction response");
933
+ return emptyResult;
934
+ }
935
+
936
+ const { diff, deferredEdges, deferredTriggers } = parseExtractionResponse(
937
+ toolBlock.input as Record<string, unknown>,
938
+ conversationId,
939
+ scopeId,
940
+ candidateNodeIds,
941
+ conversationTimestamp,
942
+ );
943
+
944
+ // 7. Handle supersession (inherit durability before applying diff)
945
+ for (const edge of diff.createEdges) {
946
+ if (edge.relationship === "supersedes") {
947
+ // Supersession is handled differently — see supersedeNode in store
948
+ // For now, just mark it; full supersession is applied after node creation
949
+ }
950
+ }
951
+
952
+ // 8. Apply the diff
953
+ const result = applyDiff(diff, { conversationId });
954
+
955
+ // 9. Apply deferred edges and triggers using the created node IDs
956
+ const createdNodeIds = result.createdNodeIds;
957
+ let edgesCreated = result.edgesCreated;
958
+ let triggersCreated = result.triggersCreated;
959
+
960
+ for (const de of deferredEdges) {
961
+ const newNodeId = createdNodeIds[de.newNodeIndex];
962
+ if (!newNodeId) continue;
963
+
964
+ createEdge({
965
+ sourceNodeId: newNodeId,
966
+ targetNodeId: de.targetNodeId,
967
+ relationship: de.relationship as NewEdge["relationship"],
968
+ weight: de.weight,
969
+ created: conversationTimestamp,
970
+ });
971
+ edgesCreated++;
972
+ }
973
+
974
+ const { createTrigger } = await import("./store.js");
975
+
976
+ for (const dt of deferredTriggers) {
977
+ const newNodeId = createdNodeIds[dt.newNodeIndex];
978
+ if (!newNodeId) continue;
979
+
980
+ const trigger = createTrigger({
981
+ ...dt.trigger,
982
+ nodeId: newNodeId,
983
+ });
984
+ triggersCreated++;
985
+
986
+ if (trigger.type === "semantic" && trigger.condition) {
987
+ enqueueGraphTriggerEmbed(trigger.id);
988
+ }
989
+ }
990
+
991
+ // 10. Embed new nodes — inline for bootstrap, async for live conversations
992
+ const createdNodes = getNodesByIds(createdNodeIds);
993
+ if (opts?.embedInline) {
994
+ const { embedGraphNodeDirect } = await import("./graph-search.js");
995
+ for (const node of createdNodes) {
996
+ try {
997
+ await embedGraphNodeDirect(node, config);
998
+ } catch (err) {
999
+ const msg = err instanceof Error ? err.message : String(err);
1000
+ log.warn(
1001
+ { nodeId: node.id, err: msg },
1002
+ "Inline embed failed (non-fatal)",
1003
+ );
1004
+ console.error(` [embed] Failed for ${node.id}: ${msg}`);
1005
+ }
1006
+ }
1007
+ } else {
1008
+ for (const node of createdNodes) {
1009
+ enqueueGraphNodeEmbed(node.id);
1010
+ }
1011
+ }
1012
+
1013
+ log.info(
1014
+ {
1015
+ conversationId,
1016
+ nodesCreated: result.nodesCreated,
1017
+ nodesUpdated: result.nodesUpdated,
1018
+ nodesReinforced: result.nodesReinforced,
1019
+ edgesCreated,
1020
+ triggersCreated,
1021
+ },
1022
+ "Graph extraction complete",
1023
+ );
1024
+
1025
+ return {
1026
+ nodesCreated: result.nodesCreated,
1027
+ nodesUpdated: result.nodesUpdated,
1028
+ nodesReinforced: result.nodesReinforced,
1029
+ edgesCreated,
1030
+ triggersCreated,
1031
+ lastProcessedTimestamp: conversationTimestamp,
1032
+ };
1033
+ }
1034
+
1035
+ // ---------------------------------------------------------------------------
1036
+ // Helpers
1037
+ // ---------------------------------------------------------------------------
1038
+
1039
+ function resolveConversationTimestamp(conversationId: string): number | null {
1040
+ const db = getDb();
1041
+ // Use the last message timestamp, not the conversation creation time.
1042
+ // A conversation can span hours/days — memories should be timestamped
1043
+ // to when the relevant content was actually discussed.
1044
+ const lastMsg = db
1045
+ .select({ createdAt: messages.createdAt })
1046
+ .from(messages)
1047
+ .where(eq(messages.conversationId, conversationId))
1048
+ .orderBy(desc(messages.createdAt))
1049
+ .limit(1)
1050
+ .get();
1051
+ if (lastMsg) return lastMsg.createdAt;
1052
+
1053
+ // Fallback to conversation creation time if no messages in DB
1054
+ const conv = db
1055
+ .select({ createdAt: conversations.createdAt })
1056
+ .from(conversations)
1057
+ .where(eq(conversations.id, conversationId))
1058
+ .get();
1059
+ return conv?.createdAt ?? null;
1060
+ }
1061
+
1062
+ function resolveImageRefMimeType(
1063
+ messageId: string,
1064
+ blockIndex: number,
1065
+ conversationId: string,
1066
+ ): string | null {
1067
+ const db = getDb();
1068
+ const msg = db
1069
+ .select({ content: messages.content })
1070
+ .from(messages)
1071
+ .where(
1072
+ and(
1073
+ eq(messages.id, messageId),
1074
+ eq(messages.conversationId, conversationId),
1075
+ ),
1076
+ )
1077
+ .get();
1078
+ if (!msg) return null;
1079
+
1080
+ try {
1081
+ const blocks = JSON.parse(msg.content) as Array<{
1082
+ type?: string;
1083
+ source?: { media_type?: string };
1084
+ }>;
1085
+ const block = blocks[blockIndex];
1086
+ if (!block || block.type !== "image") return null;
1087
+ return block.source?.media_type ?? null;
1088
+ } catch {
1089
+ return null;
1090
+ }
1091
+ }
1092
+
1093
+ function loadTranscriptFromDisk(
1094
+ conversationId: string,
1095
+ afterTimestamp?: number,
1096
+ ): string | null {
1097
+ const db = getDb();
1098
+ const conv = db
1099
+ .select({ createdAt: conversations.createdAt })
1100
+ .from(conversations)
1101
+ .where(eq(conversations.id, conversationId))
1102
+ .get();
1103
+
1104
+ if (!conv) return null;
1105
+
1106
+ try {
1107
+ const dirPath = getConversationDirPath(conversationId, conv.createdAt);
1108
+ const messagesPath = join(dirPath, "messages.jsonl");
1109
+ const content = readFileSync(messagesPath, "utf-8");
1110
+
1111
+ const lines = content
1112
+ .trim()
1113
+ .split("\n")
1114
+ .filter((line) => line.length > 0);
1115
+
1116
+ const parts: string[] = [];
1117
+ for (const line of lines) {
1118
+ try {
1119
+ const msg = JSON.parse(line) as {
1120
+ role?: string;
1121
+ content?: string;
1122
+ ts?: string;
1123
+ };
1124
+ if (!msg.role || !msg.content) continue;
1125
+
1126
+ // Filter by timestamp for incremental extraction
1127
+ if (afterTimestamp && msg.ts) {
1128
+ const msgTime = new Date(msg.ts).getTime();
1129
+ if (msgTime <= afterTimestamp) continue;
1130
+ }
1131
+
1132
+ parts.push(`[${msg.role}]: ${msg.content}`);
1133
+ } catch {
1134
+ // Skip malformed lines
1135
+ }
1136
+ }
1137
+
1138
+ return parts.length > 0 ? parts.join("\n\n") : null;
1139
+ } catch {
1140
+ return null;
1141
+ }
1142
+ }
1143
+
1144
+ /**
1145
+ * Load a conversation transcript from the DB with interleaved text and image
1146
+ * content blocks. Returns a single consolidated `Message` with role "user"
1147
+ * containing text annotations and `ImageContent` blocks so the extraction LLM
1148
+ * can see images alongside their textual context.
1149
+ *
1150
+ * Images are capped at 10 per transcript to control extraction cost.
1151
+ */
1152
+ export function loadTranscriptWithImages(
1153
+ conversationId: string,
1154
+ afterTimestamp?: number,
1155
+ ): {
1156
+ message: Message;
1157
+ hasImages: boolean;
1158
+ lastTimestamp: number | null;
1159
+ } | null {
1160
+ const db = getDb();
1161
+
1162
+ // Build query conditions
1163
+ const conditions = [eq(messages.conversationId, conversationId)];
1164
+ if (afterTimestamp !== undefined) {
1165
+ conditions.push(gt(messages.createdAt, afterTimestamp));
1166
+ }
1167
+
1168
+ const rows = db
1169
+ .select({
1170
+ id: messages.id,
1171
+ role: messages.role,
1172
+ content: messages.content,
1173
+ createdAt: messages.createdAt,
1174
+ })
1175
+ .from(messages)
1176
+ .where(and(...conditions))
1177
+ .orderBy(asc(messages.createdAt))
1178
+ .all();
1179
+
1180
+ if (rows.length === 0) return null;
1181
+
1182
+ const MAX_IMAGES = 10;
1183
+ let imageCount = 0;
1184
+ let hasImagesFlag = false;
1185
+ let totalTextLength = 0;
1186
+ let lastTimestamp: number | null = null;
1187
+
1188
+ const contentBlocks: ContentBlock[] = [];
1189
+
1190
+ for (const row of rows) {
1191
+ lastTimestamp = row.createdAt;
1192
+
1193
+ let parsed: ContentBlock[];
1194
+ try {
1195
+ const raw = JSON.parse(row.content) as unknown;
1196
+ if (typeof raw === "string") {
1197
+ parsed = [{ type: "text", text: raw }];
1198
+ } else if (Array.isArray(raw)) {
1199
+ parsed = raw as ContentBlock[];
1200
+ } else {
1201
+ continue;
1202
+ }
1203
+ } catch {
1204
+ // If content is a plain string (not JSON), wrap it
1205
+ parsed = [{ type: "text", text: row.content }];
1206
+ }
1207
+
1208
+ // Build content blocks preserving original text/image interleaving
1209
+ let prefixAdded = false;
1210
+ for (let i = 0; i < parsed.length; i++) {
1211
+ const block = parsed[i];
1212
+ if (block?.type === "text") {
1213
+ const rawText = typeof block.text === "string" ? block.text : "";
1214
+ const text = prefixAdded ? rawText : `[${row.role}]: ${rawText}`;
1215
+ prefixAdded = true;
1216
+ totalTextLength += text.length;
1217
+ contentBlocks.push({ type: "text", text });
1218
+ } else if (block?.type === "image") {
1219
+ if (imageCount < MAX_IMAGES) {
1220
+ const imgBlock = block as ImageContent;
1221
+ // Add annotation so the extraction LLM knows the image's reference coordinates
1222
+ contentBlocks.push({
1223
+ type: "text",
1224
+ text: `<image message_id="${row.id}" block_index="${i}" type="${imgBlock.source.media_type}" />`,
1225
+ });
1226
+ contentBlocks.push(imgBlock);
1227
+ imageCount++;
1228
+ hasImagesFlag = true;
1229
+ }
1230
+ // After cap, skip image blocks but continue processing text
1231
+ }
1232
+ }
1233
+ }
1234
+
1235
+ // Skip if transcript is too short (images count toward the threshold)
1236
+ if (totalTextLength < 100 && !hasImagesFlag) return null;
1237
+
1238
+ const message: Message = {
1239
+ role: "user",
1240
+ content: contentBlocks,
1241
+ };
1242
+
1243
+ return { message, hasImages: hasImagesFlag, lastTimestamp };
1244
+ }
1245
+
1246
+ async function findCandidateNodes(
1247
+ transcript: string,
1248
+ scopeId: string,
1249
+ config: AssistantConfig,
1250
+ activeContextNodeIds?: string[],
1251
+ skipQdrant?: boolean,
1252
+ ) {
1253
+ const allNodeIds = new Set<string>();
1254
+
1255
+ if (skipQdrant) {
1256
+ // Bootstrap mode: load candidates directly from DB (embeddings may not be ready).
1257
+ // Get the most recent and most significant non-gone nodes.
1258
+ const dbCandidates = queryNodes({
1259
+ scopeId,
1260
+ fidelityNot: ["gone"],
1261
+ limit: 100,
1262
+ });
1263
+ for (const node of dbCandidates) allNodeIds.add(node.id);
1264
+ } else {
1265
+ // Live mode: semantic search via Qdrant
1266
+ const { embedWithRetry } = await import("../embed.js");
1267
+ const searchText =
1268
+ transcript.length > 3000
1269
+ ? transcript.slice(0, 1500) + "\n...\n" + transcript.slice(-1500)
1270
+ : transcript;
1271
+
1272
+ try {
1273
+ const embedding = await embedWithRetry(config, [searchText]);
1274
+ const queryVector = embedding.vectors[0];
1275
+ if (queryVector) {
1276
+ const searchResults = await searchGraphNodes(queryVector, 100, [
1277
+ scopeId,
1278
+ ]);
1279
+ for (const r of searchResults) allNodeIds.add(r.nodeId);
1280
+ }
1281
+ } catch (err) {
1282
+ log.warn(
1283
+ { err },
1284
+ "Failed to embed transcript for candidate search, continuing without candidates",
1285
+ );
1286
+ }
1287
+ }
1288
+
1289
+ // Combine with active context nodes
1290
+ if (activeContextNodeIds) {
1291
+ for (const id of activeContextNodeIds) allNodeIds.add(id);
1292
+ }
1293
+
1294
+ if (allNodeIds.size === 0) return [];
1295
+
1296
+ return getNodesByIds([...allNodeIds]);
1297
+ }