@vellumai/assistant 0.5.16 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (407) hide show
  1. package/ARCHITECTURE.md +1 -1
  2. package/Dockerfile +0 -3
  3. package/knip.json +2 -1
  4. package/openapi.yaml +660 -80
  5. package/package.json +1 -1
  6. package/src/__tests__/actor-token-service.test.ts +68 -0
  7. package/src/__tests__/agent-loop.test.ts +0 -32
  8. package/src/__tests__/always-loaded-tools-guard.test.ts +2 -2
  9. package/src/__tests__/anthropic-provider.test.ts +57 -3
  10. package/src/__tests__/app-compiler.test.ts +120 -0
  11. package/src/__tests__/assistant-feature-flags-integration.test.ts +2 -2
  12. package/src/__tests__/call-conversation-messages.test.ts +2 -6
  13. package/src/__tests__/call-domain.test.ts +2 -6
  14. package/src/__tests__/call-pointer-messages.test.ts +2 -14
  15. package/src/__tests__/call-recovery.test.ts +2 -6
  16. package/src/__tests__/call-routes-http.test.ts +2 -6
  17. package/src/__tests__/call-store.test.ts +2 -6
  18. package/src/__tests__/cancel-resolves-conversation-key.test.ts +2 -6
  19. package/src/__tests__/canonical-guardian-store.test.ts +2 -6
  20. package/src/__tests__/channel-delivery-store.test.ts +2 -6
  21. package/src/__tests__/channel-retry-sweep.test.ts +2 -6
  22. package/src/__tests__/checker.test.ts +25 -3
  23. package/src/__tests__/clawhub.test.ts +54 -24
  24. package/src/__tests__/cli-command-risk-guard.test.ts +14 -0
  25. package/src/__tests__/cli-memory.test.ts +74 -69
  26. package/src/__tests__/config-schema.test.ts +1 -1
  27. package/src/__tests__/config-set-platform-guard.test.ts +302 -0
  28. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +2 -6
  29. package/src/__tests__/contacts-tools.test.ts +31 -0
  30. package/src/__tests__/context-overflow-reducer.test.ts +86 -0
  31. package/src/__tests__/context-token-estimator.test.ts +175 -10
  32. package/src/__tests__/conversation-agent-loop-overflow.test.ts +9 -0
  33. package/src/__tests__/conversation-agent-loop.test.ts +9 -0
  34. package/src/__tests__/conversation-attachments.test.ts +2 -6
  35. package/src/__tests__/conversation-attention-store.test.ts +2 -6
  36. package/src/__tests__/conversation-clear-safety.test.ts +2 -6
  37. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +4 -10
  38. package/src/__tests__/conversation-disk-view-integration.test.ts +2 -6
  39. package/src/__tests__/conversation-disk-view.test.ts +2 -6
  40. package/src/__tests__/conversation-error.test.ts +33 -2
  41. package/src/__tests__/conversation-fork-crud.test.ts +2 -6
  42. package/src/__tests__/conversation-history-web-search.test.ts +5 -0
  43. package/src/__tests__/conversation-load-history-repair.test.ts +5 -1
  44. package/src/__tests__/conversation-media-retry.test.ts +91 -0
  45. package/src/__tests__/conversation-starter-routes.test.ts +20 -11
  46. package/src/__tests__/conversation-store.test.ts +2 -6
  47. package/src/__tests__/conversation-usage.test.ts +2 -6
  48. package/src/__tests__/conversation-wipe.test.ts +11 -408
  49. package/src/__tests__/credential-execution-feature-gates.test.ts +3 -3
  50. package/src/__tests__/credential-execution-shell-lockdown.test.ts +2 -2
  51. package/src/__tests__/credential-security-e2e.test.ts +2 -0
  52. package/src/__tests__/followup-tools.test.ts +2 -6
  53. package/src/__tests__/graph-extraction-event-date.test.ts +186 -0
  54. package/src/__tests__/guardian-action-conversation-turn.test.ts +2 -6
  55. package/src/__tests__/guardian-action-followup-executor.test.ts +2 -6
  56. package/src/__tests__/guardian-action-followup-store.test.ts +2 -6
  57. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +2 -6
  58. package/src/__tests__/guardian-action-late-reply.test.ts +2 -6
  59. package/src/__tests__/guardian-action-store.test.ts +2 -6
  60. package/src/__tests__/guardian-binding-drift-heal.test.ts +2 -6
  61. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +8 -8
  62. package/src/__tests__/guardian-dispatch.test.ts +2 -6
  63. package/src/__tests__/guardian-grant-minting.test.ts +2 -14
  64. package/src/__tests__/guardian-principal-id-roundtrip.test.ts +2 -6
  65. package/src/__tests__/guardian-routing-invariants.test.ts +192 -6
  66. package/src/__tests__/guardian-routing-state.test.ts +2 -6
  67. package/src/__tests__/guardian-verification-voice-binding.test.ts +2 -6
  68. package/src/__tests__/inbound-invite-redemption.test.ts +2 -6
  69. package/src/__tests__/injection-block.test.ts +154 -0
  70. package/src/__tests__/install-meta.test.ts +506 -0
  71. package/src/__tests__/install-skill-routing.test.ts +292 -0
  72. package/src/__tests__/invite-redemption-service.test.ts +2 -6
  73. package/src/__tests__/invite-routes-http.test.ts +2 -6
  74. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +2 -14
  75. package/src/__tests__/list-messages-attachments.test.ts +2 -6
  76. package/src/__tests__/llm-context-route-provider.test.ts +2 -6
  77. package/src/__tests__/llm-request-log-turn-query.test.ts +2 -6
  78. package/src/__tests__/llm-usage-store.test.ts +2 -6
  79. package/src/__tests__/log-export-workspace.test.ts +2 -6
  80. package/src/__tests__/managed-store.test.ts +38 -11
  81. package/src/__tests__/memory-jobs-worker-backoff.test.ts +2 -8
  82. package/src/__tests__/memory-recall-log-store.test.ts +2 -6
  83. package/src/__tests__/memory-upsert-concurrency.test.ts +4 -112
  84. package/src/__tests__/non-member-access-request.test.ts +2 -6
  85. package/src/__tests__/notification-guardian-path.test.ts +2 -6
  86. package/src/__tests__/oauth-cli.test.ts +364 -2
  87. package/src/__tests__/oauth2-gateway-transport.test.ts +18 -3
  88. package/src/__tests__/outlook-attachments.test.ts +301 -0
  89. package/src/__tests__/outlook-automation-tools.test.ts +425 -0
  90. package/src/__tests__/outlook-categories.test.ts +212 -0
  91. package/src/__tests__/outlook-client-automation.test.ts +246 -0
  92. package/src/__tests__/outlook-compose-tools.test.ts +325 -0
  93. package/src/__tests__/outlook-declutter-tools.test.ts +585 -0
  94. package/src/__tests__/outlook-email-watcher.test.ts +322 -0
  95. package/src/__tests__/outlook-follow-up.test.ts +196 -0
  96. package/src/__tests__/outlook-messaging-provider.test.ts +498 -3
  97. package/src/__tests__/outlook-trash.test.ts +77 -0
  98. package/src/__tests__/outlook-unsubscribe.test.ts +250 -0
  99. package/src/__tests__/platform-callback-registration.test.ts +4 -4
  100. package/src/__tests__/playbook-execution.test.ts +76 -80
  101. package/src/__tests__/playbook-tools.test.ts +5 -7
  102. package/src/__tests__/provider-error-scenarios.test.ts +21 -0
  103. package/src/__tests__/rebuild-index-graph-nodes.test.ts +273 -0
  104. package/src/__tests__/registry.test.ts +2 -2
  105. package/src/__tests__/require-fresh-approval.test.ts +64 -2
  106. package/src/__tests__/runtime-events-sse-parity.test.ts +2 -6
  107. package/src/__tests__/runtime-events-sse.test.ts +2 -6
  108. package/src/__tests__/schedule-store.test.ts +2 -6
  109. package/src/__tests__/schedule-tools.test.ts +2 -6
  110. package/src/__tests__/scheduler-recurrence.test.ts +1 -5
  111. package/src/__tests__/scoped-approval-grants.test.ts +2 -6
  112. package/src/__tests__/scoped-grant-security-matrix.test.ts +2 -6
  113. package/src/__tests__/search-skills-unified.test.ts +421 -0
  114. package/src/__tests__/secret-onetime-send.test.ts +2 -0
  115. package/src/__tests__/send-endpoint-busy.test.ts +2 -6
  116. package/src/__tests__/sequence-store.test.ts +2 -6
  117. package/src/__tests__/server-history-render.test.ts +2 -6
  118. package/src/__tests__/skill-feature-flags-integration.test.ts +38 -31
  119. package/src/__tests__/skill-feature-flags.test.ts +6 -6
  120. package/src/__tests__/skill-load-feature-flag.test.ts +11 -11
  121. package/src/__tests__/skill-memory.test.ts +140 -98
  122. package/src/__tests__/skills-uninstall.test.ts +2 -2
  123. package/src/__tests__/skills.test.ts +1 -1
  124. package/src/__tests__/slack-inbound-verification.test.ts +2 -6
  125. package/src/__tests__/task-compiler.test.ts +2 -6
  126. package/src/__tests__/task-management-tools.test.ts +2 -6
  127. package/src/__tests__/task-memory-cleanup.test.ts +173 -229
  128. package/src/__tests__/task-runner.test.ts +2 -6
  129. package/src/__tests__/task-scheduler.test.ts +2 -6
  130. package/src/__tests__/test-preload.ts +3 -0
  131. package/src/__tests__/tool-approval-handler.test.ts +2 -6
  132. package/src/__tests__/tool-grant-request-escalation.test.ts +2 -6
  133. package/src/__tests__/tool-side-effects-slack-dm.test.ts +276 -0
  134. package/src/__tests__/trust-store.test.ts +1 -1
  135. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -6
  136. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +2 -6
  137. package/src/__tests__/trusted-contact-multichannel.test.ts +2 -6
  138. package/src/__tests__/trusted-contact-verification.test.ts +2 -6
  139. package/src/__tests__/turn-boundary-resolution.test.ts +2 -6
  140. package/src/__tests__/usage-cache-backfill-migration.test.ts +1 -6
  141. package/src/__tests__/usage-routes.test.ts +2 -6
  142. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  143. package/src/__tests__/voice-invite-redemption.test.ts +2 -6
  144. package/src/__tests__/voice-scoped-grant-consumer.test.ts +2 -6
  145. package/src/__tests__/voice-session-bridge.test.ts +2 -6
  146. package/src/__tests__/volume-security-guard.test.ts +2 -0
  147. package/src/__tests__/workspace-lifecycle.test.ts +29 -1
  148. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +2 -6
  149. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +2 -6
  150. package/src/__tests__/workspace-migration-026-backfill-install-meta.test.ts +558 -0
  151. package/src/__tests__/workspace-policy.test.ts +1 -1
  152. package/src/agent/attachments.ts +7 -2
  153. package/src/agent/image-optimize.ts +165 -0
  154. package/src/agent/loop.ts +1 -15
  155. package/src/bundler/app-compiler.ts +179 -2
  156. package/src/bundler/package-resolver.ts +3 -5
  157. package/src/cli/__tests__/notifications.test.ts +1 -2
  158. package/src/cli/cli-memory.ts +67 -64
  159. package/src/cli/commands/avatar.ts +3 -3
  160. package/src/cli/commands/config.ts +26 -13
  161. package/src/cli/commands/doctor.ts +2 -2
  162. package/src/cli/commands/memory.ts +41 -55
  163. package/src/cli/commands/oauth/__tests__/connect.test.ts +2 -2
  164. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +2 -2
  165. package/src/cli/commands/oauth/__tests__/mode.test.ts +8 -1
  166. package/src/cli/commands/oauth/__tests__/status.test.ts +2 -2
  167. package/src/cli/commands/oauth/connect.ts +11 -6
  168. package/src/cli/commands/oauth/mode.ts +7 -0
  169. package/src/cli/commands/oauth/shared.ts +39 -3
  170. package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
  171. package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
  172. package/src/cli/commands/platform/__tests__/status.test.ts +5 -5
  173. package/src/cli/commands/platform/index.ts +16 -16
  174. package/src/cli/commands/skills.ts +88 -16
  175. package/src/cli/commands/trust.ts +2 -2
  176. package/src/cli/lib/daemon-credential-client.ts +2 -3
  177. package/src/config/bundled-skills/acp/TOOLS.json +1 -1
  178. package/src/config/bundled-skills/contacts/SKILL.md +0 -1
  179. package/src/config/bundled-skills/contacts/TOOLS.json +0 -8
  180. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +0 -4
  181. package/src/config/bundled-skills/gmail/SKILL.md +2 -10
  182. package/src/config/bundled-skills/google-calendar/SKILL.md +1 -9
  183. package/src/config/bundled-skills/messaging/SKILL.md +10 -18
  184. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +40 -33
  185. package/src/config/bundled-skills/outlook/SKILL.md +189 -0
  186. package/src/config/bundled-skills/outlook/TOOLS.json +530 -0
  187. package/src/config/bundled-skills/outlook/tools/outlook-attachments.ts +85 -0
  188. package/src/config/bundled-skills/outlook/tools/outlook-categories.ts +77 -0
  189. package/src/config/bundled-skills/outlook/tools/outlook-draft.ts +84 -0
  190. package/src/config/bundled-skills/outlook/tools/outlook-follow-up.ts +94 -0
  191. package/src/config/bundled-skills/outlook/tools/outlook-forward.ts +49 -0
  192. package/src/config/bundled-skills/outlook/tools/outlook-outreach-scan.ts +237 -0
  193. package/src/config/bundled-skills/outlook/tools/outlook-rules.ts +161 -0
  194. package/src/config/bundled-skills/outlook/tools/outlook-send-draft.ts +32 -0
  195. package/src/config/bundled-skills/outlook/tools/outlook-sender-digest.ts +272 -0
  196. package/src/config/bundled-skills/outlook/tools/outlook-trash.ts +29 -0
  197. package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +129 -0
  198. package/src/config/bundled-skills/outlook/tools/outlook-vacation.ts +87 -0
  199. package/src/config/bundled-skills/outlook/tools/shared.ts +20 -0
  200. package/src/config/bundled-skills/outlook-calendar/SKILL.md +51 -0
  201. package/src/config/bundled-skills/outlook-calendar/TOOLS.json +221 -0
  202. package/src/config/bundled-skills/outlook-calendar/calendar-client.ts +252 -0
  203. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-check-availability.ts +53 -0
  204. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-create-event.ts +74 -0
  205. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-get-event.ts +18 -0
  206. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-list-events.ts +46 -0
  207. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-rsvp.ts +36 -0
  208. package/src/config/bundled-skills/outlook-calendar/tools/shared.ts +17 -0
  209. package/src/config/bundled-skills/outlook-calendar/types.ts +120 -0
  210. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +47 -40
  211. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +16 -29
  212. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +16 -18
  213. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +39 -47
  214. package/src/config/bundled-skills/slack/SKILL.md +1 -7
  215. package/src/config/bundled-tool-registry.ts +56 -4
  216. package/src/config/env-registry.ts +15 -8
  217. package/src/config/feature-flag-registry.json +21 -124
  218. package/src/config/schemas/platform.ts +8 -0
  219. package/src/config/schemas/timeouts.ts +1 -1
  220. package/src/config/skills.ts +18 -7
  221. package/src/context/token-estimator.ts +25 -18
  222. package/src/context/window-manager.ts +6 -2
  223. package/src/credential-execution/process-manager.ts +3 -1
  224. package/src/daemon/context-overflow-reducer.ts +46 -2
  225. package/src/daemon/conversation-agent-loop-handlers.ts +123 -82
  226. package/src/daemon/conversation-agent-loop.ts +96 -61
  227. package/src/daemon/conversation-error.ts +31 -8
  228. package/src/daemon/conversation-lifecycle.ts +33 -0
  229. package/src/daemon/conversation-media-retry.ts +85 -7
  230. package/src/daemon/conversation-notifiers.ts +4 -1
  231. package/src/daemon/conversation-runtime-assembly.ts +5 -0
  232. package/src/daemon/conversation.ts +41 -2
  233. package/src/daemon/daemon-control.ts +8 -2
  234. package/src/daemon/handlers/shared.ts +22 -12
  235. package/src/daemon/handlers/skills.ts +416 -202
  236. package/src/daemon/lifecycle.ts +40 -1
  237. package/src/daemon/main.ts +5 -1
  238. package/src/daemon/message-types/conversations.ts +4 -1
  239. package/src/daemon/message-types/messages.ts +3 -1
  240. package/src/daemon/message-types/skills.ts +97 -36
  241. package/src/daemon/providers-setup.ts +5 -0
  242. package/src/daemon/server.ts +11 -2
  243. package/src/daemon/tool-side-effects.ts +27 -5
  244. package/src/heartbeat/heartbeat-service.ts +1 -0
  245. package/src/hooks/cli.ts +2 -2
  246. package/src/hooks/runner.ts +15 -38
  247. package/src/inbound/platform-callback-registration.ts +14 -14
  248. package/src/memory/admin.ts +11 -45
  249. package/src/memory/conversation-bootstrap.ts +2 -0
  250. package/src/memory/conversation-crud.ts +242 -348
  251. package/src/memory/conversation-group-migration.ts +157 -0
  252. package/src/memory/conversation-queries.ts +4 -2
  253. package/src/memory/db-init.ts +30 -3
  254. package/src/memory/embed.ts +73 -0
  255. package/src/memory/embedding-backend.ts +8 -14
  256. package/src/memory/embedding-runtime-manager.ts +12 -114
  257. package/src/memory/fingerprint.ts +2 -2
  258. package/src/memory/graph/bootstrap.ts +512 -0
  259. package/src/memory/graph/capability-seed.ts +297 -0
  260. package/src/memory/graph/consolidation.ts +691 -0
  261. package/src/memory/graph/conversation-graph-memory.ts +630 -0
  262. package/src/memory/graph/decay.test.ts +208 -0
  263. package/src/memory/graph/decay.ts +195 -0
  264. package/src/memory/graph/extraction-job.ts +69 -0
  265. package/src/memory/graph/extraction.test.ts +936 -0
  266. package/src/memory/graph/extraction.ts +1254 -0
  267. package/src/memory/graph/graph-search.ts +266 -0
  268. package/src/memory/graph/image-ref-utils.ts +29 -0
  269. package/src/memory/graph/injection.test.ts +513 -0
  270. package/src/memory/graph/injection.ts +439 -0
  271. package/src/memory/graph/inspect.ts +534 -0
  272. package/src/memory/graph/narrative.ts +267 -0
  273. package/src/memory/graph/pattern-scan.ts +269 -0
  274. package/src/memory/graph/retriever.ts +1008 -0
  275. package/src/memory/graph/scoring.test.ts +548 -0
  276. package/src/memory/graph/scoring.ts +232 -0
  277. package/src/memory/graph/serendipity.ts +65 -0
  278. package/src/memory/graph/store.test.ts +1050 -0
  279. package/src/memory/graph/store.ts +699 -0
  280. package/src/memory/graph/tool-handlers.ts +426 -0
  281. package/src/memory/graph/tools.ts +141 -0
  282. package/src/memory/graph/triggers.test.ts +487 -0
  283. package/src/memory/graph/triggers.ts +223 -0
  284. package/src/memory/graph/types.ts +271 -0
  285. package/src/memory/group-crud.ts +191 -0
  286. package/src/memory/indexer.ts +37 -19
  287. package/src/memory/job-handlers/cleanup.ts +0 -53
  288. package/src/memory/job-handlers/conversation-starters.ts +91 -53
  289. package/src/memory/job-handlers/embedding.ts +5 -31
  290. package/src/memory/job-handlers/index-maintenance.ts +23 -11
  291. package/src/memory/job-handlers/summarization.ts +32 -17
  292. package/src/memory/job-utils.ts +1 -1
  293. package/src/memory/jobs-store.ts +50 -70
  294. package/src/memory/jobs-worker.ts +147 -112
  295. package/src/memory/message-content.ts +1 -0
  296. package/src/memory/migrations/202-memory-graph-tables.ts +130 -0
  297. package/src/memory/migrations/203-drop-memory-items-tables.ts +23 -0
  298. package/src/memory/migrations/204-rename-memory-graph-type-values.ts +46 -0
  299. package/src/memory/migrations/205-memory-graph-image-refs.ts +11 -0
  300. package/src/memory/migrations/index.ts +4 -0
  301. package/src/memory/migrations/registry.ts +8 -0
  302. package/src/memory/qdrant-client.ts +44 -17
  303. package/src/memory/schema/index.ts +1 -0
  304. package/src/memory/schema/memory-graph.ts +139 -0
  305. package/src/memory/search/semantic.ts +47 -91
  306. package/src/memory/task-memory-cleanup.ts +28 -50
  307. package/src/messaging/providers/outlook/adapter.ts +8 -1
  308. package/src/messaging/providers/outlook/client.ts +299 -0
  309. package/src/messaging/providers/outlook/types.ts +118 -0
  310. package/src/notifications/adapters/macos.ts +1 -0
  311. package/src/notifications/copy-composer.ts +9 -0
  312. package/src/notifications/signal.ts +16 -0
  313. package/src/oauth/seed-providers.ts +2 -1
  314. package/src/permissions/checker.ts +24 -3
  315. package/src/permissions/defaults.ts +4 -4
  316. package/src/permissions/workspace-policy.ts +1 -1
  317. package/src/playbooks/playbook-compiler.ts +19 -18
  318. package/src/playbooks/types.ts +4 -3
  319. package/src/prompts/system-prompt.ts +3 -29
  320. package/src/providers/anthropic/client.ts +47 -19
  321. package/src/providers/gemini/client.ts +1 -1
  322. package/src/providers/openai/client.ts +1 -1
  323. package/src/providers/registry.ts +1 -1
  324. package/src/providers/retry.ts +19 -3
  325. package/src/runtime/actor-trust-resolver.ts +5 -1
  326. package/src/runtime/auth/route-policy.ts +7 -0
  327. package/src/runtime/guardian-reply-router.ts +5 -1
  328. package/src/runtime/http-server.ts +23 -3
  329. package/src/runtime/middleware/auth.ts +20 -0
  330. package/src/runtime/routes/attachment-routes.test.ts +106 -0
  331. package/src/runtime/routes/attachment-routes.ts +106 -16
  332. package/src/runtime/routes/brain-graph-routes.ts +21 -22
  333. package/src/runtime/routes/btw-routes.ts +8 -0
  334. package/src/runtime/routes/conversation-management-routes.ts +2 -0
  335. package/src/runtime/routes/conversation-starter-routes.ts +2 -2
  336. package/src/runtime/routes/debug-routes.ts +1 -1
  337. package/src/runtime/routes/global-search-routes.ts +21 -19
  338. package/src/runtime/routes/group-routes.ts +207 -0
  339. package/src/runtime/routes/guardian-action-routes.ts +21 -10
  340. package/src/runtime/routes/guardian-bootstrap-routes.ts +23 -19
  341. package/src/runtime/routes/inbound-message-handler.ts +19 -0
  342. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +292 -0
  343. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +207 -0
  344. package/src/runtime/routes/memory-item-routes.test.ts +2 -14
  345. package/src/runtime/routes/memory-item-routes.ts +341 -388
  346. package/src/runtime/routes/schedule-routes.ts +2 -0
  347. package/src/runtime/routes/skills-routes.ts +103 -37
  348. package/src/runtime/routes/work-items-routes.test.ts +2 -6
  349. package/src/schedule/scheduler.ts +8 -1
  350. package/src/security/oauth2.ts +1 -1
  351. package/src/security/secure-keys.ts +4 -8
  352. package/src/shared/provider-env-vars.ts +19 -0
  353. package/src/skills/catalog-cache.ts +5 -0
  354. package/src/skills/catalog-install.ts +15 -14
  355. package/src/skills/clawhub.ts +134 -154
  356. package/src/skills/install-meta.ts +208 -0
  357. package/src/skills/managed-store.ts +27 -16
  358. package/src/skills/skill-memory.ts +152 -77
  359. package/src/skills/skillssh-registry.ts +19 -17
  360. package/src/tasks/task-runner.ts +3 -1
  361. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -5
  362. package/src/tools/browser/runtime-check.ts +3 -1
  363. package/src/tools/memory/register.ts +63 -46
  364. package/src/tools/permission-checker.ts +7 -1
  365. package/src/tools/shared/filesystem/image-read.ts +22 -85
  366. package/src/tools/terminal/safe-env.ts +1 -0
  367. package/src/tools/tool-manifest.ts +3 -3
  368. package/src/util/browser.ts +25 -10
  369. package/src/util/bun-runtime.ts +172 -0
  370. package/src/watcher/providers/outlook-calendar.ts +343 -0
  371. package/src/watcher/providers/outlook.ts +198 -0
  372. package/src/workspace/migrations/025-remove-oauth-app-setup-skills.ts +76 -0
  373. package/src/workspace/migrations/026-backfill-install-meta.ts +325 -0
  374. package/src/workspace/migrations/027-remove-orphaned-optimized-images-cache.ts +42 -0
  375. package/src/workspace/migrations/registry.ts +6 -0
  376. package/src/__tests__/context-memory-e2e.test.ts +0 -415
  377. package/src/__tests__/journal-context.test.ts +0 -268
  378. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -297
  379. package/src/__tests__/memory-lifecycle-e2e.test.ts +0 -459
  380. package/src/__tests__/memory-query-builder.test.ts +0 -59
  381. package/src/__tests__/memory-recall-quality.test.ts +0 -1046
  382. package/src/__tests__/memory-regressions.experimental.test.ts +0 -629
  383. package/src/__tests__/memory-regressions.test.ts +0 -3696
  384. package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -295
  385. package/src/daemon/conversation-memory.ts +0 -207
  386. package/src/memory/conversation-starters-cadence.ts +0 -74
  387. package/src/memory/items-extractor.ts +0 -860
  388. package/src/memory/job-handlers/batch-extraction.ts +0 -753
  389. package/src/memory/job-handlers/extraction.ts +0 -40
  390. package/src/memory/job-handlers/journal-carry-forward.test.ts +0 -355
  391. package/src/memory/job-handlers/journal-carry-forward.ts +0 -255
  392. package/src/memory/journal-memory.ts +0 -224
  393. package/src/memory/query-builder.ts +0 -47
  394. package/src/memory/query-expansion.ts +0 -83
  395. package/src/memory/retriever.test.ts +0 -1592
  396. package/src/memory/retriever.ts +0 -1331
  397. package/src/memory/search/formatting.test.ts +0 -140
  398. package/src/memory/search/formatting.ts +0 -262
  399. package/src/memory/search/mmr.ts +0 -139
  400. package/src/memory/search/ranking.ts +0 -15
  401. package/src/memory/search/staleness.ts +0 -40
  402. package/src/memory/search/tier-classifier.ts +0 -18
  403. package/src/memory/search/types.ts +0 -121
  404. package/src/prompts/journal-context.ts +0 -154
  405. package/src/tools/memory/definitions.ts +0 -69
  406. package/src/tools/memory/handlers.test.ts +0 -562
  407. package/src/tools/memory/handlers.ts +0 -434
