@vellumai/assistant 0.7.2 → 0.8.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 (424) hide show
  1. package/ARCHITECTURE.md +45 -29
  2. package/Dockerfile +1 -0
  3. package/__tests__/permissions/gateway-threshold-reader.test.ts +236 -9
  4. package/bun.lock +3 -0
  5. package/docs/architecture/memory.md +5 -2
  6. package/knip.json +1 -0
  7. package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +13 -4
  8. package/node_modules/@vellumai/ipc-server-utils/bun.lock +24 -0
  9. package/node_modules/@vellumai/ipc-server-utils/package.json +18 -0
  10. package/node_modules/@vellumai/ipc-server-utils/src/index.ts +6 -0
  11. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.test.ts +430 -0
  12. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.ts +221 -0
  13. package/node_modules/@vellumai/ipc-server-utils/tsconfig.json +20 -0
  14. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -9
  15. package/node_modules/@vellumai/slack-text/src/index.test.ts +18 -35
  16. package/node_modules/@vellumai/slack-text/src/index.ts +2 -48
  17. package/openapi.yaml +470 -25
  18. package/package.json +3 -1
  19. package/src/__tests__/annotate-risk-options.test.ts +291 -0
  20. package/src/__tests__/app-control-flow.test.ts +21 -11
  21. package/src/__tests__/approval-cascade.test.ts +8 -16
  22. package/src/__tests__/approval-routes-http.test.ts +6 -0
  23. package/src/__tests__/assistant-event-hub.test.ts +48 -0
  24. package/src/__tests__/assistant-event.test.ts +0 -10
  25. package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -7
  26. package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -0
  27. package/src/__tests__/auto-analysis-end-to-end.test.ts +48 -0
  28. package/src/__tests__/background-workers-disk-pressure.test.ts +268 -0
  29. package/src/__tests__/call-constants.test.ts +10 -1
  30. package/src/__tests__/call-controller.test.ts +127 -0
  31. package/src/__tests__/call-conversation-messages.test.ts +8 -2
  32. package/src/__tests__/channel-inbound-disk-pressure.test.ts +537 -0
  33. package/src/__tests__/channel-readiness-service.test.ts +4 -2
  34. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +58 -28
  35. package/src/__tests__/config-loader-backfill.test.ts +379 -0
  36. package/src/__tests__/config-loader-platform-defaults.test.ts +284 -1
  37. package/src/__tests__/config-schema.test.ts +1 -0
  38. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +18 -9
  39. package/src/__tests__/config-watcher.test.ts +140 -69
  40. package/src/__tests__/context-search-agent-runner.test.ts +61 -3
  41. package/src/__tests__/context-search-conversations-source.test.ts +0 -24
  42. package/src/__tests__/context-search-fanout.test.ts +0 -1
  43. package/src/__tests__/context-search-memory-source.test.ts +6 -33
  44. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -2
  45. package/src/__tests__/context-search-pkb-source.test.ts +12 -7
  46. package/src/__tests__/context-search-workspace-source.test.ts +0 -1
  47. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -0
  48. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +223 -0
  49. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
  50. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
  51. package/src/__tests__/conversation-agent-loop.test.ts +457 -8
  52. package/src/__tests__/conversation-confirmation-signals.test.ts +5 -13
  53. package/src/__tests__/conversation-error.test.ts +150 -3
  54. package/src/__tests__/conversation-init.benchmark.test.ts +1 -1
  55. package/src/__tests__/conversation-process-callsite.test.ts +38 -0
  56. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -0
  57. package/src/__tests__/conversation-runtime-assembly.test.ts +74 -0
  58. package/src/__tests__/conversation-slash-unknown.test.ts +1 -0
  59. package/src/__tests__/conversation-speed-override.test.ts +0 -3
  60. package/src/__tests__/conversation-store.test.ts +0 -18
  61. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +170 -9
  62. package/src/__tests__/conversation-surfaces-app-control.test.ts +15 -4
  63. package/src/__tests__/conversation-surfaces-data-persist.test.ts +476 -0
  64. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +61 -5
  65. package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
  66. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  67. package/src/__tests__/credentials-cli.test.ts +7 -0
  68. package/src/__tests__/cu-unified-flow.test.ts +176 -10
  69. package/src/__tests__/date-context.test.ts +164 -2
  70. package/src/__tests__/disk-pressure-guard.test.ts +262 -0
  71. package/src/__tests__/disk-pressure-lifecycle.test.ts +168 -0
  72. package/src/__tests__/disk-pressure-policy.test.ts +241 -0
  73. package/src/__tests__/disk-pressure-routes.test.ts +379 -0
  74. package/src/__tests__/disk-pressure-tools.test.ts +277 -0
  75. package/src/__tests__/disk-usage.test.ts +150 -0
  76. package/src/__tests__/events-client-registration.test.ts +52 -0
  77. package/src/__tests__/events-dev-bypass-actor.test.ts +162 -0
  78. package/src/__tests__/file-write-tool.test.ts +4 -10
  79. package/src/__tests__/filing-service.test.ts +2 -20
  80. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +10 -26
  81. package/src/__tests__/heartbeat-disk-pressure.test.ts +183 -0
  82. package/src/__tests__/heartbeat-service.test.ts +260 -11
  83. package/src/__tests__/host-app-control-proxy.test.ts +195 -25
  84. package/src/__tests__/host-bash-proxy.test.ts +227 -34
  85. package/src/__tests__/host-bash-routes.test.ts +178 -13
  86. package/src/__tests__/host-cu-proxy.test.ts +210 -3
  87. package/src/__tests__/host-cu-routes-targeted.test.ts +141 -12
  88. package/src/__tests__/host-file-proxy-targeted.test.ts +48 -9
  89. package/src/__tests__/host-file-proxy.test.ts +268 -6
  90. package/src/__tests__/host-file-routes-targeted.test.ts +175 -17
  91. package/src/__tests__/host-transfer-proxy-targeted.test.ts +408 -59
  92. package/src/__tests__/host-transfer-routes-targeted.test.ts +232 -17
  93. package/src/__tests__/http-user-message-parity.test.ts +107 -1
  94. package/src/__tests__/injector-chain.test.ts +36 -16
  95. package/src/__tests__/injector-disk-pressure.test.ts +224 -0
  96. package/src/__tests__/injector-pkb-v2-silenced.test.ts +10 -7
  97. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +154 -67
  98. package/src/__tests__/managed-profile-guard.test.ts +18 -0
  99. package/src/__tests__/mcp-abort-signal.test.ts +130 -0
  100. package/src/__tests__/memory-admin-recall.test.ts +3 -11
  101. package/src/__tests__/memory-retrieval-pipeline.test.ts +22 -1
  102. package/src/__tests__/normalize-onboarding.test.ts +180 -0
  103. package/src/__tests__/notification-decision-fallback.test.ts +91 -0
  104. package/src/__tests__/notification-decision-strategy.test.ts +22 -0
  105. package/src/__tests__/oauth-cli.test.ts +121 -0
  106. package/src/__tests__/oauth-connect-routes.test.ts +316 -0
  107. package/src/__tests__/oauth-provider-seed-logos.test.ts +24 -2
  108. package/src/__tests__/onboarding-persona-write.test.ts +308 -0
  109. package/src/__tests__/openai-provider.test.ts +45 -8
  110. package/src/__tests__/persist-onboarding-artifacts.test.ts +44 -64
  111. package/src/__tests__/platform-callback-registration.test.ts +21 -4
  112. package/src/__tests__/platform.test.ts +2 -1
  113. package/src/__tests__/playbook-execution.test.ts +0 -43
  114. package/src/__tests__/plugin-tool-contribution.test.ts +47 -0
  115. package/src/__tests__/prechat-onboarding-contract.test.ts +214 -27
  116. package/src/__tests__/provider-tool-name.test.ts +23 -0
  117. package/src/__tests__/relay-server.test.ts +60 -5
  118. package/src/__tests__/runtime-events-sse.test.ts +4 -8
  119. package/src/__tests__/scheduler-disk-pressure.test.ts +148 -0
  120. package/src/__tests__/secret-ingress-http.test.ts +0 -1
  121. package/src/__tests__/secret-prompt-log-hygiene.test.ts +7 -5
  122. package/src/__tests__/secret-prompter-channel-fallback.test.ts +7 -5
  123. package/src/__tests__/secret-response-routing.test.ts +7 -5
  124. package/src/__tests__/server-history-render.test.ts +82 -0
  125. package/src/__tests__/skill-include-graph.test.ts +31 -0
  126. package/src/__tests__/skill-load-tool.test.ts +44 -16
  127. package/src/__tests__/skills.test.ts +39 -0
  128. package/src/__tests__/suggestion-routes.test.ts +46 -0
  129. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -42
  130. package/src/__tests__/tool-executor.test.ts +155 -0
  131. package/src/__tests__/twilio-validation.test.ts +2 -2
  132. package/src/__tests__/voice-session-bridge.test.ts +3 -0
  133. package/src/__tests__/workspace-migration-065-bump-stale-heartbeat-interval.test.ts +122 -0
  134. package/src/__tests__/workspace-migration-066-seed-heartbeat-callsite-cost-default.test.ts +285 -0
  135. package/src/__tests__/workspace-migration-068-release-notes-local-timezone.test.ts +90 -0
  136. package/src/__tests__/workspace-migration-069-seed-onboarding-threads.test.ts +120 -0
  137. package/src/__tests__/workspace-migration-071-remove-safe-storage-release-note.test.ts +206 -0
  138. package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +78 -0
  139. package/src/agent/loop.ts +11 -0
  140. package/src/approvals/guardian-request-resolvers.ts +3 -32
  141. package/src/backup/snapshot-lock.ts +2 -27
  142. package/src/bundler/compiler-tools.ts +3 -2
  143. package/src/calls/call-constants.ts +5 -8
  144. package/src/calls/call-controller.ts +130 -67
  145. package/src/calls/call-conversation-messages.ts +46 -10
  146. package/src/calls/relay-server.ts +7 -1
  147. package/src/calls/voice-session-bridge.ts +1 -1
  148. package/src/cli/commands/__tests__/webhooks.test.ts +0 -4
  149. package/src/cli/commands/bash.ts +35 -108
  150. package/src/cli/commands/contacts.ts +64 -25
  151. package/src/cli/commands/credentials.ts +56 -0
  152. package/src/cli/commands/memory-v2.ts +11 -10
  153. package/src/cli/commands/oauth/__tests__/connect.test.ts +401 -219
  154. package/src/cli/commands/oauth/connect.ts +124 -40
  155. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -3
  156. package/src/cli/commands/platform/__tests__/connect.test.ts +7 -1
  157. package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
  158. package/src/cli/commands/platform/__tests__/status.test.ts +103 -6
  159. package/src/cli/commands/platform/index.ts +16 -7
  160. package/src/cli/commands/status.ts +57 -0
  161. package/src/cli/program.ts +4 -2
  162. package/src/config/assistant-feature-flags.ts +13 -3
  163. package/src/config/bundled-skills/app-builder/SKILL.md +1 -3
  164. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -3
  165. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +13 -7
  166. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -2
  167. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +2 -2
  168. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +2 -2
  169. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -2
  170. package/src/config/env.ts +0 -8
  171. package/src/config/feature-flag-registry.json +13 -5
  172. package/src/config/loader.ts +199 -27
  173. package/src/config/schemas/__tests__/memory-v2.test.ts +10 -5
  174. package/src/config/schemas/call-site-catalog.ts +14 -0
  175. package/src/config/schemas/channels.ts +0 -5
  176. package/src/config/schemas/heartbeat.ts +1 -1
  177. package/src/config/schemas/llm.ts +2 -0
  178. package/src/config/schemas/memory-lifecycle.ts +13 -0
  179. package/src/config/schemas/memory-v2.ts +76 -12
  180. package/src/config/schemas/platform.ts +43 -3
  181. package/src/config/schemas/services.ts +28 -0
  182. package/src/config/seed-inference-profiles.ts +230 -33
  183. package/src/contacts/contact-store.ts +0 -25
  184. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +32 -0
  185. package/src/daemon/__tests__/conversation-tool-setup.test.ts +86 -25
  186. package/src/daemon/assistant-attachments.ts +4 -4
  187. package/src/daemon/config-watcher.ts +85 -57
  188. package/src/daemon/conversation-agent-loop-handlers.ts +38 -0
  189. package/src/daemon/conversation-agent-loop.ts +183 -43
  190. package/src/daemon/conversation-error.ts +87 -15
  191. package/src/daemon/conversation-lifecycle.ts +22 -10
  192. package/src/daemon/conversation-process.ts +8 -0
  193. package/src/daemon/conversation-runtime-assembly.ts +26 -0
  194. package/src/daemon/conversation-store.ts +2 -2
  195. package/src/daemon/conversation-surfaces.ts +211 -29
  196. package/src/daemon/conversation-tool-setup.ts +66 -19
  197. package/src/daemon/conversation.ts +18 -23
  198. package/src/daemon/date-context.ts +71 -22
  199. package/src/daemon/disk-pressure-background-gate.ts +73 -0
  200. package/src/daemon/disk-pressure-guard.ts +343 -0
  201. package/src/daemon/disk-pressure-policy.ts +163 -0
  202. package/src/daemon/handlers/shared.ts +26 -1
  203. package/src/daemon/handlers/skills.ts +3 -4
  204. package/src/daemon/host-app-control-proxy.ts +137 -41
  205. package/src/daemon/host-bash-proxy.ts +47 -22
  206. package/src/daemon/host-browser-proxy.ts +1 -1
  207. package/src/daemon/host-cu-proxy.ts +50 -4
  208. package/src/daemon/host-file-proxy.ts +44 -8
  209. package/src/daemon/host-transfer-proxy.ts +97 -6
  210. package/src/daemon/lifecycle.ts +167 -101
  211. package/src/daemon/meet-host-supervisor.ts +4 -4
  212. package/src/daemon/meet-manifest-loader.ts +0 -1
  213. package/src/daemon/memory-v2-startup.ts +66 -15
  214. package/src/daemon/message-protocol.ts +3 -0
  215. package/src/daemon/message-types/conversations.ts +4 -0
  216. package/src/daemon/message-types/disk-pressure.ts +9 -0
  217. package/src/daemon/message-types/messages.ts +22 -1
  218. package/src/daemon/profiler-run-store.ts +5 -5
  219. package/src/daemon/tool-setup-types.ts +2 -2
  220. package/src/documents/document-store.ts +119 -0
  221. package/src/filing/filing-service.ts +29 -5
  222. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +9 -16
  223. package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +36 -0
  224. package/src/heartbeat/heartbeat-run-store.ts +13 -0
  225. package/src/heartbeat/heartbeat-service.ts +205 -31
  226. package/src/home/feed-scheduler.ts +18 -0
  227. package/src/inbound/platform-callback-registration.ts +8 -15
  228. package/src/ipc/__tests__/clients-list-ipc.test.ts +169 -0
  229. package/src/ipc/assistant-server.ts +149 -38
  230. package/src/ipc/gateway-client.ts +37 -3
  231. package/src/ipc/skill-server.ts +99 -42
  232. package/src/live-voice/live-voice-archive.ts +4 -4
  233. package/src/live-voice/protocol.ts +5 -7
  234. package/src/media/image-service.ts +1 -7
  235. package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +21 -13
  236. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +34 -51
  237. package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +0 -6
  238. package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +272 -0
  239. package/src/memory/admin.ts +5 -9
  240. package/src/memory/context-search/agent-runner.ts +19 -2
  241. package/src/memory/context-search/sources/conversations.ts +2 -11
  242. package/src/memory/context-search/sources/memory-v2.ts +1 -16
  243. package/src/memory/context-search/sources/memory.ts +2 -3
  244. package/src/memory/context-search/sources/pkb.ts +2 -3
  245. package/src/memory/context-search/types.ts +0 -1
  246. package/src/memory/conversation-crud.ts +4 -12
  247. package/src/memory/db-init.ts +2 -0
  248. package/src/memory/embedding-runtime-manager.ts +119 -5
  249. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +136 -82
  250. package/src/memory/graph/__tests__/handle-remember-v2.test.ts +11 -26
  251. package/src/memory/graph/conversation-graph-memory.ts +72 -61
  252. package/src/memory/graph/extraction.ts +1 -3
  253. package/src/memory/graph/graph-search.test.ts +11 -67
  254. package/src/memory/graph/graph-search.ts +4 -24
  255. package/src/memory/graph/retriever.test.ts +12 -1
  256. package/src/memory/graph/retriever.ts +10 -15
  257. package/src/memory/graph/tool-handlers.ts +3 -4
  258. package/src/memory/graph/tools.ts +4 -4
  259. package/src/memory/indexer.ts +53 -45
  260. package/src/memory/job-handlers/backfill.ts +2 -11
  261. package/src/memory/job-handlers/cleanup.ts +43 -0
  262. package/src/memory/job-handlers/embedding.ts +6 -8
  263. package/src/memory/job-handlers/summarization.ts +2 -7
  264. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +116 -0
  265. package/src/memory/jobs/embed-concept-page.ts +223 -87
  266. package/src/memory/jobs-store.ts +48 -0
  267. package/src/memory/jobs-worker.ts +85 -43
  268. package/src/memory/memory-v2-activation-log-store.ts +32 -14
  269. package/src/memory/memory-v2-concept-frequency.ts +169 -0
  270. package/src/memory/migrations/239-trace-events-created-at-index.ts +18 -0
  271. package/src/memory/migrations/index.ts +1 -0
  272. package/src/memory/pkb/pkb-search.test.ts +7 -0
  273. package/src/memory/pkb/pkb-search.ts +4 -5
  274. package/src/memory/qdrant-client.ts +3 -13
  275. package/src/memory/rerank-local.ts +374 -0
  276. package/src/memory/search/semantic.ts +10 -72
  277. package/src/memory/trace-event-store.ts +1 -17
  278. package/src/memory/v2/__tests__/activation.test.ts +346 -255
  279. package/src/memory/v2/__tests__/consolidation-job.test.ts +61 -40
  280. package/src/memory/v2/__tests__/injection.test.ts +297 -190
  281. package/src/memory/v2/__tests__/prompts-consolidation.test.ts +61 -2
  282. package/src/memory/v2/__tests__/qdrant.test.ts +326 -9
  283. package/src/memory/v2/__tests__/reranker.test.ts +338 -0
  284. package/src/memory/v2/__tests__/sim.test.ts +113 -196
  285. package/src/memory/v2/__tests__/skill-store.test.ts +71 -65
  286. package/src/memory/v2/__tests__/static-context.test.ts +77 -14
  287. package/src/memory/v2/__tests__/sweep-job.test.ts +19 -33
  288. package/src/memory/v2/activation.ts +149 -156
  289. package/src/memory/v2/consolidation-job.ts +69 -20
  290. package/src/memory/v2/injection.ts +75 -68
  291. package/src/memory/v2/page-store.ts +39 -0
  292. package/src/memory/v2/prompts/consolidation.ts +41 -1
  293. package/src/memory/v2/qdrant.ts +306 -46
  294. package/src/memory/v2/reranker.ts +177 -0
  295. package/src/memory/v2/sim.ts +77 -110
  296. package/src/memory/v2/skill-content.ts +4 -3
  297. package/src/memory/v2/skill-store.ts +82 -59
  298. package/src/memory/v2/static-context.ts +26 -8
  299. package/src/memory/v2/sweep-job.ts +5 -6
  300. package/src/memory/v2/types.ts +17 -10
  301. package/src/notifications/copy-composer.ts +47 -0
  302. package/src/notifications/decision-engine.ts +46 -0
  303. package/src/notifications/signal.ts +4 -0
  304. package/src/oauth/AGENTS.md +3 -1
  305. package/src/oauth/__tests__/oauth-connect-state.test.ts +137 -0
  306. package/src/oauth/connect-orchestrator.ts +2 -0
  307. package/src/oauth/connection-resolver.test.ts +66 -1
  308. package/src/oauth/connection-resolver.ts +55 -1
  309. package/src/oauth/oauth-connect-state.ts +77 -0
  310. package/src/oauth/seed-providers.ts +58 -1
  311. package/src/permissions/gateway-threshold-reader.ts +116 -8
  312. package/src/permissions/prompter.ts +86 -96
  313. package/src/permissions/secret-prompter.ts +31 -31
  314. package/src/plugins/defaults/injectors.ts +36 -4
  315. package/src/plugins/defaults/memory-retrieval.ts +5 -6
  316. package/src/plugins/types.ts +7 -0
  317. package/src/proactive-artifact/aux-message-injector.ts +74 -0
  318. package/src/proactive-artifact/decision.test.ts +226 -0
  319. package/src/proactive-artifact/decision.ts +165 -0
  320. package/src/proactive-artifact/index.ts +7 -0
  321. package/src/proactive-artifact/job.test.ts +914 -0
  322. package/src/proactive-artifact/job.ts +366 -0
  323. package/src/proactive-artifact/message-copy.ts +58 -0
  324. package/src/proactive-artifact/trigger-state.test.ts +277 -0
  325. package/src/proactive-artifact/trigger-state.ts +119 -0
  326. package/src/prompts/normalize-onboarding.ts +80 -0
  327. package/src/prompts/persona-resolver.ts +101 -9
  328. package/src/prompts/system-prompt.ts +21 -7
  329. package/src/prompts/templates/BOOTSTRAP.md +13 -5
  330. package/src/prompts/templates/SOUL.md +13 -28
  331. package/src/providers/__tests__/retry-callsite.test.ts +222 -1
  332. package/src/providers/model-intents.ts +7 -0
  333. package/src/providers/openrouter/client.ts +8 -0
  334. package/src/providers/retry.ts +50 -0
  335. package/src/providers/types.ts +1 -0
  336. package/src/runtime/__tests__/agent-wake.test.ts +456 -3
  337. package/src/runtime/agent-wake.ts +238 -100
  338. package/src/runtime/assistant-event-hub.ts +36 -6
  339. package/src/runtime/assistant-event.ts +0 -1
  340. package/src/runtime/auth/__tests__/route-policy.test.ts +64 -0
  341. package/src/runtime/auth/route-policy.ts +15 -1
  342. package/src/runtime/auth/same-actor.ts +216 -0
  343. package/src/runtime/channel-approvals.ts +3 -2
  344. package/src/runtime/channel-retry-sweep.ts +65 -1
  345. package/src/runtime/local-actor-identity.ts +52 -11
  346. package/src/runtime/pending-interactions.ts +27 -15
  347. package/src/runtime/routes/__tests__/client-routes.test.ts +155 -0
  348. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +0 -5
  349. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
  350. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +147 -0
  351. package/src/runtime/routes/approval-routes.ts +7 -3
  352. package/src/runtime/routes/client-routes.ts +20 -2
  353. package/src/runtime/routes/consolidation-routes.ts +8 -9
  354. package/src/runtime/routes/contact-routes.ts +0 -25
  355. package/src/runtime/routes/conversation-query-routes.ts +44 -1
  356. package/src/runtime/routes/conversation-routes.ts +35 -26
  357. package/src/runtime/routes/debug-bash-routes.ts +165 -0
  358. package/src/runtime/routes/disk-pressure-routes.ts +121 -0
  359. package/src/runtime/routes/document-pdf-renderer.ts +6 -2
  360. package/src/runtime/routes/documents-routes.ts +2 -75
  361. package/src/runtime/routes/events-routes.ts +41 -9
  362. package/src/runtime/routes/filing-routes.ts +2 -3
  363. package/src/runtime/routes/host-bash-routes.ts +23 -3
  364. package/src/runtime/routes/host-cu-routes.ts +33 -6
  365. package/src/runtime/routes/host-file-routes.ts +32 -6
  366. package/src/runtime/routes/host-transfer-routes.ts +79 -16
  367. package/src/runtime/routes/identity-routes.ts +7 -138
  368. package/src/runtime/routes/inbound-message-handler.ts +77 -12
  369. package/src/runtime/routes/index.ts +6 -0
  370. package/src/runtime/routes/memory-item-routes.test.ts +37 -17
  371. package/src/runtime/routes/memory-item-routes.ts +5 -6
  372. package/src/runtime/routes/memory-v2-routes.ts +136 -17
  373. package/src/runtime/routes/oauth-connect-routes.ts +153 -0
  374. package/src/runtime/verification-outbound-actions.ts +4 -4
  375. package/src/schedule/run-script.ts +37 -5
  376. package/src/schedule/scheduler.ts +20 -1
  377. package/src/security/encrypted-store.ts +2 -0
  378. package/src/security/secure-keys.ts +55 -0
  379. package/src/skills/include-graph.ts +35 -13
  380. package/src/skills/remote-skill-policy.ts +4 -10
  381. package/src/subagent/index.ts +1 -7
  382. package/src/subagent/manager.ts +1 -15
  383. package/src/tasks/task-runner.ts +0 -1
  384. package/src/tasks/task-store.ts +0 -3
  385. package/src/tools/background-tool-registry.ts +17 -3
  386. package/src/tools/document/document-tool.ts +20 -0
  387. package/src/tools/executor.ts +18 -2
  388. package/src/tools/host-filesystem/edit.test.ts +151 -0
  389. package/src/tools/host-filesystem/edit.ts +43 -1
  390. package/src/tools/host-filesystem/read.test.ts +129 -0
  391. package/src/tools/host-filesystem/read.ts +43 -1
  392. package/src/tools/host-filesystem/transfer.test.ts +127 -2
  393. package/src/tools/host-filesystem/transfer.ts +56 -11
  394. package/src/tools/host-filesystem/write.test.ts +134 -0
  395. package/src/tools/host-filesystem/write.ts +43 -1
  396. package/src/tools/host-terminal/host-shell.ts +13 -6
  397. package/src/tools/mcp/mcp-tool-factory.ts +2 -1
  398. package/src/tools/memory/register.test.ts +14 -9
  399. package/src/tools/memory/register.ts +1 -2
  400. package/src/tools/permission-checker.ts +15 -0
  401. package/src/tools/provider-tool-name.ts +28 -0
  402. package/src/tools/registry.ts +30 -9
  403. package/src/tools/skills/load.ts +24 -20
  404. package/src/tools/terminal/shell.ts +9 -1
  405. package/src/tools/tool-approval-handler.ts +31 -6
  406. package/src/tools/tool-name-aliases.ts +19 -0
  407. package/src/tools/types.ts +43 -3
  408. package/src/tts/provider-catalog.ts +3 -5
  409. package/src/util/disk-usage.ts +138 -0
  410. package/src/util/platform.ts +21 -11
  411. package/src/util/process-liveness.ts +26 -0
  412. package/src/workspace/heartbeat-service.ts +19 -0
  413. package/src/workspace/migrations/065-bump-stale-heartbeat-interval.ts +60 -0
  414. package/src/workspace/migrations/066-seed-heartbeat-callsite-cost-default.ts +146 -0
  415. package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +14 -0
  416. package/src/workspace/migrations/068-release-notes-local-timezone.ts +65 -0
  417. package/src/workspace/migrations/069-seed-onboarding-threads.ts +28 -0
  418. package/src/workspace/migrations/070-memory-v2-summary-schema-rebuild.ts +31 -0
  419. package/src/workspace/migrations/071-remove-safe-storage-release-note.ts +111 -0
  420. package/src/workspace/migrations/registry.ts +14 -0
  421. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +0 -167
  422. package/src/memory/v2/__tests__/skill-qdrant.test.ts +0 -657
  423. package/src/memory/v2/skill-qdrant.ts +0 -404
  424. package/src/signals/bash.ts +0 -198
