@vellumai/assistant 0.7.0 → 0.7.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 (666) hide show
  1. package/ARCHITECTURE.md +6 -7
  2. package/Dockerfile +1 -0
  3. package/README.md +2 -2
  4. package/__tests__/permissions/gateway-threshold-reader.test.ts +79 -139
  5. package/bun.lock +3 -0
  6. package/docs/architecture/security.md +18 -16
  7. package/knip.json +1 -0
  8. package/node_modules/@vellumai/skill-host-contracts/__tests__/client.test.ts +1 -5
  9. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -5
  10. package/node_modules/@vellumai/skill-host-contracts/src/client.ts +10 -16
  11. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +1 -9
  12. package/node_modules/@vellumai/skill-host-contracts/src/tool-types.ts +12 -12
  13. package/node_modules/@vellumai/slack-text/bun.lock +24 -0
  14. package/node_modules/@vellumai/slack-text/package.json +18 -0
  15. package/node_modules/@vellumai/slack-text/src/index.test.ts +153 -0
  16. package/node_modules/@vellumai/slack-text/src/index.ts +235 -0
  17. package/node_modules/@vellumai/slack-text/tsconfig.json +20 -0
  18. package/openapi.yaml +294 -107
  19. package/package.json +4 -2
  20. package/scripts/generate-openapi.ts +16 -111
  21. package/src/__tests__/agent-wake-override-profile.test.ts +23 -1
  22. package/src/__tests__/anthropic-provider.test.ts +56 -13
  23. package/src/__tests__/app-conversation-ids-backfill.test.ts +278 -0
  24. package/src/__tests__/app-conversation-ids.test.ts +151 -0
  25. package/src/__tests__/approval-cascade.test.ts +0 -15
  26. package/src/__tests__/approval-routes-http.test.ts +6 -17
  27. package/src/__tests__/assistant-event-hub.test.ts +126 -77
  28. package/src/__tests__/assistant-event.test.ts +0 -5
  29. package/src/__tests__/assistant-events-sse-hardening.test.ts +37 -15
  30. package/src/__tests__/assistant-feature-flags-integration.test.ts +0 -29
  31. package/src/__tests__/background-shell-host-bash.test.ts +34 -43
  32. package/src/__tests__/call-controller.test.ts +1 -1
  33. package/src/__tests__/call-site-routing-provider.test.ts +193 -0
  34. package/src/__tests__/channel-approval-routes.test.ts +10 -296
  35. package/src/__tests__/channel-approvals.test.ts +25 -17
  36. package/src/__tests__/channel-guardian.test.ts +100 -146
  37. package/src/__tests__/checker.test.ts +20 -34
  38. package/src/__tests__/compact-event-conversation-id-guard.test.ts +50 -0
  39. package/src/__tests__/compaction-events.test.ts +2 -0
  40. package/src/__tests__/config-schema.test.ts +6 -48
  41. package/src/__tests__/config-watcher.test.ts +12 -0
  42. package/src/__tests__/connection-policy.test.ts +1 -52
  43. package/src/__tests__/contacts-write.test.ts +2 -64
  44. package/src/__tests__/context-image-dimensions.test.ts +1 -1
  45. package/src/__tests__/context-search-memory-source.test.ts +120 -1
  46. package/src/__tests__/context-search-memory-v2-source.test.ts +383 -0
  47. package/src/__tests__/context-search-pkb-source.test.ts +49 -0
  48. package/src/__tests__/context-search-workspace-source.test.ts +9 -22
  49. package/src/__tests__/context-window-manager.test.ts +46 -0
  50. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
  51. package/src/__tests__/conversation-agent-loop-overflow.test.ts +102 -29
  52. package/src/__tests__/conversation-agent-loop.test.ts +980 -13
  53. package/src/__tests__/conversation-analysis-routes.test.ts +12 -10
  54. package/src/__tests__/conversation-attention-telegram.test.ts +11 -3
  55. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -291
  56. package/src/__tests__/conversation-history-web-search.test.ts +4 -3
  57. package/src/__tests__/conversation-inference-profile-route.test.ts +12 -23
  58. package/src/__tests__/conversation-lifecycle.test.ts +4 -4
  59. package/src/__tests__/conversation-process-callsite.test.ts +79 -2
  60. package/src/__tests__/conversation-queue.test.ts +3 -8
  61. package/src/__tests__/conversation-routes-disk-view.test.ts +1 -161
  62. package/src/__tests__/conversation-routes-guardian-reply.test.ts +0 -32
  63. package/src/__tests__/conversation-routes-slash-commands.test.ts +75 -66
  64. package/src/__tests__/conversation-runtime-assembly.test.ts +257 -3
  65. package/src/__tests__/conversation-slash-commands.test.ts +24 -4
  66. package/src/__tests__/conversation-slash-queue.test.ts +2 -0
  67. package/src/__tests__/conversation-speed-override.test.ts +0 -3
  68. package/src/__tests__/conversation-starter-routes.test.ts +79 -2
  69. package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +12 -5
  70. package/src/__tests__/conversation-surfaces-standalone.test.ts +18 -14
  71. package/src/__tests__/conversation-surfaces-state-update.test.ts +3 -2
  72. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +8 -46
  73. package/src/__tests__/conversation-usage.test.ts +253 -3
  74. package/src/__tests__/credential-execution-shell-lockdown.test.ts +0 -39
  75. package/src/__tests__/credential-health-service.test.ts +68 -0
  76. package/src/__tests__/credential-security-e2e.test.ts +4 -3
  77. package/src/__tests__/credential-security-invariants.test.ts +1 -5
  78. package/src/__tests__/credential-token-resolver.test.ts +180 -0
  79. package/src/__tests__/cu-unified-flow.test.ts +33 -16
  80. package/src/__tests__/daemon-assistant-events.test.ts +34 -21
  81. package/src/__tests__/daemon-credential-client.test.ts +4 -1
  82. package/src/__tests__/db-connection-isolation.test.ts +125 -0
  83. package/src/__tests__/db-migration-rollback.test.ts +101 -0
  84. package/src/__tests__/db-slack-compaction-watermark-migration.test.ts +169 -0
  85. package/src/__tests__/deterministic-verification-control-plane.test.ts +7 -80
  86. package/src/__tests__/document-conversations.test.ts +332 -0
  87. package/src/__tests__/embedding-managed-proxy-selection.test.ts +2 -2
  88. package/src/__tests__/emit-event-signal.test.ts +4 -6
  89. package/src/__tests__/events-client-registration.test.ts +193 -49
  90. package/src/__tests__/filing-service.test.ts +58 -7
  91. package/src/__tests__/first-greeting.test.ts +156 -150
  92. package/src/__tests__/fixtures/mock-chrome-extension.ts +108 -66
  93. package/src/__tests__/get-skill-detail-audit.test.ts +3 -8
  94. package/src/__tests__/guardian-binding-drift-heal.test.ts +1 -1
  95. package/src/__tests__/guardian-dispatch.test.ts +1 -1
  96. package/src/__tests__/guardian-grant-minting.test.ts +7 -2
  97. package/src/__tests__/guardian-routing-invariants.test.ts +7 -2
  98. package/src/__tests__/guardian-routing-state.test.ts +1 -1
  99. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +32 -11
  100. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +2 -83
  101. package/src/__tests__/headless-browser-mode.test.ts +4 -9
  102. package/src/__tests__/headless-browser-navigate.test.ts +21 -20
  103. package/src/__tests__/heartbeat-service.test.ts +289 -7
  104. package/src/__tests__/helpers/channel-test-adapter.ts +2 -2
  105. package/src/__tests__/helpers/create-guardian-binding.ts +91 -0
  106. package/src/__tests__/host-bash-proxy.test.ts +46 -122
  107. package/src/__tests__/host-browser-e2e-cloud.test.ts +36 -497
  108. package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +26 -96
  109. package/src/__tests__/host-browser-proxy.test.ts +111 -185
  110. package/src/__tests__/host-browser-routes.test.ts +45 -75
  111. package/src/__tests__/host-browser-ws-events-e2e.test.ts +26 -30
  112. package/src/__tests__/host-cu-proxy.test.ts +56 -111
  113. package/src/__tests__/host-file-proxy.test.ts +44 -98
  114. package/src/__tests__/host-file-read-tool.test.ts +42 -21
  115. package/src/__tests__/host-shell-tool.test.ts +33 -68
  116. package/src/__tests__/host-transfer-pending-interactions.test.ts +2 -18
  117. package/src/__tests__/host-transfer-proxy.test.ts +43 -53
  118. package/src/__tests__/http-user-message-parity.test.ts +0 -6
  119. package/src/__tests__/inbound-slack-persistence.test.ts +31 -0
  120. package/src/__tests__/injector-chain.test.ts +10 -5
  121. package/src/__tests__/injector-pkb-v2-silenced.test.ts +124 -0
  122. package/src/__tests__/inline-command-runner.test.ts +0 -66
  123. package/src/__tests__/inline-skill-load-permissions.test.ts +0 -2
  124. package/src/__tests__/install-skill-routing.test.ts +1 -13
  125. package/src/__tests__/llm-callsite-catalog.test.ts +34 -0
  126. package/src/__tests__/llm-catalog-parity.test.ts +90 -0
  127. package/src/__tests__/llm-context-resolution.test.ts +180 -0
  128. package/src/__tests__/llm-resolver.test.ts +80 -12
  129. package/src/__tests__/llm-usage-store.test.ts +269 -4
  130. package/src/__tests__/log-export-routes.test.ts +89 -0
  131. package/src/__tests__/managed-profile-guard.test.ts +225 -0
  132. package/src/__tests__/managed-skill-lifecycle.test.ts +0 -10
  133. package/src/__tests__/manual-token-reconciliation.test.ts +334 -0
  134. package/src/__tests__/memory-v2-static-injector.test.ts +95 -0
  135. package/src/__tests__/migration-cross-version-compatibility.test.ts +197 -291
  136. package/src/__tests__/migration-export-http.test.ts +33 -26
  137. package/src/__tests__/migration-export-streaming.test.ts +18 -10
  138. package/src/__tests__/migration-export-to-gcs.test.ts +49 -9
  139. package/src/__tests__/migration-import-commit-http.test.ts +66 -21
  140. package/src/__tests__/migration-import-from-gcs.test.ts +50 -9
  141. package/src/__tests__/migration-import-from-url.test.ts +20 -6
  142. package/src/__tests__/migration-import-preflight-http.test.ts +95 -95
  143. package/src/__tests__/migration-parity-persistence.test.ts +62 -25
  144. package/src/__tests__/migration-transport.test.ts +115 -23
  145. package/src/__tests__/migration-validate-http.test.ts +105 -80
  146. package/src/__tests__/migration-wizard.test.ts +133 -27
  147. package/src/__tests__/non-member-access-request.test.ts +1 -1
  148. package/src/__tests__/notification-guardian-path.test.ts +1 -1
  149. package/src/__tests__/oauth-store.test.ts +19 -0
  150. package/src/__tests__/platform-bash-auto-approve.test.ts +21 -12
  151. package/src/__tests__/prechat-onboarding-contract.test.ts +31 -7
  152. package/src/__tests__/pricing.test.ts +68 -4
  153. package/src/__tests__/process-message-background-slack.test.ts +331 -0
  154. package/src/__tests__/provider-managed-proxy-integration.test.ts +153 -17
  155. package/src/__tests__/provider-send-message-override-profile.test.ts +50 -0
  156. package/src/__tests__/provider-usage-tracking.test.ts +208 -0
  157. package/src/__tests__/reaction-persistence.test.ts +9 -6
  158. package/src/__tests__/rebind-secrets-screen.test.ts +53 -16
  159. package/src/__tests__/recording-handler.test.ts +64 -81
  160. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +4 -3
  161. package/src/__tests__/relay-server.test.ts +18 -13
  162. package/src/__tests__/require-fresh-approval.test.ts +13 -22
  163. package/src/__tests__/runtime-attachment-metadata.test.ts +1 -1
  164. package/src/__tests__/runtime-events-sse-parity.test.ts +3 -4
  165. package/src/__tests__/runtime-events-sse.test.ts +3 -12
  166. package/src/__tests__/search-skills-unified.test.ts +9 -15
  167. package/src/__tests__/secret-ingress-cli.test.ts +2 -5
  168. package/src/__tests__/secret-ingress-http.test.ts +0 -4
  169. package/src/__tests__/secret-onetime-send.test.ts +4 -2
  170. package/src/__tests__/secret-prompt-log-hygiene.test.ts +24 -7
  171. package/src/__tests__/secret-prompter-channel-fallback.test.ts +42 -47
  172. package/src/__tests__/secret-response-routing.test.ts +29 -15
  173. package/src/__tests__/secret-routes-managed-proxy.test.ts +5 -1
  174. package/src/__tests__/secret-scanner.test.ts +2 -545
  175. package/src/__tests__/send-endpoint-busy.test.ts +9 -24
  176. package/src/__tests__/settings-routes.test.ts +1 -1
  177. package/src/__tests__/shell-credential-ref.test.ts +0 -8
  178. package/src/__tests__/shell-tool-proxy-mode.test.ts +0 -56
  179. package/src/__tests__/skill-script-runner-sandbox.test.ts +0 -11
  180. package/src/__tests__/skill-tool-factory.test.ts +97 -0
  181. package/src/__tests__/skills-file-content-endpoint.test.ts +9 -30
  182. package/src/__tests__/skills-files-catalog-fallback.test.ts +11 -17
  183. package/src/__tests__/slack-inbound-verification.test.ts +1 -62
  184. package/src/__tests__/subagent-fork-notifications.test.ts +57 -47
  185. package/src/__tests__/subagent-manager-notify.test.ts +70 -70
  186. package/src/__tests__/subagent-notify-parent.test.ts +80 -83
  187. package/src/__tests__/system-prompt.test.ts +115 -13
  188. package/src/__tests__/terminal-tools.test.ts +0 -89
  189. package/src/__tests__/thread-backfill.test.ts +945 -31
  190. package/src/__tests__/tool-domain-event-publisher.test.ts +0 -36
  191. package/src/__tests__/tool-execute-pipeline.test.ts +0 -6
  192. package/src/__tests__/tool-execution-abort-cleanup.test.ts +0 -16
  193. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +9 -19
  194. package/src/__tests__/tool-executor-lifecycle-events.test.ts +4 -7
  195. package/src/__tests__/tool-executor.test.ts +12 -19
  196. package/src/__tests__/tool-metrics-listener.test.ts +0 -35
  197. package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
  198. package/src/__tests__/tool-trace-listener.test.ts +0 -17
  199. package/src/__tests__/transfer-progress-screen.test.ts +63 -26
  200. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +2 -149
  201. package/src/__tests__/trusted-contact-multichannel.test.ts +2 -4
  202. package/src/__tests__/trusted-contact-verification.test.ts +1 -1
  203. package/src/__tests__/tts-catalog-parity.test.ts +16 -5
  204. package/src/__tests__/usage-attribution.test.ts +247 -0
  205. package/src/__tests__/usage-cli.test.ts +143 -0
  206. package/src/__tests__/usage-grouped-buckets.test.ts +155 -0
  207. package/src/__tests__/usage-routes.test.ts +150 -0
  208. package/src/__tests__/validation-results-screen.test.ts +39 -16
  209. package/src/__tests__/vbundle-pax-and-symlink.test.ts +12 -3
  210. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +49 -137
  211. package/src/__tests__/verification-control-plane-policy.test.ts +4 -7
  212. package/src/__tests__/voice-session-bridge.test.ts +5 -5
  213. package/src/__tests__/workspace-migration-062-drop-memory-v2-edges-json.test.ts +103 -0
  214. package/src/__tests__/workspace-migration-063-release-notes-dynamic-model-context.test.ts +77 -0
  215. package/src/__tests__/workspace-migration-064-unwind-main-agent-opus-seed.test.ts +225 -0
  216. package/src/__tests__/workspace-migration-memory-v2-init.test.ts +8 -30
  217. package/src/acp/index.ts +0 -15
  218. package/src/acp/session-manager.ts +37 -34
  219. package/src/agent/loop.ts +16 -1
  220. package/src/approvals/AGENTS.md +4 -0
  221. package/src/approvals/__tests__/guardian-feed-event.test.ts +10 -3
  222. package/src/approvals/guardian-request-resolvers.ts +10 -2
  223. package/src/backup/__tests__/backup-worker.test.ts +36 -8
  224. package/src/backup/__tests__/paths.test.ts +2 -2
  225. package/src/backup/__tests__/restore.test.ts +45 -28
  226. package/src/backup/backup-worker.ts +36 -2
  227. package/src/backup/paths.ts +9 -6
  228. package/src/browser-session/events.ts +0 -9
  229. package/src/calls/call-store.ts +1 -34
  230. package/src/calls/guardian-question-copy.ts +0 -108
  231. package/src/calls/relay-server.ts +0 -24
  232. package/src/calls/twilio-rest.ts +0 -38
  233. package/src/calls/twilio-routes.ts +1 -1
  234. package/src/calls/voice-session-bridge.ts +7 -38
  235. package/src/channels/types.ts +1 -36
  236. package/src/cli/commands/__tests__/cache.test.ts +152 -5
  237. package/src/cli/commands/__tests__/memory-v2.test.ts +14 -28
  238. package/src/cli/commands/__tests__/trust.test.ts +21 -387
  239. package/src/cli/commands/backup.ts +4 -4
  240. package/src/cli/commands/cache-fs.ts +8 -0
  241. package/src/cli/commands/cache.ts +153 -82
  242. package/src/cli/commands/clients.ts +63 -5
  243. package/src/cli/commands/completions.ts +3 -3
  244. package/src/cli/commands/contacts.ts +231 -76
  245. package/src/cli/commands/keys.ts +4 -1
  246. package/src/cli/commands/memory-v2.ts +24 -52
  247. package/src/cli/commands/oauth/shared.ts +2 -29
  248. package/src/cli/commands/pending.ts +102 -0
  249. package/src/cli/commands/skills.ts +77 -35
  250. package/src/cli/commands/trust.ts +70 -430
  251. package/src/cli/commands/usage.ts +25 -16
  252. package/src/cli/lib/daemon-credential-client.ts +14 -0
  253. package/src/cli/program.ts +2 -0
  254. package/src/cli.ts +0 -21
  255. package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
  256. package/src/config/bundled-skills/messaging/TOOLS.json +14 -4
  257. package/src/config/env-registry.ts +12 -2
  258. package/src/config/env.ts +3 -14
  259. package/src/config/feature-flag-registry.json +30 -30
  260. package/src/config/llm-callsite-catalog.ts +12 -0
  261. package/src/config/llm-context-resolution.ts +80 -0
  262. package/src/config/llm-resolver.ts +58 -22
  263. package/src/config/loader.ts +3 -3
  264. package/src/config/schema.ts +2 -158
  265. package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
  266. package/src/config/schemas/call-site-catalog.ts +271 -0
  267. package/src/config/schemas/calls.ts +5 -5
  268. package/src/config/schemas/inference.ts +1 -1
  269. package/src/config/schemas/ingress.ts +1 -1
  270. package/src/config/schemas/llm.ts +31 -3
  271. package/src/config/schemas/memory-retrieval.ts +2 -2
  272. package/src/config/schemas/memory-v2.ts +9 -0
  273. package/src/config/schemas/security.ts +1 -42
  274. package/src/config/schemas/services.ts +6 -6
  275. package/src/config/schemas/skills.ts +5 -5
  276. package/src/config/schemas/tts.ts +1 -1
  277. package/src/config/seed-inference-profiles.ts +117 -0
  278. package/src/config/skills.ts +0 -90
  279. package/src/config/types.ts +3 -6
  280. package/src/contacts/contact-store.ts +0 -17
  281. package/src/contacts/contacts-write.ts +1 -105
  282. package/src/context/window-manager.ts +44 -5
  283. package/src/credential-execution/process-manager.ts +34 -10
  284. package/src/credential-health/credential-health-service.ts +21 -16
  285. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +75 -82
  286. package/src/daemon/__tests__/daemon-skill-host.test.ts +2 -9
  287. package/src/daemon/connection-policy.ts +1 -26
  288. package/src/daemon/conversation-agent-loop-handlers.ts +53 -4
  289. package/src/daemon/conversation-agent-loop.ts +277 -36
  290. package/src/daemon/conversation-history.ts +8 -8
  291. package/src/daemon/conversation-launch.ts +20 -135
  292. package/src/daemon/conversation-lifecycle.ts +1 -1
  293. package/src/daemon/conversation-messaging.ts +1 -0
  294. package/src/daemon/conversation-process.ts +83 -163
  295. package/src/daemon/conversation-runtime-assembly.ts +219 -76
  296. package/src/daemon/conversation-slash.ts +47 -5
  297. package/src/daemon/conversation-store.ts +7 -31
  298. package/src/daemon/conversation-surfaces.ts +22 -28
  299. package/src/daemon/conversation-tool-setup.ts +3 -33
  300. package/src/daemon/conversation-usage.ts +36 -0
  301. package/src/daemon/conversation.ts +117 -233
  302. package/src/daemon/daemon-control.ts +3 -71
  303. package/src/daemon/daemon-skill-host.ts +8 -11
  304. package/src/daemon/dictation-profile-store.ts +2 -26
  305. package/src/daemon/first-greeting.ts +44 -156
  306. package/src/daemon/handlers/config-channels.ts +12 -12
  307. package/src/daemon/handlers/config-ingress.ts +4 -165
  308. package/src/daemon/handlers/config-model.ts +1 -1
  309. package/src/daemon/handlers/config-voice.ts +0 -42
  310. package/src/daemon/handlers/conversations.ts +11 -190
  311. package/src/daemon/handlers/recording.ts +26 -158
  312. package/src/daemon/handlers/shared.ts +23 -71
  313. package/src/daemon/handlers/skills.ts +42 -93
  314. package/src/daemon/host-bash-proxy.ts +67 -45
  315. package/src/daemon/host-browser-proxy.ts +65 -27
  316. package/src/daemon/host-cu-proxy.ts +40 -39
  317. package/src/daemon/host-file-proxy.ts +58 -37
  318. package/src/daemon/host-transfer-proxy.ts +84 -46
  319. package/src/daemon/lifecycle.ts +49 -15
  320. package/src/daemon/message-types/conversations.ts +7 -0
  321. package/src/daemon/message-types/host-bash.ts +1 -0
  322. package/src/daemon/message-types/host-cu.ts +1 -0
  323. package/src/daemon/message-types/host-file.ts +1 -0
  324. package/src/daemon/message-types/host-transfer.ts +1 -0
  325. package/src/daemon/message-types/messages.ts +10 -9
  326. package/src/daemon/message-types/workspace.ts +1 -1
  327. package/src/daemon/process-message.ts +102 -239
  328. package/src/daemon/server.ts +13 -462
  329. package/src/daemon/shutdown-handlers.ts +2 -2
  330. package/src/daemon/tool-side-effects.ts +125 -107
  331. package/src/daemon/trust-context.ts +13 -0
  332. package/src/daemon/wake-target-adapter.ts +4 -9
  333. package/src/events/domain-events.ts +0 -8
  334. package/src/events/tool-audit-listener.ts +3 -1
  335. package/src/events/tool-domain-event-publisher.ts +0 -10
  336. package/src/events/tool-metrics-listener.ts +0 -17
  337. package/src/events/tool-trace-listener.ts +0 -14
  338. package/src/filing/filing-service.ts +13 -1
  339. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +6 -2
  340. package/src/heartbeat/heartbeat-service.ts +23 -5
  341. package/src/home/__tests__/feed-writer.test.ts +0 -4
  342. package/src/home/__tests__/relationship-state-writer.test.ts +30 -0
  343. package/src/home/feed-writer.ts +1 -2
  344. package/src/home/relationship-state-writer.ts +16 -3
  345. package/src/ipc/__tests__/browser-ipc.test.ts +2 -12
  346. package/src/ipc/__tests__/skill-server-bidirectional.test.ts +0 -1
  347. package/src/ipc/assistant-server.ts +3 -10
  348. package/src/ipc/routes/__tests__/memory-v2-backfill.test.ts +39 -20
  349. package/src/ipc/routes/route-adapter.ts +1 -1
  350. package/src/ipc/routes/trust-rules.test.ts +0 -95
  351. package/src/ipc/skill-ipc-types.ts +41 -0
  352. package/src/ipc/skill-routes/__tests__/events-ipc.test.ts +13 -27
  353. package/src/ipc/skill-routes/__tests__/identity.test.ts +4 -23
  354. package/src/ipc/skill-routes/events.ts +12 -23
  355. package/src/ipc/skill-routes/identity.ts +4 -17
  356. package/src/ipc/skill-routes/index.ts +1 -1
  357. package/src/ipc/skill-server.ts +6 -39
  358. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +0 -8
  359. package/src/live-voice/protocol.ts +4 -13
  360. package/src/mcp/manager.ts +0 -5
  361. package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +55 -0
  362. package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +127 -0
  363. package/src/memory/app-git-service.ts +0 -32
  364. package/src/memory/app-store.ts +154 -0
  365. package/src/memory/attachments-store.ts +6 -0
  366. package/src/memory/context-search/sources/memory-v2.ts +578 -0
  367. package/src/memory/context-search/sources/memory.ts +5 -0
  368. package/src/memory/context-search/sources/pkb.ts +10 -1
  369. package/src/memory/context-search/sources/workspace.ts +3 -2
  370. package/src/memory/conversation-crud.ts +29 -4
  371. package/src/memory/conversation-disk-view.ts +1 -5
  372. package/src/memory/conversation-starter-checkpoints.ts +63 -0
  373. package/src/memory/db-connection.ts +62 -0
  374. package/src/memory/db-init.ts +14 -0
  375. package/src/memory/embedding-backend.ts +3 -21
  376. package/src/memory/embedding-gemini.ts +0 -2
  377. package/src/memory/embedding-local.ts +6 -6
  378. package/src/memory/embedding-ollama.ts +6 -6
  379. package/src/memory/embedding-openai.ts +6 -6
  380. package/src/memory/embedding-types.ts +21 -0
  381. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +3 -7
  382. package/src/memory/graph/conversation-graph-memory.ts +35 -13
  383. package/src/memory/graph/injection.test.ts +2 -2
  384. package/src/memory/graph/injection.ts +1 -1
  385. package/src/memory/guardian-action-store.ts +0 -83
  386. package/src/memory/guardian-approvals.ts +0 -48
  387. package/src/memory/indexer.ts +1 -15
  388. package/src/memory/job-handlers/conversation-starters.ts +36 -53
  389. package/src/memory/job-utils.ts +0 -6
  390. package/src/memory/jobs-store.ts +0 -1
  391. package/src/memory/jobs-worker.ts +2 -16
  392. package/src/memory/llm-request-log-store.ts +0 -41
  393. package/src/memory/llm-usage-store.ts +129 -43
  394. package/src/memory/memory-v2-activation-log-store.ts +115 -0
  395. package/src/memory/migrations/233-document-conversations.ts +54 -0
  396. package/src/memory/migrations/234-memory-v2-activation-logs.ts +55 -0
  397. package/src/memory/migrations/235-llm-usage-attribution.ts +31 -0
  398. package/src/memory/migrations/235-slack-compaction-watermark.ts +44 -0
  399. package/src/memory/migrations/236-tool-invocations-matched-rule-id.ts +26 -0
  400. package/src/memory/migrations/__tests__/234-memory-v2-activation-logs.test.ts +182 -0
  401. package/src/memory/migrations/index.ts +14 -0
  402. package/src/memory/migrations/registry.ts +24 -0
  403. package/src/memory/raw-query.ts +2 -68
  404. package/src/memory/schema/conversations.ts +7 -0
  405. package/src/memory/schema/infrastructure.ts +25 -0
  406. package/src/memory/search/semantic.ts +5 -16
  407. package/src/memory/tool-usage-store.ts +2 -0
  408. package/src/memory/usage-buckets.ts +40 -1
  409. package/src/memory/usage-grouped-buckets.ts +127 -0
  410. package/src/memory/v2/__tests__/activation.test.ts +289 -90
  411. package/src/memory/v2/__tests__/backfill-jobs.test.ts +2 -129
  412. package/src/memory/v2/__tests__/consolidation-job.test.ts +28 -11
  413. package/src/memory/v2/__tests__/edge-index.test.ts +278 -0
  414. package/src/memory/v2/__tests__/injection.test.ts +384 -15
  415. package/src/memory/v2/__tests__/migration.test.ts +64 -36
  416. package/src/memory/v2/__tests__/page-store.test.ts +191 -8
  417. package/src/memory/v2/__tests__/prompts-consolidation.test.ts +181 -0
  418. package/src/memory/v2/__tests__/skill-store.test.ts +115 -3
  419. package/src/memory/v2/__tests__/static-context.test.ts +153 -0
  420. package/src/memory/v2/activation.ts +168 -97
  421. package/src/memory/v2/backfill-jobs.ts +15 -100
  422. package/src/memory/v2/consolidation-job.ts +14 -12
  423. package/src/memory/v2/edge-index.ts +191 -0
  424. package/src/memory/v2/injection.ts +182 -58
  425. package/src/memory/v2/migration.ts +57 -64
  426. package/src/memory/v2/now-text.ts +2 -3
  427. package/src/memory/v2/page-store.ts +168 -31
  428. package/src/memory/v2/prompts/consolidation.ts +118 -42
  429. package/src/memory/v2/prompts/sweep.ts +3 -3
  430. package/src/memory/v2/skill-store.ts +55 -7
  431. package/src/memory/v2/static-context.ts +62 -0
  432. package/src/memory/v2/types.ts +10 -20
  433. package/src/memory/validation.ts +0 -11
  434. package/src/messaging/draft-store.ts +0 -6
  435. package/src/messaging/provider-types.ts +8 -0
  436. package/src/messaging/provider.ts +7 -0
  437. package/src/messaging/providers/gmail/client.ts +1 -121
  438. package/src/messaging/providers/outlook/client.ts +0 -73
  439. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +226 -0
  440. package/src/messaging/providers/slack/adapter.ts +122 -21
  441. package/src/messaging/providers/slack/backfill.test.ts +95 -6
  442. package/src/messaging/providers/slack/backfill.ts +89 -11
  443. package/src/messaging/providers/slack/client.ts +10 -124
  444. package/src/messaging/providers/slack/message-metadata.ts +12 -2
  445. package/src/messaging/providers/slack/render-transcript.test.ts +56 -0
  446. package/src/messaging/providers/slack/render-transcript.ts +126 -25
  447. package/src/messaging/providers/slack/types.ts +1 -0
  448. package/src/oauth/connection-resolver.test.ts +8 -0
  449. package/src/oauth/connection-resolver.ts +8 -16
  450. package/src/oauth/credential-token-resolver.ts +97 -0
  451. package/src/oauth/manual-token-connection.ts +30 -34
  452. package/src/oauth/oauth-store.ts +6 -4
  453. package/src/outbound-proxy/certs.ts +0 -7
  454. package/src/outbound-proxy/config.ts +0 -74
  455. package/src/outbound-proxy/health.ts +0 -44
  456. package/src/outbound-proxy/index.ts +0 -22
  457. package/src/permissions/approval-provenance.test.ts +184 -0
  458. package/src/permissions/approval-provenance.ts +70 -0
  459. package/src/permissions/checker.ts +4 -1
  460. package/src/permissions/gateway-threshold-reader.ts +4 -1
  461. package/src/permissions/prompter.ts +9 -2
  462. package/src/permissions/secret-prompter.ts +21 -48
  463. package/src/permissions/types.ts +33 -0
  464. package/src/permissions/workspace-policy.ts +0 -5
  465. package/src/platform/sync-identity.ts +0 -8
  466. package/src/plugins/defaults/injectors.ts +69 -2
  467. package/src/plugins/defaults/overflow-reduce.ts +3 -2
  468. package/src/plugins/types.ts +8 -0
  469. package/src/prompts/system-prompt.ts +34 -70
  470. package/src/prompts/templates/BOOTSTRAP.md +52 -6
  471. package/src/prompts/update-bulletin-job.ts +2 -0
  472. package/src/providers/__tests__/retry-callsite.test.ts +138 -1
  473. package/src/providers/anthropic/client.ts +72 -33
  474. package/src/providers/call-site-routing.ts +42 -3
  475. package/src/providers/gemini/client.ts +18 -2
  476. package/src/providers/managed-proxy/context.ts +0 -5
  477. package/src/providers/model-catalog.ts +105 -19
  478. package/src/providers/openai/chat-completions-provider.ts +6 -0
  479. package/src/providers/openai/responses-provider.ts +7 -1
  480. package/src/providers/provider-send-message.ts +45 -2
  481. package/src/providers/ratelimit.ts +7 -2
  482. package/src/providers/registry.ts +14 -9
  483. package/src/providers/retry.ts +96 -8
  484. package/src/providers/types.ts +13 -0
  485. package/src/providers/usage-tracking.ts +96 -0
  486. package/src/runtime/AGENTS.md +10 -6
  487. package/src/runtime/__tests__/agent-wake.test.ts +89 -0
  488. package/src/runtime/agent-wake.ts +39 -2
  489. package/src/runtime/assistant-event-hub.ts +541 -45
  490. package/src/runtime/assistant-event.ts +1 -6
  491. package/src/runtime/auth/context.ts +0 -9
  492. package/src/runtime/auth/middleware.ts +1 -1
  493. package/src/runtime/auth/route-policy.ts +11 -9
  494. package/src/runtime/auth/token-service.ts +0 -11
  495. package/src/runtime/channel-approvals.ts +6 -2
  496. package/src/runtime/channel-verification-service.ts +3 -5
  497. package/src/runtime/http-errors.ts +0 -34
  498. package/src/runtime/http-router.ts +6 -3
  499. package/src/runtime/http-server.ts +22 -82
  500. package/src/runtime/http-types.ts +5 -0
  501. package/src/runtime/interactive-ui.ts +0 -1
  502. package/src/runtime/middleware/auth.ts +0 -20
  503. package/src/runtime/migrations/__tests__/v1-test-helpers.ts +112 -0
  504. package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +11 -4
  505. package/src/runtime/migrations/__tests__/vbundle-builder-v1-shape.test.ts +253 -0
  506. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +19 -6
  507. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +71 -27
  508. package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +41 -2
  509. package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +143 -79
  510. package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +143 -23
  511. package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +2 -2
  512. package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +371 -0
  513. package/src/runtime/migrations/migration-transport.ts +46 -13
  514. package/src/runtime/migrations/migration-wizard.ts +2 -2
  515. package/src/runtime/migrations/origin-mode.ts +40 -0
  516. package/src/runtime/migrations/vbundle-builder.ts +133 -79
  517. package/src/runtime/migrations/vbundle-import-analyzer.ts +9 -7
  518. package/src/runtime/migrations/vbundle-importer.ts +7 -7
  519. package/src/runtime/migrations/vbundle-metadata-merge.ts +1 -1
  520. package/src/runtime/migrations/vbundle-streaming-importer.ts +3 -3
  521. package/src/runtime/migrations/vbundle-streaming-validator.ts +48 -26
  522. package/src/runtime/migrations/vbundle-validator.ts +214 -41
  523. package/src/runtime/pending-interactions.ts +13 -4
  524. package/src/runtime/routes/__tests__/acp-routes.test.ts +0 -1
  525. package/src/runtime/routes/__tests__/backup-routes.test.ts +28 -19
  526. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +235 -0
  527. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +58 -0
  528. package/src/runtime/routes/__tests__/migration-export-secrets-redacted.test.ts +54 -0
  529. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +19 -6
  530. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +7 -7
  531. package/src/runtime/routes/acp-routes.test.ts +0 -3
  532. package/src/runtime/routes/acp-routes.ts +3 -7
  533. package/src/runtime/routes/app-management-routes.ts +18 -9
  534. package/src/runtime/routes/approval-routes.ts +55 -14
  535. package/src/runtime/routes/avatar-routes.ts +3 -5
  536. package/src/runtime/routes/browser-routes.ts +1 -15
  537. package/src/runtime/routes/channel-guardian-routes.ts +1 -5
  538. package/src/runtime/routes/channel-readiness-routes.ts +3 -7
  539. package/src/runtime/routes/channel-route-shared.ts +2 -28
  540. package/src/runtime/routes/client-routes.ts +45 -12
  541. package/src/runtime/routes/consolidation-routes.ts +115 -0
  542. package/src/runtime/routes/conversation-list-routes.ts +12 -29
  543. package/src/runtime/routes/conversation-management-routes.ts +14 -51
  544. package/src/runtime/routes/conversation-query-routes.ts +120 -8
  545. package/src/runtime/routes/conversation-routes.ts +44 -528
  546. package/src/runtime/routes/conversation-starter-routes.ts +19 -40
  547. package/src/runtime/routes/documents-routes.ts +53 -18
  548. package/src/runtime/routes/events-routes.ts +59 -91
  549. package/src/runtime/routes/filing-routes.ts +18 -1
  550. package/src/runtime/routes/guardian-action-routes.ts +4 -9
  551. package/src/runtime/routes/host-bash-routes.ts +3 -2
  552. package/src/runtime/routes/host-browser-routes.ts +9 -33
  553. package/src/runtime/routes/host-cu-routes.ts +6 -1
  554. package/src/runtime/routes/host-file-routes.ts +3 -2
  555. package/src/runtime/routes/host-transfer-routes.ts +11 -15
  556. package/src/runtime/routes/identity-routes.ts +78 -6
  557. package/src/runtime/routes/inbound-message-handler.ts +580 -137
  558. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -88
  559. package/src/runtime/routes/inbound-stages/background-dispatch.ts +3 -0
  560. package/src/runtime/routes/index.ts +4 -0
  561. package/src/runtime/routes/integrations/slack/channel.ts +0 -24
  562. package/src/runtime/routes/llm-call-sites-routes.ts +22 -0
  563. package/src/runtime/routes/memory-v2-routes.ts +10 -15
  564. package/src/runtime/routes/migration-routes.ts +188 -31
  565. package/src/runtime/routes/playground/guard.ts +1 -1
  566. package/src/runtime/routes/playground/index.ts +0 -2
  567. package/src/runtime/routes/recording-routes.ts +4 -24
  568. package/src/runtime/routes/rename-conversation-routes.ts +2 -6
  569. package/src/runtime/routes/schedule-routes.ts +3 -6
  570. package/src/runtime/routes/secret-routes.ts +87 -18
  571. package/src/runtime/routes/settings-routes.ts +29 -28
  572. package/src/runtime/routes/skills-routes.ts +12 -31
  573. package/src/runtime/routes/suggest-trust-rule-routes.ts +32 -1
  574. package/src/runtime/routes/task-routes.ts +6 -6
  575. package/src/runtime/routes/trust-rules-routes.ts +3 -94
  576. package/src/runtime/routes/types.ts +4 -4
  577. package/src/runtime/routes/upgrade-broadcast-routes.ts +3 -10
  578. package/src/runtime/routes/usage-routes.ts +87 -10
  579. package/src/runtime/routes/user-routes.ts +17 -31
  580. package/src/runtime/routes/work-items-routes.ts +1 -4
  581. package/src/runtime/services/__tests__/analyze-conversation.test.ts +2 -2
  582. package/src/runtime/services/analyze-conversation.ts +7 -17
  583. package/src/runtime/services/conversation-serializer.ts +2 -4
  584. package/src/runtime/verification-outbound-actions.ts +1 -1
  585. package/src/runtime/verification-rate-limiter.ts +1 -1
  586. package/src/schedule/schedule-store.ts +0 -16
  587. package/src/security/secret-scanner.ts +14 -547
  588. package/src/security/secure-keys.ts +31 -11
  589. package/src/security/token-manager.ts +7 -3
  590. package/src/signals/cancel.ts +16 -25
  591. package/src/signals/conversation-undo.ts +2 -27
  592. package/src/signals/emit-event.ts +1 -2
  593. package/src/signals/user-message.ts +108 -22
  594. package/src/skills/catalog-install.ts +1 -0
  595. package/src/skills/clawhub.ts +2 -2
  596. package/src/skills/inline-command-runner.ts +1 -7
  597. package/src/subagent/manager.ts +67 -84
  598. package/src/tasks/task-store.ts +1 -28
  599. package/src/telemetry/types.ts +6 -0
  600. package/src/telemetry/usage-telemetry-reporter.test.ts +38 -15
  601. package/src/telemetry/usage-telemetry-reporter.ts +3 -5
  602. package/src/tools/acp/spawn.test.ts +1 -2
  603. package/src/tools/acp/steer.test.ts +1 -2
  604. package/src/tools/browser/__tests__/browser-status.test.ts +44 -127
  605. package/src/tools/browser/browser-execution.ts +31 -147
  606. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +92 -68
  607. package/src/tools/browser/cdp-client/factory.ts +48 -76
  608. package/src/tools/browser/cdp-client/index.ts +1 -14
  609. package/src/tools/executor.ts +44 -31
  610. package/src/tools/host-filesystem/edit.ts +3 -2
  611. package/src/tools/host-filesystem/read.ts +3 -2
  612. package/src/tools/host-filesystem/transfer.test.ts +45 -42
  613. package/src/tools/host-filesystem/transfer.ts +4 -3
  614. package/src/tools/host-filesystem/write.ts +3 -2
  615. package/src/tools/host-terminal/host-shell.ts +4 -3
  616. package/src/tools/network/script-proxy/index.ts +1 -10
  617. package/src/tools/permission-checker.ts +66 -1
  618. package/src/tools/skills/sandbox-runner.ts +1 -6
  619. package/src/tools/skills/skill-tool-factory.ts +32 -0
  620. package/src/tools/terminal/safe-env.ts +1 -0
  621. package/src/tools/terminal/shell.ts +2 -78
  622. package/src/tools/types.ts +12 -39
  623. package/src/tts/__tests__/provider-catalog.test.ts +2 -2
  624. package/src/tts/provider-catalog.ts +1 -1
  625. package/src/usage/actors.ts +2 -1
  626. package/src/usage/attribution.ts +185 -0
  627. package/src/usage/pricing.ts +166 -0
  628. package/src/usage/types.ts +14 -0
  629. package/src/util/json.ts +13 -0
  630. package/src/util/logger.ts +3 -3
  631. package/src/util/pricing.ts +50 -3
  632. package/src/work-items/work-item-runner.ts +15 -42
  633. package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +4 -3
  634. package/src/workspace/migrations/052-seed-default-inference-profiles.ts +3 -3
  635. package/src/workspace/migrations/060-memory-v2-init.ts +2 -18
  636. package/src/workspace/migrations/061-move-backup-key-to-workspace.ts +59 -0
  637. package/src/workspace/migrations/062-drop-memory-v2-edges-json.ts +27 -0
  638. package/src/workspace/migrations/063-release-notes-dynamic-model-context.ts +70 -0
  639. package/src/workspace/migrations/064-unwind-main-agent-opus-seed.ts +64 -0
  640. package/src/workspace/migrations/registry.ts +8 -0
  641. package/src/workspace/provider-commit-message-generator.ts +3 -3
  642. package/src/__tests__/sandbox-diagnostics.test.ts +0 -138
  643. package/src/__tests__/sandbox-host-parity.test.ts +0 -1024
  644. package/src/__tests__/secret-detection-handler.test.ts +0 -67
  645. package/src/__tests__/secret-scanner-executor.test.ts +0 -450
  646. package/src/__tests__/tcc-sandbox-deny.test.ts +0 -198
  647. package/src/__tests__/terminal-sandbox.test.ts +0 -374
  648. package/src/__tests__/tool-notification-listener.test.ts +0 -65
  649. package/src/context/__tests__/microcompact.test.ts +0 -805
  650. package/src/context/microcompact.ts +0 -443
  651. package/src/daemon/handlers/slack-channel-oauth-install.ts +0 -197
  652. package/src/events/tool-notification-listener.ts +0 -17
  653. package/src/ipc/routes/__tests__/memory-v2-validate.test.ts +0 -219
  654. package/src/memory/v2/__tests__/edges.test.ts +0 -435
  655. package/src/memory/v2/edges.ts +0 -217
  656. package/src/prompts/__tests__/system-prompt-memory-v2.test.ts +0 -197
  657. package/src/runtime/__tests__/chrome-extension-registry.test.ts +0 -518
  658. package/src/runtime/__tests__/client-registry.test.ts +0 -271
  659. package/src/runtime/chrome-extension-registry.ts +0 -368
  660. package/src/runtime/client-registry.ts +0 -254
  661. package/src/runtime/routes/inbound-stages/verification-intercept.ts +0 -329
  662. package/src/tools/secret-detection-handler.ts +0 -269
  663. package/src/tools/terminal/backends/native.ts +0 -327
  664. package/src/tools/terminal/backends/types.ts +0 -37
  665. package/src/tools/terminal/sandbox-diagnostics.ts +0 -87
  666. package/src/tools/terminal/sandbox.ts +0 -40