@@ -0,0 +1,630 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Memory Graph — Conversation-level memory integration
3
+ //
4
+ // Replaces the old `prepareMemoryContext` from conversation-memory.ts.
5
+ // Manages the InContextTracker lifecycle and dispatches to the correct
6
+ // retrieval mode based on conversation state.
7
+ // ---------------------------------------------------------------------------
8
+
9
+ import { and, desc, eq, ne } from "drizzle-orm";
10
+
11
+ import type { AssistantConfig } from "../../config/types.js";
12
+ import { estimateTextTokens } from "../../context/token-estimator.js";
13
+ import type { ServerMessage } from "../../daemon/message-protocol.js";
14
+ import type {
15
+ ContentBlock,
16
+ ImageContent,
17
+ Message,
18
+ } from "../../providers/types.js";
19
+ import { getLogger } from "../../util/logger.js";
20
+ import { getDb } from "../db.js";
21
+ import { memorySummaries } from "../schema.js";
22
+ import { conversations } from "../schema/conversations.js";
23
+ import {
24
+ assembleContextBlock,
25
+ assembleInjectionBlock,
26
+ InContextTracker,
27
+ MAX_CONTEXT_LOAD_IMAGES,
28
+ MAX_PER_TURN_IMAGES,
29
+ MAX_REFRESH_IMAGES,
30
+ type ResolvedImage,
31
+ resolveInjectionImages,
32
+ } from "./injection.js";
33
+ import {
34
+ loadContextMemory,
35
+ REFRESH_INTERVAL_TURNS,
36
+ refreshContextMemory,
37
+ retrieveForTurn,
38
+ } from "./retriever.js";
39
+
40
+ const log = getLogger("graph-conversation-memory");
41
+
42
+ const ESTIMATED_IMAGE_TOKENS = 1000;
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Per-conversation state
46
+ // ---------------------------------------------------------------------------
47
+
48
+ /**
49
+ * Manages memory graph state for a single conversation.
50
+ * Create one per Conversation instance. Persists across turns.
51
+ */
52
+ export class ConversationGraphMemory {
53
+ readonly tracker = new InContextTracker();
54
+ private turnCount = 0;
55
+ private initialized = false;
56
+ private lastCompactedAt: number | null = null;
57
+ private needsReload = false;
58
+ private scopeId: string;
59
+ private conversationId: string;
60
+ private lastInjectedBlock: string | null = null;
61
+ private lastInjectedNodeIds: string[] = [];
62
+ private lastInjectedImages: Map<string, ResolvedImage> = new Map();
63
+
64
+ constructor(scopeId: string, conversationId: string) {
65
+ this.scopeId = scopeId;
66
+ this.conversationId = conversationId;
67
+ }
68
+
69
+ /**
70
+ * Fetch the most recent conversation summaries (excluding the current
71
+ * conversation, which won't have one yet at context-load time).
72
+ *
73
+ * Prioritizes user conversations (conversationType != "background"),
74
+ * allowing at most 1 background conversation summary so the retrieval
75
+ * signal is mostly from direct interactions.
76
+ *
77
+ * Returns up to 3 summary texts, most recent first.
78
+ */
79
+ private fetchRecentSummaries(): string[] {
80
+ try {
81
+ const db = getDb();
82
+ const baseWhere = and(
83
+ eq(memorySummaries.scope, "conversation"),
84
+ eq(memorySummaries.scopeId, this.scopeId),
85
+ ne(memorySummaries.scopeKey, this.conversationId),
86
+ );
87
+
88
+ // Fetch user conversations first (up to 3)
89
+ const userRows = db
90
+ .select({ summary: memorySummaries.summary })
91
+ .from(memorySummaries)
92
+ .innerJoin(
93
+ conversations,
94
+ eq(memorySummaries.scopeKey, conversations.id),
95
+ )
96
+ .where(and(baseWhere, ne(conversations.conversationType, "background")))
97
+ .orderBy(desc(memorySummaries.updatedAt))
98
+ .limit(3)
99
+ .all();
100
+
101
+ if (userRows.length >= 3) {
102
+ return userRows.map((r) => r.summary);
103
+ }
104
+
105
+ // Fill remaining slots with at most 1 background conversation
106
+ const remaining = Math.min(1, 3 - userRows.length);
107
+ const bgRows = db
108
+ .select({ summary: memorySummaries.summary })
109
+ .from(memorySummaries)
110
+ .innerJoin(
111
+ conversations,
112
+ eq(memorySummaries.scopeKey, conversations.id),
113
+ )
114
+ .where(and(baseWhere, eq(conversations.conversationType, "background")))
115
+ .orderBy(desc(memorySummaries.updatedAt))
116
+ .limit(remaining)
117
+ .all();
118
+
119
+ return [...userRows, ...bgRows].map((r) => r.summary);
120
+ } catch (err) {
121
+ log.warn({ err }, "Failed to fetch recent conversation summaries");
122
+ return [];
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Notify that context compaction just happened.
128
+ * On the next turn, we'll re-run full context load.
129
+ */
130
+ onCompacted(compactedMessageCount: number): void {
131
+ // Evict everything — compaction summarized all prior turns.
132
+ // The tracker can't know exactly which turns were compacted,
133
+ // so we conservatively clear everything and reload.
134
+ this.tracker.evictCompactedTurns(this.tracker.getTurn());
135
+ this.needsReload = true;
136
+ this.lastCompactedAt = Date.now();
137
+ log.info(
138
+ { compactedMessageCount },
139
+ "Compaction detected — will reload context on next turn",
140
+ );
141
+ }
142
+
143
+ /**
144
+ * Re-inject the most recent memory block after context compaction.
145
+ * Synchronous — reuses the cached block from the last successful retrieval.
146
+ * Does NOT advance turn count or run new retrieval.
147
+ */
148
+ reinjectCachedMemory(messages: Message[]): {
149
+ runMessages: Message[];
150
+ injectedTokens: number;
151
+ } {
152
+ if (!this.lastInjectedBlock) {
153
+ return { runMessages: messages, injectedTokens: 0 };
154
+ }
155
+ // Re-track node IDs since onCompacted evicted them
156
+ this.tracker.add(this.lastInjectedNodeIds);
157
+ // Strip any existing <memory __injected> blocks from the last user message
158
+ // before re-injecting, so compaction sites don't end up with duplicates.
159
+ const cleaned = stripExistingMemoryInjections(messages);
160
+
161
+ const injectedTokens =
162
+ estimateTextTokens(this.lastInjectedBlock) +
163
+ this.lastInjectedImages.size * ESTIMATED_IMAGE_TOKENS;
164
+
165
+ if (this.lastInjectedImages.size > 0) {
166
+ return {
167
+ runMessages: injectMemoryBlock(
168
+ cleaned,
169
+ this.lastInjectedBlock,
170
+ this.lastInjectedImages,
171
+ ),
172
+ injectedTokens,
173
+ };
174
+ }
175
+
176
+ return {
177
+ runMessages: injectTextBlock(cleaned, this.lastInjectedBlock),
178
+ injectedTokens,
179
+ };
180
+ }
181
+
182
+ /**
183
+ * Main entry point — called on every turn before the LLM sees the messages.
184
+ *
185
+ * Dispatches to the appropriate retrieval mode:
186
+ * - Turn 1 (or after compaction): full context load
187
+ * - Every 5 turns: periodic refresh
188
+ * - Every other turn: per-turn injection
189
+ *
190
+ * Returns augmented messages with memory context prepended to the last
191
+ * user message, following the same injection pattern as the old system.
192
+ */
193
+ async prepareMemory(
194
+ messages: Message[],
195
+ config: AssistantConfig,
196
+ abortSignal: AbortSignal,
197
+ onEvent: (msg: ServerMessage) => void,
198
+ ): Promise<{
199
+ runMessages: Message[];
200
+ injectedTokens: number;
201
+ latencyMs: number;
202
+ mode: "context-load" | "refresh" | "per-turn" | "none";
203
+ /** The raw text content of the injected block (without XML wrapper), or null if nothing was injected. */
204
+ injectedBlockText: string | null;
205
+ }> {
206
+ this.turnCount++;
207
+ this.tracker.advanceTurn();
208
+
209
+ const noopResult = {
210
+ runMessages: messages,
211
+ injectedTokens: 0,
212
+ latencyMs: 0,
213
+ mode: "none" as const,
214
+ injectedBlockText: null as string | null,
215
+ };
216
+
217
+ // Gate: skip for empty/tool-result-only messages — unless we need to
218
+ // reload after compaction (needsReload) or haven't initialized yet.
219
+ const lastMessage = messages[messages.length - 1];
220
+ if (!lastMessage || lastMessage.role !== "user") return noopResult;
221
+ const hasUserContent = lastMessage.content.some(
222
+ (block) => block.type === "text" && block.text.trim().length > 0,
223
+ );
224
+ if (!hasUserContent && this.initialized && !this.needsReload)
225
+ return noopResult;
226
+
227
+ try {
228
+ // Decide which retrieval mode to use
229
+ if (!this.initialized || this.needsReload) {
230
+ const recentSummaries = this.fetchRecentSummaries();
231
+
232
+ // Extract the first user message as an additional retrieval signal
233
+ // so context-load biases toward what the user is asking about
234
+ const firstUserText = extractUserText(lastMessage);
235
+ if (firstUserText) {
236
+ recentSummaries.unshift(firstUserText);
237
+ }
238
+
239
+ return await this.runContextLoad(
240
+ messages,
241
+ config,
242
+ recentSummaries,
243
+ abortSignal,
244
+ onEvent,
245
+ );
246
+ }
247
+
248
+ if (this.turnCount % REFRESH_INTERVAL_TURNS === 0) {
249
+ return await this.runRefresh(messages, config, abortSignal);
250
+ }
251
+
252
+ return await this.runPerTurn(messages, config, abortSignal);
253
+ } catch (err) {
254
+ log.warn(
255
+ { err: err instanceof Error ? err.message : String(err) },
256
+ "Memory retrieval failed (non-fatal)",
257
+ );
258
+ return noopResult;
259
+ }
260
+ }
261
+
262
+ // ---------------------------------------------------------------------------
263
+ // Retrieval modes
264
+ // ---------------------------------------------------------------------------
265
+
266
+ private async runContextLoad(
267
+ messages: Message[],
268
+ config: AssistantConfig,
269
+ recentSummaries: string[],
270
+ signal: AbortSignal,
271
+ onEvent: (msg: ServerMessage) => void,
272
+ ) {
273
+ const result = await loadContextMemory({
274
+ scopeId: this.scopeId,
275
+ recentSummaries,
276
+ config,
277
+ signal,
278
+ });
279
+
280
+ this.initialized = true;
281
+ this.needsReload = false;
282
+
283
+ if (result.nodes.length === 0) {
284
+ this.lastInjectedBlock = null;
285
+ this.lastInjectedNodeIds = [];
286
+ this.lastInjectedImages = new Map();
287
+ return {
288
+ runMessages: messages,
289
+ injectedTokens: 0,
290
+ latencyMs: result.latencyMs,
291
+ mode: "context-load" as const,
292
+ injectedBlockText: null,
293
+ };
294
+ }
295
+
296
+ // Track loaded nodes (including serendipity)
297
+ this.tracker.add(result.nodes.map((n) => n.node.id));
298
+ this.tracker.add(result.serendipityNodes.map((n) => n.node.id));
299
+
300
+ // Assemble context block
301
+ const contextBlock = assembleContextBlock(result.nodes, {
302
+ serendipityNodes: result.serendipityNodes,
303
+ });
304
+ if (!contextBlock) {
305
+ return {
306
+ runMessages: messages,
307
+ injectedTokens: 0,
308
+ latencyMs: result.latencyMs,
309
+ mode: "context-load" as const,
310
+ injectedBlockText: null,
311
+ };
312
+ }
313
+
314
+ // Resolve images from scored nodes
315
+ const images = await resolveInjectionImages(
316
+ [...result.nodes, ...result.serendipityNodes],
317
+ MAX_CONTEXT_LOAD_IMAGES,
318
+ );
319
+
320
+ const injectedTokens =
321
+ estimateTextTokens(contextBlock) + images.size * ESTIMATED_IMAGE_TOKENS;
322
+
323
+ onEvent({
324
+ type: "memory_status",
325
+ enabled: true,
326
+ degraded: false,
327
+ } as ServerMessage);
328
+
329
+ this.lastInjectedBlock = contextBlock;
330
+ this.lastInjectedNodeIds = [
331
+ ...result.nodes.map((n) => n.node.id),
332
+ ...result.serendipityNodes.map((n) => n.node.id),
333
+ ];
334
+ this.lastInjectedImages = images;
335
+
336
+ return {
337
+ runMessages: injectMemoryBlock(messages, contextBlock, images),
338
+ injectedTokens,
339
+ latencyMs: result.latencyMs,
340
+ mode: "context-load" as const,
341
+ 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,
416
+ };
417
+ }
418
+
419
+ private async runPerTurn(
420
+ messages: Message[],
421
+ config: AssistantConfig,
422
+ signal: AbortSignal,
423
+ ) {
424
+ // Extract last assistant and user messages as text
425
+ let assistantLast = "";
426
+ let userLast = "";
427
+ let userLastBlocks: ContentBlock[] = [];
428
+
429
+ for (let i = messages.length - 1; i >= 0; i--) {
430
+ const msg = messages[i];
431
+ const text = msg.content
432
+ .filter(
433
+ (b): b is Extract<typeof b, { type: "text" }> => b.type === "text",
434
+ )
435
+ .map((b) => b.text)
436
+ .join(" ");
437
+
438
+ if (msg.role === "user") {
439
+ if (userLastBlocks.length === 0) {
440
+ userLastBlocks = msg.content;
441
+ userLast = text;
442
+ }
443
+ } else if (msg.role === "assistant" && !assistantLast) {
444
+ assistantLast = text;
445
+ }
446
+ if (userLastBlocks.length > 0 && assistantLast) break;
447
+ }
448
+
449
+ const result = await retrieveForTurn({
450
+ assistantLastMessage: assistantLast,
451
+ userLastMessage: userLast,
452
+ userLastMessageBlocks: userLastBlocks,
453
+ scopeId: this.scopeId,
454
+ config,
455
+ tracker: this.tracker,
456
+ signal,
457
+ });
458
+
459
+ if (result.nodes.length === 0) {
460
+ this.lastInjectedBlock = null;
461
+ this.lastInjectedNodeIds = [];
462
+ this.lastInjectedImages = new Map();
463
+ return {
464
+ runMessages: messages,
465
+ injectedTokens: 0,
466
+ latencyMs: result.latencyMs,
467
+ mode: "per-turn" as const,
468
+ injectedBlockText: null,
469
+ };
470
+ }
471
+
472
+ // Track new nodes
473
+ this.tracker.add(result.nodes.map((n) => n.node.id));
474
+
475
+ const injectionBlock = assembleInjectionBlock(result.nodes);
476
+ if (!injectionBlock) {
477
+ return {
478
+ runMessages: messages,
479
+ injectedTokens: 0,
480
+ latencyMs: result.latencyMs,
481
+ mode: "per-turn" as const,
482
+ injectedBlockText: null,
483
+ };
484
+ }
485
+
486
+ // Resolve images from scored nodes
487
+ const images = await resolveInjectionImages(
488
+ result.nodes,
489
+ MAX_PER_TURN_IMAGES,
490
+ );
491
+
492
+ this.lastInjectedBlock = injectionBlock;
493
+ this.lastInjectedNodeIds = result.nodes.map((n) => n.node.id);
494
+ this.lastInjectedImages = images;
495
+
496
+ return {
497
+ runMessages: injectMemoryBlock(messages, injectionBlock, images),
498
+ injectedTokens:
499
+ estimateTextTokens(injectionBlock) +
500
+ images.size * ESTIMATED_IMAGE_TOKENS,
501
+ latencyMs: result.latencyMs,
502
+ mode: "per-turn" as const,
503
+ injectedBlockText: injectionBlock,
504
+ };
505
+ }
506
+ }
507
+
508
+ // ---------------------------------------------------------------------------
509
+ // Injection helper — same pattern as old injectMemoryRecallAsUserBlock
510
+ // ---------------------------------------------------------------------------
511
+
512
+ /**
513
+ * Remove all memory-injected blocks from the last user message.
514
+ *
515
+ * `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
+ *
519
+ * We strip all leading blocks that match this pattern so that
520
+ * `reinjectCachedMemory` is idempotent — no duplicate images after compaction.
521
+ */
522
+ function stripExistingMemoryInjections(messages: Message[]): Message[] {
523
+ if (messages.length === 0) return messages;
524
+ const last = messages[messages.length - 1];
525
+ if (!last || last.role !== "user") return messages;
526
+
527
+ // Walk from the front and skip all memory-injected blocks.
528
+ // The injection prefix is always contiguous at the start of content.
529
+ let firstNonMemory = 0;
530
+ const content = last.content;
531
+ while (firstNonMemory < content.length) {
532
+ const block = content[firstNonMemory];
533
+ if (
534
+ block.type === "text" &&
535
+ block.text.startsWith("<memory __injected>\n")
536
+ ) {
537
+ firstNonMemory++;
538
+ } else if (
539
+ block.type === "text" &&
540
+ block.text.startsWith("<memory_image>")
541
+ ) {
542
+ firstNonMemory++;
543
+ } else if (block.type === "image") {
544
+ firstNonMemory++;
545
+ } else {
546
+ break;
547
+ }
548
+ }
549
+
550
+ // Nothing to strip
551
+ if (firstNonMemory === 0) return messages;
552
+
553
+ return [
554
+ ...messages.slice(0, -1),
555
+ { ...last, content: content.slice(firstNonMemory) },
556
+ ];
557
+ }
558
+
559
+ function injectTextBlock(messages: Message[], text: string): Message[] {
560
+ if (text.trim().length === 0) return messages;
561
+ if (messages.length === 0) return messages;
562
+ // Strip existing memory blocks from the last user message first to prevent
563
+ // duplicates when the message was loaded from DB with a persisted block.
564
+ const cleaned = stripExistingMemoryInjections(messages);
565
+ const userTail = cleaned[cleaned.length - 1];
566
+ if (!userTail || userTail.role !== "user") return messages;
567
+ return [
568
+ ...cleaned.slice(0, -1),
569
+ {
570
+ ...userTail,
571
+ content: [
572
+ {
573
+ type: "text" as const,
574
+ text: `<memory __injected>\n${text}\n</memory>`,
575
+ },
576
+ ...userTail.content,
577
+ ],
578
+ },
579
+ ];
580
+ }
581
+
582
+ function injectMemoryBlock(
583
+ messages: Message[],
584
+ text: string,
585
+ images: Map<string, ResolvedImage>,
586
+ ): Message[] {
587
+ if (text.trim().length === 0 && images.size === 0) return messages;
588
+ if (messages.length === 0) return messages;
589
+ // Strip existing memory blocks from the last user message first to prevent
590
+ // duplicates when the message was loaded from DB with a persisted block.
591
+ const cleaned = stripExistingMemoryInjections(messages);
592
+ const userTail = cleaned[cleaned.length - 1];
593
+ if (!userTail || userTail.role !== "user") return messages;
594
+
595
+ const blocks: ContentBlock[] = [
596
+ { type: "text" as const, text: `<memory __injected>\n${text}\n</memory>` },
597
+ ];
598
+
599
+ for (const [_nodeId, img] of images) {
600
+ blocks.push({
601
+ type: "text" as const,
602
+ text: `<memory_image>${img.description}</memory_image>`,
603
+ });
604
+ blocks.push({
605
+ type: "image" as const,
606
+ source: {
607
+ type: "base64" as const,
608
+ media_type: img.mediaType,
609
+ data: img.base64Data,
610
+ },
611
+ } as ImageContent);
612
+ }
613
+
614
+ return [
615
+ ...cleaned.slice(0, -1),
616
+ { ...userTail, content: [...blocks, ...userTail.content] },
617
+ ];
618
+ }
619
+
620
+ /** Extract text content from a user message. */
621
+ function extractUserText(message: Message): string | null {
622
+ const texts = message.content
623
+ .filter((b): b is Extract<typeof b, { type: "text" }> => b.type === "text")
624
+ .map((b) => b.text.trim())
625
+ .filter((t) => t.length > 0);
626
+ if (texts.length === 0) return null;
627
+ const joined = texts.join(" ");
628
+ // Skip very short messages ("hi", "yes") — they produce vague embeddings
629
+ return joined.length > 10 ? joined : null;
630
+ }