@@ -2,14 +2,13 @@
2
2
  // Memory Tool handlers
3
3
  //
4
4
  // remember: save facts to the PKB (buffer.md + daily archive) under the v1
5
- // path, or to memory/buffer.md + memory/archive/<today>.md when the
6
- // `memory-v2-enabled` feature flag is on.
5
+ // path, or to memory/buffer.md + memory/archive/<today>.md when memory v2 is
6
+ // active.
7
7
  // ---------------------------------------------------------------------------
8
8
 
9
9
  import { appendFileSync, existsSync, mkdirSync } from "node:fs";
10
10
  import { join } from "node:path";
11
11
 
12
- import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
13
12
  import type { AssistantConfig } from "../../config/types.js";
14
13
  import { getLogger } from "../../util/logger.js";
15
14
  import { getWorkspaceDir } from "../../util/platform.js";
@@ -46,7 +45,7 @@ export function handleRemember(
46
45
  const now = new Date();
47
46
  const entry = formatRememberEntry(input.content.trim(), now);
48
47
 
49
- if (isAssistantFeatureFlagEnabled("memory-v2-enabled", config)) {
48
+ if (config.memory.v2.enabled) {
50
49
  appendBufferAndArchive({
51
50
  rootDir: join(workspaceDir, "memory"),
52
51
  entry,
@@ -56,10 +56,10 @@ export const graphRecallDefinition: ToolDefinition = {
56
56
  /**
57
57
  * Save a fact to the assistant's knowledge base. The fact is appended to
58
58
  * `buffer.md` (immediately available in the next conversation) and the daily
59
- * archive (permanent date-indexed record). With the `memory-v2-enabled`
60
- * feature flag on, writes go under `memory/`; otherwise they go under
61
- * `pkb/`. Consolidation of the buffer into longer-form storage runs as a
62
- * separate periodic job in both modes.
59
+ * archive (permanent date-indexed record). When `memory.v2.enabled` is true,
60
+ * writes go under `memory/`; otherwise they go under `pkb/`. Consolidation
61
+ * of the buffer into longer-form storage runs as a separate periodic job in
62
+ * both modes.
63
63
  */
64
64
  export const graphRememberDefinition: ToolDefinition = {
65
65
  name: "remember",
@@ -175,39 +175,9 @@ export async function indexMessageNow(
175
175
  // Summaries still run — they feed the graph retrieval pipeline and
176
176
  // are not recursion-prone.
177
177
  if (!isAutoAnalysisSource) {
178
- // ── Graph extraction ────────────────────────────────────────────
179
- const graphPendingKey = `graph_extract:${input.conversationId}:pending_count`;
180
- const graphCurrentVal = getMemoryCheckpoint(graphPendingKey);
181
- const graphPendingCount =
182
- (graphCurrentVal ? parseInt(graphCurrentVal, 10) : 0) + 1;
183
- setMemoryCheckpoint(graphPendingKey, String(graphPendingCount));
184
-
185
- const graphBatchFired = graphPendingCount >= batchSize;
186
- if (graphBatchFired) {
187
- setMemoryCheckpoint(graphPendingKey, "0");
188
- }
189
-
190
- // Single pending `graph_extract` row per conversation. If the
191
- // batch threshold just fired, pull `runAfter` back to now so the
192
- // job runs immediately; otherwise debounce by the idle timeout.
193
- // Routing both paths through `upsertDebouncedJob` ensures the
194
- // row's `runAfter` reflects whichever trigger ran last, so a
195
- // batch crossing always takes effect immediately.
196
- const extractRunAfter = graphBatchFired
197
- ? Date.now()
198
- : Date.now() + idleTimeoutMs;
199
- upsertDebouncedJob(
200
- "graph_extract",
201
- {
202
- conversationId: input.conversationId,
203
- scopeId: input.scopeId ?? "default",
204
- },
205
- extractRunAfter,
206
- );
207
-
208
- // Reading config here is best-effort: feature-gated triggers below
209
- // (memory v2 sweep, auto-analyze batch) skip when it fails — the
210
- // idle-debounced enqueues above are unaffected.
178
+ // Reading config here is best-effort: when it fails we treat v2 as
179
+ // inactive (failing-open to v1) so a config error never silently
180
+ // drops both extraction paths.
211
181
  let triggerConfig: ReturnType<typeof getConfig> | null = null;
212
182
  try {
213
183
  triggerConfig = getConfig();
@@ -218,20 +188,58 @@ export async function indexMessageNow(
218
188
  );
219
189
  }
220
190
 
221
- // Memory v2 sweep mirrors graph_extract's debounce: when the v2
222
- // flag + config are on AND `sweep_enabled` is set, every extraction
223
- // trigger also enqueues a sweep. The sweep itself reads recent
224
- // messages globally, so the `conversationId` here is just the dedup
225
- // key — one pending row per active conversation. All three gates
226
- // (feature flag, v2 master toggle, sweep_enabled) must be true.
191
+ const v2Config =
192
+ triggerConfig != null && triggerConfig.memory.v2.enabled
193
+ ? triggerConfig
194
+ : null;
195
+
196
+ // ── Graph extraction (v1) ───────────────────────────────────────
197
+ // Suppressed when v2 is active — v2 reads memory from buffer.md
198
+ // and concept pages, so the v1 graph would be stale data nobody
199
+ // consumes. Pending-count tracking is suppressed too; otherwise a
200
+ // flag flip back to v1 would fire an immediate batch from counts
201
+ // accumulated during the v2 window.
202
+ let extractRunAfter: number;
203
+ if (v2Config == null) {
204
+ const graphPendingKey = `graph_extract:${input.conversationId}:pending_count`;
205
+ const graphCurrentVal = getMemoryCheckpoint(graphPendingKey);
206
+ const graphPendingCount =
207
+ (graphCurrentVal ? parseInt(graphCurrentVal, 10) : 0) + 1;
208
+ setMemoryCheckpoint(graphPendingKey, String(graphPendingCount));
209
+
210
+ const graphBatchFired = graphPendingCount >= batchSize;
211
+ if (graphBatchFired) {
212
+ setMemoryCheckpoint(graphPendingKey, "0");
213
+ }
214
+
215
+ // Single pending `graph_extract` row per conversation. If the
216
+ // batch threshold just fired, pull `runAfter` back to now so the
217
+ // job runs immediately; otherwise debounce by the idle timeout.
218
+ // Routing both paths through `upsertDebouncedJob` ensures the
219
+ // row's `runAfter` reflects whichever trigger ran last, so a
220
+ // batch crossing always takes effect immediately.
221
+ extractRunAfter = graphBatchFired
222
+ ? Date.now()
223
+ : Date.now() + idleTimeoutMs;
224
+ upsertDebouncedJob(
225
+ "graph_extract",
226
+ {
227
+ conversationId: input.conversationId,
228
+ scopeId: input.scopeId ?? "default",
229
+ },
230
+ extractRunAfter,
231
+ );
232
+ } else {
233
+ extractRunAfter = Date.now() + idleTimeoutMs;
234
+ }
235
+
236
+ // Memory v2 sweep: when v2 is on AND `sweep_enabled` is set, every
237
+ // extraction trigger also enqueues a sweep. The sweep itself reads
238
+ // recent messages globally, so the `conversationId` here is just
239
+ // the dedup key — one pending row per active conversation.
227
240
  // `sweep_enabled` defaults to false because `remember()` is the
228
241
  // primary capture path; the sweep is opt-in.
229
- if (
230
- triggerConfig != null &&
231
- isAssistantFeatureFlagEnabled("memory-v2-enabled", triggerConfig) &&
232
- triggerConfig.memory.v2.enabled &&
233
- triggerConfig.memory.v2.sweep_enabled
234
- ) {
242
+ if (v2Config != null && v2Config.memory.v2.sweep_enabled) {
235
243
  upsertDebouncedJob(
236
244
  "memory_v2_sweep",
237
245
  { conversationId: input.conversationId },
@@ -7,10 +7,7 @@ import {
7
7
  resetMessageCursorCheckpoint,
8
8
  writeMessageCursorCheckpoint,
9
9
  } from "../checkpoints.js";
10
- import {
11
- getConversationMemoryScopeId,
12
- messageMetadataSchema,
13
- } from "../conversation-crud.js";
10
+ import { messageMetadataSchema } from "../conversation-crud.js";
14
11
  import { getDb } from "../db-connection.js";
15
12
  import { indexMessageNow } from "../indexer.js";
16
13
  import { enqueueMemoryJob, type MemoryJob } from "../jobs-store.js";
@@ -72,13 +69,7 @@ export async function backfillJob(
72
69
  .all();
73
70
 
74
71
  if (batch.length > 0) {
75
- const scopeCache = new Map<string, string>();
76
72
  for (const message of batch) {
77
- let scopeId = scopeCache.get(message.conversationId);
78
- if (scopeId === undefined) {
79
- scopeId = getConversationMemoryScopeId(message.conversationId);
80
- scopeCache.set(message.conversationId, scopeId);
81
- }
82
73
  const { provenanceTrustClass, automated } = parseMessageMetadata(
83
74
  message.metadata ?? null,
84
75
  );
@@ -89,7 +80,7 @@ export async function backfillJob(
89
80
  role: message.role,
90
81
  content: message.content,
91
82
  createdAt: message.createdAt,
92
- scopeId,
83
+ scopeId: "default",
93
84
  provenanceTrustClass,
94
85
  automated,
95
86
  },
@@ -54,6 +54,49 @@ export function pruneOldLlmRequestLogsJob(
54
54
  );
55
55
  }
56
56
 
57
+ /**
58
+ * Delete trace events older than the configured retention period.
59
+ * Processes in batches to avoid long DB locks and excessive WAL growth.
60
+ * Re-enqueues itself if more rows remain.
61
+ */
62
+ export function pruneOldTraceEventsJob(
63
+ job: MemoryJob,
64
+ config: AssistantConfig,
65
+ ): void {
66
+ const rawRetention = job.payload.retentionDays;
67
+ const retentionDays =
68
+ typeof rawRetention === "number" &&
69
+ Number.isFinite(rawRetention) &&
70
+ rawRetention >= 0
71
+ ? rawRetention
72
+ : config.memory.cleanup.traceEventRetentionDays;
73
+
74
+ // 0 means disabled
75
+ if (retentionDays === 0) return;
76
+
77
+ const cutoffMs = Date.now() - retentionDays * 86_400_000;
78
+
79
+ rawRun(
80
+ `DELETE FROM trace_events WHERE rowid IN (SELECT rowid FROM trace_events WHERE created_at < ? LIMIT ?)`,
81
+ cutoffMs,
82
+ PRUNE_LOG_BATCH_LIMIT,
83
+ );
84
+ const deleted = rawChanges();
85
+
86
+ if (deleted >= PRUNE_LOG_BATCH_LIMIT) {
87
+ enqueueMemoryJob("prune_old_trace_events", { retentionDays });
88
+ }
89
+
90
+ log.info(
91
+ {
92
+ deleted,
93
+ retentionDays,
94
+ cutoffMs,
95
+ },
96
+ "Pruned old trace events",
97
+ );
98
+ }
99
+
57
100
  /**
58
101
  * Delete conversations that have had no activity (updatedAt) for longer than
59
102
  * the configured retention period. Processes in batches so a single job doesn't
@@ -3,7 +3,6 @@ import { readFile } from "node:fs/promises";
3
3
  import { eq } from "drizzle-orm";
4
4
 
5
5
  import type { AssistantConfig } from "../../config/types.js";
6
- import { getConversationMemoryScopeId } from "../conversation-crud.js";
7
6
  import { getDb } from "../db-connection.js";
8
7
  import type { EmbeddingInput } from "../embedding-types.js";
9
8
  import { asString, embedAndUpsert } from "../job-utils.js";
@@ -18,7 +17,7 @@ import {
18
17
 
19
18
  export async function embedSegmentJob(
20
19
  job: MemoryJob,
21
- config: AssistantConfig
20
+ config: AssistantConfig,
22
21
  ): Promise<void> {
23
22
  const segmentId = asString(job.payload.segmentId);
24
23
  if (!segmentId) return;
@@ -39,7 +38,7 @@ export async function embedSegmentJob(
39
38
 
40
39
  export async function embedSummaryJob(
41
40
  job: MemoryJob,
42
- config: AssistantConfig
41
+ config: AssistantConfig,
43
42
  ): Promise<void> {
44
43
  const summaryId = asString(job.payload.summaryId);
45
44
  if (!summaryId) return;
@@ -60,13 +59,13 @@ export async function embedSummaryJob(
60
59
  created_at: summary.startAt,
61
60
  last_seen_at: summary.endAt,
62
61
  memory_scope_id: summary.scopeId,
63
- }
62
+ },
64
63
  );
65
64
  }
66
65
 
67
66
  export async function embedMediaJob(
68
67
  job: MemoryJob,
69
- config: AssistantConfig
68
+ config: AssistantConfig,
70
69
  ): Promise<void> {
71
70
  const assetId = asString(job.payload.assetId);
72
71
  if (!assetId) return;
@@ -99,7 +98,7 @@ export async function embedMediaJob(
99
98
 
100
99
  export async function embedAttachmentJob(
101
100
  job: MemoryJob,
102
- config: AssistantConfig
101
+ config: AssistantConfig,
103
102
  ): Promise<void> {
104
103
  const messageId = asString(job.payload.messageId);
105
104
  const blockIndex = job.payload.blockIndex as number;
@@ -125,11 +124,10 @@ export async function embedAttachmentJob(
125
124
 
126
125
  // Use messageId + blockIndex as targetId for uniqueness
127
126
  const targetId = `${messageId}:${blockIndex}`;
128
- const memoryScopeId = getConversationMemoryScopeId(message.conversationId);
129
127
  await embedAndUpsert(config, "media", targetId, input, {
130
128
  created_at: message.createdAt,
131
129
  message_id: messageId,
132
130
  conversation_id: message.conversationId,
133
- memory_scope_id: memoryScopeId,
131
+ memory_scope_id: "default",
134
132
  });
135
133
  }
@@ -10,7 +10,6 @@ import {
10
10
  userMessage,
11
11
  } from "../../providers/provider-send-message.js";
12
12
  import { getLogger } from "../../util/logger.js";
13
- import { getConversationMemoryScopeId } from "../conversation-crud.js";
14
13
  import { getDb } from "../db-connection.js";
15
14
  import { asString, truncate } from "../job-utils.js";
16
15
  import { enqueueMemoryJob, type MemoryJob } from "../jobs-store.js";
@@ -91,10 +90,6 @@ export async function buildConversationSummaryJob(
91
90
  "conversation",
92
91
  );
93
92
 
94
- // Inherit the conversation's memory scope so summaries stay aligned with
95
- // the retrieval scope used by the source conversation.
96
- const scopeId = getConversationMemoryScopeId(conversationId);
97
-
98
93
  const now = Date.now();
99
94
  const summaryId = existing?.id ?? uuid();
100
95
  const nextVersion = (existing?.version ?? 0) + 1;
@@ -108,7 +103,7 @@ export async function buildConversationSummaryJob(
108
103
  id: summaryId,
109
104
  scope: "conversation",
110
105
  scopeKey: conversationId,
111
- scopeId,
106
+ scopeId: "default",
112
107
  summary: summaryText,
113
108
  tokenEstimate: estimateTextTokens(summaryText),
114
109
  version: nextVersion,
@@ -123,7 +118,7 @@ export async function buildConversationSummaryJob(
123
118
  summary: summaryText,
124
119
  tokenEstimate: estimateTextTokens(summaryText),
125
120
  version: sql`${memorySummaries.version} + 1`,
126
- scopeId,
121
+ scopeId: "default",
127
122
  startAt: earliestCovered,
128
123
  endAt: latestCovered,
129
124
  updatedAt: now,
@@ -86,6 +86,10 @@ const upsertCalls: Array<{
86
86
  slug: string;
87
87
  dense: number[];
88
88
  sparse: { indices: number[]; values: number[] };
89
+ summary?: {
90
+ dense: number[];
91
+ sparse: { indices: number[]; values: number[] };
92
+ };
89
93
  updatedAt: number;
90
94
  }> = [];
91
95
 
@@ -96,6 +100,10 @@ mock.module("../../v2/qdrant.js", () => ({
96
100
  slug: string;
97
101
  dense: number[];
98
102
  sparse: { indices: number[]; values: number[] };
103
+ summary?: {
104
+ dense: number[];
105
+ sparse: { indices: number[]; values: number[] };
106
+ };
99
107
  updatedAt: number;
100
108
  }) => {
101
109
  upsertCalls.push(params);
@@ -242,6 +250,114 @@ describe("embedConceptPageJob — happy path", () => {
242
250
  });
243
251
  });
244
252
 
253
+ describe("embedConceptPageJob — summary embedding", () => {
254
+ test("embeds the summary when present and forwards summary vectors to upsert", async () => {
255
+ await writePage(tmpWorkspace, {
256
+ slug: "summarized-page",
257
+ frontmatter: {
258
+ edges: [],
259
+ ref_files: [],
260
+ summary: "A short prose summary that retrieval indexes separately.",
261
+ },
262
+ body: "Long-form body content.\n",
263
+ });
264
+
265
+ await embedConceptPageJob(
266
+ makeJob({ slug: "summarized-page" }),
267
+ TEST_CONFIG,
268
+ );
269
+
270
+ // Body and summary are batched into one backend call (saves a round-trip).
271
+ expect(embedWithBackendCalls).toHaveLength(1);
272
+ expect(embedWithBackendCalls[0].inputs).toHaveLength(2);
273
+ expect(upsertCalls).toHaveLength(1);
274
+ const call = upsertCalls[0];
275
+ expect(call.slug).toBe("summarized-page");
276
+ expect(call.dense).toEqual([0.1, 0.2, 0.3, 0.4]);
277
+ expect(call.sparse).toBeDefined();
278
+ expect(call.summary?.dense).toEqual([0.1, 0.2, 0.3, 0.4]);
279
+ expect(call.summary?.sparse).toBeDefined();
280
+ });
281
+
282
+ test("skips summary embedding when the page has no summary in frontmatter", async () => {
283
+ await writePage(tmpWorkspace, {
284
+ slug: "legacy-page",
285
+ frontmatter: { edges: [], ref_files: [] },
286
+ body: "Body only — no summary in frontmatter.\n",
287
+ });
288
+
289
+ await embedConceptPageJob(makeJob({ slug: "legacy-page" }), TEST_CONFIG);
290
+
291
+ // Only the body was embedded.
292
+ expect(embedWithBackendCalls).toHaveLength(1);
293
+ expect(upsertCalls).toHaveLength(1);
294
+ const call = upsertCalls[0];
295
+ expect(call.summary).toBeUndefined();
296
+ });
297
+
298
+ test("skips summary embedding when the summary is whitespace-only", async () => {
299
+ // Whitespace-only summaries (` `, `\n`) are equivalent to absent — the
300
+ // embedding backend would reject the empty input downstream anyway.
301
+ await writePage(tmpWorkspace, {
302
+ slug: "whitespace-summary",
303
+ frontmatter: {
304
+ edges: [],
305
+ ref_files: [],
306
+ summary: " ",
307
+ },
308
+ body: "Body content.\n",
309
+ });
310
+
311
+ await embedConceptPageJob(
312
+ makeJob({ slug: "whitespace-summary" }),
313
+ TEST_CONFIG,
314
+ );
315
+
316
+ expect(embedWithBackendCalls).toHaveLength(1);
317
+ expect(upsertCalls[0].summary).toBeUndefined();
318
+ });
319
+
320
+ test("body and summary cache rows are independent (summary edit doesn't invalidate body)", async () => {
321
+ // Write a page with a summary, run the job to prime caches.
322
+ await writePage(tmpWorkspace, {
323
+ slug: "cached-summary",
324
+ frontmatter: {
325
+ edges: [],
326
+ ref_files: [],
327
+ summary: "First version of the summary.",
328
+ },
329
+ body: "Stable body that never changes.\n",
330
+ });
331
+ await embedConceptPageJob(
332
+ makeJob({ slug: "cached-summary" }),
333
+ TEST_CONFIG,
334
+ );
335
+ // Body + summary batched into a single backend call on first run.
336
+ expect(embedWithBackendCalls).toHaveLength(1);
337
+ expect(embedWithBackendCalls[0].inputs).toHaveLength(2);
338
+
339
+ // Edit only the summary — body stays identical, only the summary text
340
+ // changes. Re-running the job should hit the body cache (no re-embed)
341
+ // but recompute the summary embedding.
342
+ await writePage(tmpWorkspace, {
343
+ slug: "cached-summary",
344
+ frontmatter: {
345
+ edges: [],
346
+ ref_files: [],
347
+ summary: "Second version of the summary, different wording.",
348
+ },
349
+ body: "Stable body that never changes.\n",
350
+ });
351
+ await embedConceptPageJob(
352
+ makeJob({ slug: "cached-summary" }),
353
+ TEST_CONFIG,
354
+ );
355
+ // One additional backend call with only the summary text — body hit the cache.
356
+ expect(embedWithBackendCalls).toHaveLength(2);
357
+ expect(embedWithBackendCalls[1].inputs).toHaveLength(1);
358
+ });
359
+ });
360
+
245
361
  describe("embedConceptPageJob — cache hit", () => {
246
362
  test("reuses the cached dense vector when content hash matches", async () => {
247
363
  await writePage(tmpWorkspace, {