@@ -150,7 +150,10 @@ mock.module("../activation.js", () => ({
150
150
  // injection logic introspects. Stub them to empty so the test stays focused
151
151
  // on the wiring, not the pipeline internals (covered in activation.test.ts).
152
152
  selectSkillCandidates: async () => new Set<string>(),
153
- computeSkillActivation: async () => new Map<string, number>(),
153
+ computeSkillActivation: async () => ({
154
+ activation: new Map<string, number>(),
155
+ breakdown: new Map(),
156
+ }),
154
157
  selectSkillInjections: ({ topK }: { topK: number }) => ({
155
158
  topNow: skillState.topSkillIds.slice(0, topK),
156
159
  }),
@@ -160,6 +163,30 @@ mock.module("../skill-store.js", () => ({
160
163
  getSkillCapability: (id: string) => skillState.entries.get(id) ?? null,
161
164
  }));
162
165
 
166
+ // ---------------------------------------------------------------------------
167
+ // Activation-log store mock
168
+ // ---------------------------------------------------------------------------
169
+ //
170
+ // The real `recordMemoryV2ActivationLog` writes to the singleton
171
+ // `getDb()` — but this test uses an isolated in-memory database, so we mock
172
+ // the writer to capture calls in-process. `recordCalls` is the captured log
173
+ // array; `recordShouldThrow` makes the next call throw to verify the caller
174
+ // swallows the failure.
175
+
176
+ const telemetryState = {
177
+ recordCalls: [] as Array<Record<string, unknown>>,
178
+ recordShouldThrow: false,
179
+ };
180
+
181
+ mock.module("../../memory-v2-activation-log-store.js", () => ({
182
+ recordMemoryV2ActivationLog: (params: Record<string, unknown>) => {
183
+ if (telemetryState.recordShouldThrow) {
184
+ throw new Error("simulated telemetry write failure");
185
+ }
186
+ telemetryState.recordCalls.push(params);
187
+ },
188
+ }));
189
+
163
190
  // ---------------------------------------------------------------------------
164
191
  // Workspace + DB setup
165
192
  // ---------------------------------------------------------------------------
@@ -174,15 +201,9 @@ beforeAll(() => {
174
201
 
175
202
  // Seed the v2 directory layout the migration would normally create.
176
203
  mkdirSync(join(tmpWorkspace, "memory", "concepts"), { recursive: true });
177
- // edges.json: one edge so spreading-activation has structure to walk.
178
- writeFileSync(
179
- join(tmpWorkspace, "memory", "edges.json"),
180
- JSON.stringify({
181
- version: 1,
182
- edges: [["alice-vscode", "bob-coffee"]],
183
- }),
184
- );
185
- // Three concept pages with generic, placeholder bodies.
204
+ // Three concept pages with generic, placeholder bodies. Outgoing edges
205
+ // live in each page's frontmatter `edges:` list — there is no separate
206
+ // edges-index file under the directed-edges model.
186
207
  writeFileSync(
187
208
  join(tmpWorkspace, "memory", "concepts", "alice-vscode.md"),
188
209
  `---
@@ -207,6 +228,18 @@ ref_files: []
207
228
  ---
208
229
  Carol loves jazz music — Coltrane in particular.`,
209
230
  );
231
+ // A page with both `edges` and `ref_files` populated so the frontmatter-
232
+ // injection test can assert the full canonical shape.
233
+ writeFileSync(
234
+ join(tmpWorkspace, "memory", "concepts", "frontmatter-demo.md"),
235
+ `---
236
+ edges:
237
+ - alice-vscode
238
+ ref_files:
239
+ - images/demo.jpg
240
+ ---
241
+ Demo body content.`,
242
+ );
210
243
  });
211
244
 
212
245
  afterAll(() => {
@@ -319,6 +352,8 @@ function resetState(): void {
319
352
  state.queryResponses.sparse.length = 0;
320
353
  skillState.topSkillIds.length = 0;
321
354
  skillState.entries.clear();
355
+ telemetryState.recordCalls.length = 0;
356
+ telemetryState.recordShouldThrow = false;
322
357
  // The qdrant module caches its client; the cached client may be a
323
358
  // MockQdrantClient instance from a sibling test file. Reset to force a
324
359
  // fresh `new QdrantClient()` against this file's mock.
@@ -364,8 +399,8 @@ describe("injectMemoryV2Block", () => {
364
399
 
365
400
  expect(result.toInject).toEqual(["alice-vscode"]);
366
401
  expect(result.block).not.toBeNull();
367
- expect(result.block).toContain("<memory __injected>");
368
- expect(result.block).toContain("## What I Remember Right Now");
402
+ expect(result.block).toContain("<memory>");
403
+ expect(result.block).not.toContain("## What I Remember Right Now");
369
404
  expect(result.block).toContain("### alice-vscode");
370
405
  expect(result.block).toContain("VS Code");
371
406
  expect(result.block).toContain("</memory>");
@@ -512,6 +547,35 @@ describe("injectMemoryV2Block", () => {
512
547
  ]);
513
548
  });
514
549
 
550
+ test("includes the page frontmatter (edges, ref_files) in each rendered section", async () => {
551
+ // The frontmatter (`edges`, `ref_files`) lives on disk above the page
552
+ // body and is part of the page's content. Injection must reproduce both
553
+ // fields verbatim — bracketed by the canonical `---` delimiters — so the
554
+ // agent sees the page's edges and any referenced media paths alongside
555
+ // the prose body.
556
+ stageTurn([{ slug: "frontmatter-demo", denseScore: 0.9 }]);
557
+
558
+ const result = await injectMemoryV2Block({
559
+ database: db,
560
+ conversationId: "conv-1",
561
+ currentTurn: 1,
562
+ userMessage: "show me the demo",
563
+ assistantMessage: "",
564
+ nowText: "Now",
565
+ messageId: "msg-1",
566
+ config: makeConfig(),
567
+ });
568
+
569
+ expect(result.block).not.toBeNull();
570
+ // Slug header is immediately followed by the frontmatter open delimiter.
571
+ expect(result.block).toContain("### frontmatter-demo\n---\n");
572
+ // Both fields render in YAML block style with their populated values.
573
+ expect(result.block).toContain("edges:\n - alice-vscode");
574
+ expect(result.block).toContain("ref_files:\n - images/demo.jpg");
575
+ // Body still renders after the closing delimiter.
576
+ expect(result.block).toContain("Demo body content.");
577
+ });
578
+
515
579
  test("renders pages in activation-descending order", async () => {
516
580
  // Both slugs are fresh (no prior state). carol scores higher than alice
517
581
  // on every channel — so carol should be ranked first in topNow and
@@ -594,7 +658,7 @@ describe("injectMemoryV2Block", () => {
594
658
  // Skill subsection rendering
595
659
  // ---------------------------------------------------------------------------
596
660
 
597
- test("renders a skill-only block under the same `What I Remember Right Now` header", async () => {
661
+ test("renders a skill-only block in the same `<memory>` wrapper as concept-page-only blocks", async () => {
598
662
  // No concept-page candidates this turn — the candidate query and the three
599
663
  // simBatch queries all return empty. The skill pipeline is mocked to
600
664
  // surface a single skill.
@@ -624,8 +688,8 @@ describe("injectMemoryV2Block", () => {
624
688
  expect(result.toInject).toEqual([]);
625
689
  expect(result.block).not.toBeNull();
626
690
  // Same outer wrapping as concept-page-only blocks.
627
- expect(result.block).toContain("<memory __injected>");
628
- expect(result.block).toContain("## What I Remember Right Now");
691
+ expect(result.block).toContain("<memory>");
692
+ expect(result.block).not.toContain("## What I Remember Right Now");
629
693
  expect(result.block).toContain("</memory>");
630
694
  // No concept-page sections; skills subsection present with the right
631
695
  // bullet shape and the unconditional `→ use skill_load to activate` suffix.
@@ -757,6 +821,95 @@ describe("injectMemoryV2Block", () => {
757
821
  expect(persisted!.everInjected).toEqual([]);
758
822
  });
759
823
 
824
+ test("context-load mode renders topNow even when every slug was previously injected", async () => {
825
+ // Turn 1 (per-turn): seed alice as injected.
826
+ stageTurn([{ slug: "alice-vscode", denseScore: 0.9 }]);
827
+ await injectMemoryV2Block({
828
+ database: db,
829
+ conversationId: "conv-1",
830
+ currentTurn: 1,
831
+ userMessage: "Alice's editor",
832
+ assistantMessage: "",
833
+ nowText: "Now",
834
+ messageId: "msg-1",
835
+ config: makeConfig(),
836
+ });
837
+
838
+ // Subsequent context-load (post-compaction or fresh load): alice is
839
+ // back in the candidate pool. Per-turn would dedup against everInjected
840
+ // and produce a null block; context-load must re-render the full top-K
841
+ // because cached attachments don't exist on a fresh load.
842
+ stageTurn([{ slug: "alice-vscode", denseScore: 0.9 }]);
843
+ const result = await injectMemoryV2Block({
844
+ database: db,
845
+ conversationId: "conv-1",
846
+ currentTurn: 2,
847
+ userMessage: "Reload context",
848
+ assistantMessage: "",
849
+ nowText: "Now",
850
+ messageId: "msg-2",
851
+ mode: "context-load",
852
+ config: makeConfig(),
853
+ });
854
+
855
+ expect(result.block).not.toBeNull();
856
+ expect(result.block).toContain("### alice-vscode");
857
+ // No newly-injected slug — alice was already in everInjected.
858
+ expect(result.toInject).toEqual([]);
859
+
860
+ // everInjected stays a single entry (alice was already there) — context-
861
+ // load doesn't double-stamp.
862
+ const persisted = await hydrate(db, "conv-1");
863
+ expect(persisted!.everInjected).toEqual([
864
+ { slug: "alice-vscode", turn: 1 },
865
+ ]);
866
+ });
867
+
868
+ test("context-load mode renders the full top-K on a fresh first turn", async () => {
869
+ // Turn 1 with no prior state and three candidates. Per-turn and context-
870
+ // load behave identically when everInjected is empty (toInject == topNow),
871
+ // but this asserts the contract: a fresh first user message gets the
872
+ // entire top-K rendered, not just a delta.
873
+ stageTurn([
874
+ { slug: "alice-vscode", denseScore: 0.9 },
875
+ { slug: "bob-coffee", denseScore: 0.8 },
876
+ { slug: "carol-jazz", denseScore: 0.7 },
877
+ ]);
878
+
879
+ const result = await injectMemoryV2Block({
880
+ database: db,
881
+ conversationId: "conv-1",
882
+ currentTurn: 1,
883
+ userMessage: "hi",
884
+ assistantMessage: "",
885
+ nowText: "",
886
+ messageId: "msg-1",
887
+ mode: "context-load",
888
+ config: makeConfig({ top_k: 3 }),
889
+ });
890
+
891
+ expect(result.block).not.toBeNull();
892
+ expect(result.block).toContain("### alice-vscode");
893
+ expect(result.block).toContain("### bob-coffee");
894
+ expect(result.block).toContain("### carol-jazz");
895
+ // The seeded directed edges (alice→bob, bob→alice, frontmatter-demo→alice)
896
+ // mean alice has two incoming predecessors and bob has one, so directed
897
+ // spread normalizes alice's activation more aggressively than bob's. The
898
+ // resulting rank order is bob > carol (no predecessors) > alice.
899
+ expect(new Set(result.toInject)).toEqual(
900
+ new Set(["alice-vscode", "bob-coffee", "carol-jazz"]),
901
+ );
902
+ expect(result.toInject).toHaveLength(3);
903
+
904
+ // All three slugs persisted to everInjected so the next per-turn doesn't
905
+ // re-attach the same content.
906
+ const persisted = await hydrate(db, "conv-1");
907
+ expect(new Set(persisted!.everInjected.map((e) => e.slug))).toEqual(
908
+ new Set(["alice-vscode", "bob-coffee", "carol-jazz"]),
909
+ );
910
+ expect(persisted!.everInjected).toHaveLength(3);
911
+ });
912
+
760
913
  test("`top_k_skills: 0` short-circuits to no skills subsection", async () => {
761
914
  // Even when the underlying mock would surface skills, the cap at 0 must
762
915
  // drop them via `selectSkillInjections.topK = 0` → empty `topNow`.
@@ -789,4 +942,220 @@ describe("injectMemoryV2Block", () => {
789
942
  expect(result.block).not.toContain("### Skills You Can Use");
790
943
  expect(result.block).not.toContain("example-skill-a");
791
944
  });
945
+
946
+ // ---------------------------------------------------------------------------
947
+ // Activation-log telemetry
948
+ // ---------------------------------------------------------------------------
949
+
950
+ test("writes one activation-log row per turn with concept rows partitioned and sorted", async () => {
951
+ // Turn 1: seed alice as injected so turn 2 has an `in_context` candidate.
952
+ stageTurn([{ slug: "alice-vscode", denseScore: 0.9 }]);
953
+ await injectMemoryV2Block({
954
+ database: db,
955
+ conversationId: "conv-1",
956
+ currentTurn: 1,
957
+ userMessage: "Alice's editor",
958
+ assistantMessage: "",
959
+ nowText: "Now",
960
+ messageId: "msg-1",
961
+ config: makeConfig(),
962
+ });
963
+ expect(telemetryState.recordCalls.length).toBe(1);
964
+
965
+ // Turn 2: alice carries forward (now `in_context`); carol is freshly
966
+ // surfaced (`injected`); bob would be a candidate only if it carried
967
+ // forward, but with no prior bob entry it doesn't appear here.
968
+ stageTurn([
969
+ { slug: "alice-vscode", denseScore: 0.6 },
970
+ { slug: "carol-jazz", denseScore: 0.95 },
971
+ ]);
972
+ await injectMemoryV2Block({
973
+ database: db,
974
+ conversationId: "conv-1",
975
+ currentTurn: 2,
976
+ userMessage: "Carol's music",
977
+ assistantMessage: "",
978
+ nowText: "Now",
979
+ messageId: "msg-2",
980
+ config: makeConfig(),
981
+ });
982
+
983
+ expect(telemetryState.recordCalls.length).toBe(2);
984
+ const row = telemetryState.recordCalls[1] as {
985
+ conversationId: string;
986
+ turn: number;
987
+ mode: string;
988
+ concepts: Array<{
989
+ slug: string;
990
+ finalActivation: number;
991
+ status: string;
992
+ source: string;
993
+ }>;
994
+ skills: unknown[];
995
+ config: { top_k: number };
996
+ };
997
+ expect(row.conversationId).toBe("conv-1");
998
+ expect(row.turn).toBe(2);
999
+ expect(row.mode).toBe("per-turn");
1000
+ expect(row.config.top_k).toBe(20);
1001
+
1002
+ // The candidate set is the union of fromPrior (alice) and fromAnn
1003
+ // (alice + carol) → two concept rows.
1004
+ expect(row.concepts.length).toBe(2);
1005
+ const slugs = row.concepts.map((c) => c.slug);
1006
+ expect(new Set(slugs)).toEqual(new Set(["alice-vscode", "carol-jazz"]));
1007
+
1008
+ // Sorted descending by finalActivation.
1009
+ for (let i = 1; i < row.concepts.length; i++) {
1010
+ expect(row.concepts[i - 1]!.finalActivation).toBeGreaterThanOrEqual(
1011
+ row.concepts[i]!.finalActivation,
1012
+ );
1013
+ }
1014
+
1015
+ const byslug = new Map(row.concepts.map((c) => [c.slug, c]));
1016
+ // Alice was attached on turn 1 → status `in_context` on turn 2.
1017
+ expect(byslug.get("alice-vscode")!.status).toBe("in_context");
1018
+ // Carol is freshly injected on turn 2.
1019
+ expect(byslug.get("carol-jazz")!.status).toBe("injected");
1020
+ });
1021
+
1022
+ test("context-load mode marks every rendered slug as `injected`, never `in_context`", async () => {
1023
+ // Turn 1 (per-turn): seed alice as injected so the next turn's prior
1024
+ // `everInjected` includes her — the same setup the per-turn telemetry
1025
+ // test uses, so the difference between modes is unambiguous.
1026
+ stageTurn([{ slug: "alice-vscode", denseScore: 0.9 }]);
1027
+ await injectMemoryV2Block({
1028
+ database: db,
1029
+ conversationId: "conv-1",
1030
+ currentTurn: 1,
1031
+ userMessage: "Alice's editor",
1032
+ assistantMessage: "",
1033
+ nowText: "Now",
1034
+ messageId: "msg-1",
1035
+ config: makeConfig(),
1036
+ });
1037
+ expect(telemetryState.recordCalls.length).toBe(1);
1038
+
1039
+ // Turn 2 in context-load mode (post-compaction or fresh load). Alice
1040
+ // carries forward AND ranks high again; carol is a brand-new candidate.
1041
+ // Both end up in `topNow` (and therefore in `slugsToRender` since
1042
+ // context-load renders the full top-K). The status field must reflect
1043
+ // that they were physically rendered into the new user message on this
1044
+ // turn — `injected` for both — rather than reading `in_context` for
1045
+ // alice based on stale prior `everInjected` state.
1046
+ stageTurn([
1047
+ { slug: "alice-vscode", denseScore: 0.6 },
1048
+ { slug: "carol-jazz", denseScore: 0.95 },
1049
+ ]);
1050
+ await injectMemoryV2Block({
1051
+ database: db,
1052
+ conversationId: "conv-1",
1053
+ currentTurn: 2,
1054
+ userMessage: "Reload context",
1055
+ assistantMessage: "",
1056
+ nowText: "Now",
1057
+ messageId: "msg-2",
1058
+ mode: "context-load",
1059
+ config: makeConfig(),
1060
+ });
1061
+
1062
+ expect(telemetryState.recordCalls.length).toBe(2);
1063
+ const row = telemetryState.recordCalls[1] as {
1064
+ mode: string;
1065
+ concepts: Array<{ slug: string; status: string }>;
1066
+ };
1067
+ expect(row.mode).toBe("context-load");
1068
+
1069
+ const byslug = new Map(row.concepts.map((c) => [c.slug, c]));
1070
+ // Both rendered slugs read as `injected` — alice especially, even though
1071
+ // she's in prior `everInjected`, because context-load actually rendered
1072
+ // her into the fresh user message on this turn.
1073
+ expect(byslug.get("alice-vscode")!.status).toBe("injected");
1074
+ expect(byslug.get("carol-jazz")!.status).toBe("injected");
1075
+
1076
+ // No slug reads as `in_context` in context-load mode — the cache was
1077
+ // wiped, so there is no prior cached attachment to reference.
1078
+ for (const concept of row.concepts) {
1079
+ expect(concept.status).not.toBe("in_context");
1080
+ }
1081
+ });
1082
+
1083
+ test("context-load mode marks candidates outside `slugsToRender` as `not_injected`", async () => {
1084
+ // Turn 1 (per-turn): seed both alice and bob with positive activation
1085
+ // so they survive into turn 2's prior-state candidate pool.
1086
+ stageTurn([
1087
+ { slug: "alice-vscode", denseScore: 0.9 },
1088
+ { slug: "bob-coffee", denseScore: 0.8 },
1089
+ ]);
1090
+ await injectMemoryV2Block({
1091
+ database: db,
1092
+ conversationId: "conv-1",
1093
+ currentTurn: 1,
1094
+ userMessage: "Alice's editor and Bob's coffee",
1095
+ assistantMessage: "",
1096
+ nowText: "Now",
1097
+ messageId: "msg-1",
1098
+ config: makeConfig(),
1099
+ });
1100
+
1101
+ // Turn 2 (context-load) with `top_k: 1`: alice and bob both carry
1102
+ // forward as candidates, but only the top-ranked slug is rendered.
1103
+ // Whichever slug doesn't make the cut must read as `not_injected`.
1104
+ stageTurn([
1105
+ { slug: "alice-vscode", denseScore: 0.95 },
1106
+ { slug: "bob-coffee", denseScore: 0.05 },
1107
+ ]);
1108
+ await injectMemoryV2Block({
1109
+ database: db,
1110
+ conversationId: "conv-1",
1111
+ currentTurn: 2,
1112
+ userMessage: "Reload context",
1113
+ assistantMessage: "",
1114
+ nowText: "Now",
1115
+ messageId: "msg-2",
1116
+ mode: "context-load",
1117
+ config: makeConfig({ top_k: 1 }),
1118
+ });
1119
+
1120
+ expect(telemetryState.recordCalls.length).toBe(2);
1121
+ const row = telemetryState.recordCalls[1] as {
1122
+ mode: string;
1123
+ concepts: Array<{ slug: string; status: string }>;
1124
+ };
1125
+ expect(row.mode).toBe("context-load");
1126
+
1127
+ const byslug = new Map(row.concepts.map((c) => [c.slug, c]));
1128
+ // Alice ranked first → she is in `slugsToRender` → `injected`.
1129
+ expect(byslug.get("alice-vscode")!.status).toBe("injected");
1130
+ // Bob was a candidate but didn't make `top_k: 1` → `not_injected`.
1131
+ expect(byslug.get("bob-coffee")!.status).toBe("not_injected");
1132
+ });
1133
+
1134
+ test("telemetry write failure is non-fatal — injection still returns a normal result", async () => {
1135
+ telemetryState.recordShouldThrow = true;
1136
+
1137
+ stageTurn([{ slug: "alice-vscode", denseScore: 0.9 }]);
1138
+ const result = await injectMemoryV2Block({
1139
+ database: db,
1140
+ conversationId: "conv-1",
1141
+ currentTurn: 1,
1142
+ userMessage: "Alice's editor",
1143
+ assistantMessage: "",
1144
+ nowText: "Now",
1145
+ messageId: "msg-1",
1146
+ config: makeConfig(),
1147
+ });
1148
+
1149
+ // No row captured (the throw aborted the push), but the caller still
1150
+ // produced a regular block + toInject result and persisted state.
1151
+ expect(telemetryState.recordCalls.length).toBe(0);
1152
+ expect(result.toInject).toEqual(["alice-vscode"]);
1153
+ expect(result.block).not.toBeNull();
1154
+ expect(result.block).toContain("### alice-vscode");
1155
+
1156
+ const persisted = await hydrate(db, "conv-1");
1157
+ expect(persisted!.everInjected).toEqual([
1158
+ { slug: "alice-vscode", turn: 1 },
1159
+ ]);
1160
+ });
792
1161
  });
@@ -546,7 +546,7 @@ describe("derivePromotions", () => {
546
546
  // ---------------------------------------------------------------------------
547
547
 
548
548
  describe("collapseEdges", () => {
549
- test("maps v1 ids to v2 slugs and emits canonical tuples after writeEdges", () => {
549
+ test("maps v1 ids to v2 slugs and preserves direction (source target)", () => {
550
550
  const v1: V1Edge[] = [
551
551
  { sourceNodeId: "node-a", targetNodeId: "node-b" },
552
552
  { sourceNodeId: "node-b", targetNodeId: "node-c" },
@@ -556,9 +556,13 @@ describe("collapseEdges", () => {
556
556
  ["node-b", "bob"],
557
557
  ["node-c", "carol"],
558
558
  ]);
559
- const idx = collapseEdges(v1, slugMap);
560
- expect(idx.version).toBe(1);
561
- expect(idx.edges.length).toBe(2);
559
+ const outgoing = collapseEdges(v1, slugMap);
560
+ // Two distinct sources, each pointing at one target.
561
+ expect(outgoing.size).toBe(2);
562
+ expect([...(outgoing.get("alice") ?? new Set<string>())]).toEqual(["bob"]);
563
+ expect([...(outgoing.get("bob") ?? new Set<string>())]).toEqual(["carol"]);
564
+ // Direction is preserved — carol's outgoing list is empty (it's a sink).
565
+ expect(outgoing.has("carol")).toBe(false);
562
566
  });
563
567
 
564
568
  test("drops edges whose endpoints aren't in the slug map", () => {
@@ -570,21 +574,48 @@ describe("collapseEdges", () => {
570
574
  ["node-a", "alice"],
571
575
  ["node-b", "bob"],
572
576
  ]);
573
- const idx = collapseEdges(v1, slugMap);
574
- expect(idx.edges).toEqual([]);
577
+ const outgoing = collapseEdges(v1, slugMap);
578
+ expect(outgoing.size).toBe(0);
575
579
  });
576
580
 
577
581
  test("drops self-loops introduced by the slug mapping", () => {
578
582
  // Two distinct v1 nodes mapped to the same v2 slug (e.g. clustered
579
- // together) cannot produce an edge — `addEdge`/writeEdges reject self
580
- // loops, so we must filter at collapse time.
583
+ // together) cannot produce a self-edge — concept-page graphs are simple,
584
+ // so we filter at collapse time.
581
585
  const v1: V1Edge[] = [{ sourceNodeId: "node-a", targetNodeId: "node-b" }];
582
586
  const slugMap = new Map([
583
587
  ["node-a", "merged"],
584
588
  ["node-b", "merged"],
585
589
  ]);
586
- const idx = collapseEdges(v1, slugMap);
587
- expect(idx.edges).toEqual([]);
590
+ const outgoing = collapseEdges(v1, slugMap);
591
+ expect(outgoing.size).toBe(0);
592
+ });
593
+
594
+ test("collapses duplicate (source, target) pairs into a single entry", () => {
595
+ const v1: V1Edge[] = [
596
+ { sourceNodeId: "node-a", targetNodeId: "node-b" },
597
+ { sourceNodeId: "node-a", targetNodeId: "node-b" },
598
+ ];
599
+ const slugMap = new Map([
600
+ ["node-a", "alice"],
601
+ ["node-b", "bob"],
602
+ ]);
603
+ const outgoing = collapseEdges(v1, slugMap);
604
+ expect([...(outgoing.get("alice") ?? new Set<string>())]).toEqual(["bob"]);
605
+ });
606
+
607
+ test("keeps A→B and B→A as separate directed edges", () => {
608
+ const v1: V1Edge[] = [
609
+ { sourceNodeId: "node-a", targetNodeId: "node-b" },
610
+ { sourceNodeId: "node-b", targetNodeId: "node-a" },
611
+ ];
612
+ const slugMap = new Map([
613
+ ["node-a", "alice"],
614
+ ["node-b", "bob"],
615
+ ]);
616
+ const outgoing = collapseEdges(v1, slugMap);
617
+ expect([...(outgoing.get("alice") ?? new Set<string>())]).toEqual(["bob"]);
618
+ expect([...(outgoing.get("bob") ?? new Set<string>())]).toEqual(["alice"]);
588
619
  });
589
620
  });
590
621
 
@@ -648,13 +679,22 @@ describe("runMemoryV2Migration", () => {
648
679
  expect(pages.length).toBe(3);
649
680
  expect(result.pagesCreated).toBe(3);
650
681
 
651
- // -- edges.json reflects the v1 edge collapse. --
652
- const edgesRaw = readFileSync(
653
- join(workspaceDir, "memory", "edges.json"),
654
- "utf-8",
655
- );
656
- const edges = JSON.parse(edgesRaw) as { edges: [string, string][] };
657
- expect(edges.edges.length).toBe(1);
682
+ // -- Outgoing edges live in source-page frontmatter (no edges.json). --
683
+ expect(existsSync(join(workspaceDir, "memory", "edges.json"))).toBe(false);
684
+ expect(result.edgesWritten).toBe(1);
685
+ // Find the page whose frontmatter has the outgoing edge — that's the
686
+ // source slug. Exactly one page should carry the surviving v1 edge.
687
+ let pagesWithEdges = 0;
688
+ for (const file of pages) {
689
+ const body = readFileSync(join(conceptDir, file), "utf-8");
690
+ if (
691
+ /\nedges:\s*\n?\s*-\s*/.test(body) ||
692
+ /edges:\s*\[[^\]]+\]/.test(body)
693
+ ) {
694
+ pagesWithEdges += 1;
695
+ }
696
+ }
697
+ expect(pagesWithEdges).toBe(1);
658
698
 
659
699
  // -- Promotions appended to the right files. --
660
700
  const essentials = readFileSync(
@@ -664,23 +704,13 @@ describe("runMemoryV2Migration", () => {
664
704
  expect(essentials).toContain("User is Alice and works at Vellum");
665
705
  expect(result.essentialsLines).toBe(1);
666
706
 
667
- // -- Embed jobs enqueued (one per page) plus a single trailing
668
- // rebuild-edges enqueue so page frontmatter picks up edges.json. --
669
- expect(enqueuedJobs.length).toBe(4);
707
+ // -- Embed jobs enqueued (one per page). No rebuild-edges follow-up:
708
+ // the migration writes outgoing edges directly into page frontmatter. --
709
+ expect(enqueuedJobs.length).toBe(3);
670
710
  const embedJobs = enqueuedJobs.filter(
671
711
  (j) => j.type === "embed_concept_page",
672
712
  );
673
713
  expect(embedJobs.length).toBe(3);
674
- const rebuildJobs = enqueuedJobs.filter(
675
- (j) => j.type === "memory_v2_rebuild_edges",
676
- );
677
- expect(rebuildJobs.length).toBe(1);
678
- // Rebuild-edges must come last — the embeds depend only on pages on disk,
679
- // but rebuild-edges should observe the final edges.json from this run.
680
- expect(enqueuedJobs[enqueuedJobs.length - 1].type).toBe(
681
- "memory_v2_rebuild_edges",
682
- );
683
- expect(result.rebuildEdgesJobId).toBe(`job-${enqueuedJobs.length}`);
684
714
 
685
715
  // -- Sentinel written. --
686
716
  expect(existsSync(join(workspaceDir, MIGRATION_SENTINEL_RELATIVE))).toBe(
@@ -785,13 +815,11 @@ describe("runMemoryV2Migration", () => {
785
815
  });
786
816
  expect(result.pagesCreated).toBe(0);
787
817
  expect(result.embedsEnqueued).toBe(0);
818
+ expect(result.edgesWritten).toBe(0);
788
819
  expect(result.sentinelWritten).toBe(true);
789
- // Rebuild-edges still runs even with zero pages the job is idempotent
790
- // and ensures any pre-seeded frontmatter aligns with edges.json.
791
- expect(result.rebuildEdgesJobId).toBeTruthy();
792
- expect(
793
- enqueuedJobs.filter((j) => j.type === "memory_v2_rebuild_edges").length,
794
- ).toBe(1);
820
+ // No rebuild-edges follow-up outgoing edges live directly in page
821
+ // frontmatter, so a workspace with no pages has nothing to rebuild.
822
+ expect(enqueuedJobs).toEqual([]);
795
823
  });
796
824
  });
797
825