@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
@@ -15,8 +15,8 @@
15
15
  * substitute in.
16
16
  *
17
17
  * Lifecycle:
18
- * 1. Bail if the `memory-v2-enabled` feature flag is off (the worker may
19
- * have claimed a stale row at flag-flip time).
18
+ * 1. Bail if `config.memory.v2.enabled` is false (the worker may have
19
+ * claimed a stale row from before v2 was disabled).
20
20
  * 2. Acquire a single-process lock at `memory/.v2-state/consolidation.lock`
21
21
  * so two overlapping schedule windows can't fight over the same files.
22
22
  * The lock contains the holder's PID + timestamp so a crashed run leaves
@@ -53,12 +53,12 @@ import {
53
53
  } from "node:fs";
54
54
  import { dirname, join } from "node:path";
55
55
 
56
- import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
57
56
  import type { AssistantConfig } from "../../config/types.js";
58
57
  import { INTERNAL_GUARDIAN_TRUST_CONTEXT } from "../../daemon/trust-context.js";
59
58
  import { wakeAgentForOpportunity } from "../../runtime/agent-wake.js";
60
59
  import { getLogger } from "../../util/logger.js";
61
60
  import { getWorkspaceDir } from "../../util/platform.js";
61
+ import { isProcessAlive } from "../../util/process-liveness.js";
62
62
  import { bootstrapConversation } from "../conversation-bootstrap.js";
63
63
  import { deleteConversation } from "../conversation-crud.js";
64
64
  import {
@@ -84,11 +84,11 @@ const FOLLOW_UP_JOB_TYPES: readonly MemoryJobType[] = [
84
84
 
85
85
  /**
86
86
  * Job handler. See file header for the full lifecycle. Returns a discriminated
87
- * union so tests can assert on the path taken (flag-off / locked / empty /
87
+ * union so tests can assert on the path taken (disabled / locked / empty /
88
88
  * invoked) without having to spy on the filesystem.
89
89
  */
90
90
  export type ConsolidationOutcome =
91
- | { kind: "flag_off" }
91
+ | { kind: "disabled" }
92
92
  | { kind: "locked"; holder: string }
93
93
  | { kind: "empty_buffer" }
94
94
  | { kind: "wake_failed"; reason?: string }
@@ -103,9 +103,9 @@ export async function memoryV2ConsolidateJob(
103
103
  _job: MemoryJob,
104
104
  config: AssistantConfig,
105
105
  ): Promise<ConsolidationOutcome> {
106
- if (!isAssistantFeatureFlagEnabled("memory-v2-enabled", config)) {
107
- log.debug("memory-v2-enabled flag off; consolidation skipped");
108
- return { kind: "flag_off" };
106
+ if (!config.memory.v2.enabled) {
107
+ log.debug("memory.v2.enabled is false; consolidation skipped");
108
+ return { kind: "disabled" };
109
109
  }
110
110
 
111
111
  const memoryDir = join(getWorkspaceDir(), "memory");
@@ -240,14 +240,20 @@ function readBufferContent(bufferPath: string): string {
240
240
  /**
241
241
  * Atomically create the lock file with `wx` (O_CREAT | O_EXCL) flags. Returns
242
242
  * `null` on success, or the current holder string (file contents, typically
243
- * `pid timestamp`) when the file already exists the holder is surfaced for
244
- * log diagnostics so operators can identify a stuck lock without re-reading.
243
+ * `pid timestamp`) when the file already exists and the holder is still alive.
245
244
  *
246
- * Crash recovery: if the prior daemon died with the lock held, the file will
247
- * still be on disk on the next start. PR 20 keeps the lock simple per the
248
- * plan instructions; a future iteration can probe liveness via `kill(pid, 0)`
249
- * the way `snapshot-lock.ts` does. Until then, an operator can clear a
250
- * stale lock by removing the file.
245
+ * Stale-lock takeover: if the file exists but its holder PID is not running,
246
+ * unlink the stale file and retry the create exactly once. This recovers
247
+ * automatically from a crashed daemon that died with the lock held —
248
+ * otherwise every subsequent scheduled consolidation would skip with `locked`
249
+ * indefinitely until an operator manually removed the file.
250
+ *
251
+ * The simple takeover-then-retry is safe here (unlike `snapshot-lock.ts`'s
252
+ * full rename-aside dance) because only the assistant's jobs worker calls
253
+ * this lock, and at most one assistant process runs per workspace at any
254
+ * time. A holder with an unparseable / empty payload is treated as stale —
255
+ * the only writers ever produce a `<pid> <timestamp>` line, so an
256
+ * unparseable file is corruption from a partial write that crashed.
251
257
  */
252
258
  function tryAcquireLock(lockPath: string): string | null {
253
259
  // The workspace migration seeds `memory/.v2-state/`, but tests and
@@ -255,9 +261,40 @@ function tryAcquireLock(lockPath: string): string | null {
255
261
  // is idempotent, so the call is cheap when the dir already exists.
256
262
  mkdirSync(dirname(lockPath), { recursive: true });
257
263
 
264
+ const firstHolder = tryCreate(lockPath);
265
+ if (firstHolder === null) return null;
266
+ if (!isHolderStale(firstHolder)) return firstHolder;
267
+
268
+ log.info(
269
+ { lockPath, holder: firstHolder },
270
+ "consolidation: taking over stale lock (holder not running)",
271
+ );
272
+ try {
273
+ unlinkSync(lockPath);
274
+ } catch (err) {
275
+ const code = (err as NodeJS.ErrnoException).code;
276
+ if (code !== "ENOENT") {
277
+ log.warn(
278
+ { err, lockPath },
279
+ "consolidation: failed to unlink stale lock; reporting as locked",
280
+ );
281
+ return firstHolder;
282
+ }
283
+ }
284
+ // After unlink, the next `wx` create should succeed. If a third party
285
+ // raced in and re-acquired (vanishingly unlikely with one writer per
286
+ // workspace), surface their holder string rather than overwriting.
287
+ return tryCreate(lockPath);
288
+ }
289
+
290
+ /**
291
+ * Atomically create the lock file. Returns `null` on success, or the holder
292
+ * string read from the file when it already exists (`"unknown"` if the read
293
+ * itself fails). Rethrows any non-EEXIST errno from `openSync`.
294
+ */
295
+ function tryCreate(lockPath: string): string | null {
258
296
  let fd: number;
259
297
  try {
260
- // `wx` = create-if-not-exists, fail with EEXIST if it does.
261
298
  fd = openSync(lockPath, "wx");
262
299
  } catch (err) {
263
300
  if ((err as NodeJS.ErrnoException).code !== "EEXIST") throw err;
@@ -267,13 +304,10 @@ function tryAcquireLock(lockPath: string): string | null {
267
304
  return "unknown";
268
305
  }
269
306
  }
270
-
271
- // Best-effort PID + timestamp payload so a stale lock can be diagnosed.
272
- // The worker only cares that the file exists; the contents are advisory.
273
307
  try {
274
308
  writeSync(fd, `${process.pid} ${Date.now()}\n`);
275
309
  } catch {
276
- // best-effort
310
+ // best-effort — payload is advisory, the file's existence is the lock
277
311
  } finally {
278
312
  try {
279
313
  closeSync(fd);
@@ -284,6 +318,21 @@ function tryAcquireLock(lockPath: string): string | null {
284
318
  return null;
285
319
  }
286
320
 
321
+ /**
322
+ * A holder string is stale when its PID parses to a non-running process.
323
+ * The payload format is `<pid> <timestamp>` (see `tryCreate`'s write), but
324
+ * an unparseable / empty / `"unknown"` payload is also treated as stale:
325
+ * the only writer is `tryCreate` itself, so corruption indicates a partial
326
+ * write from a crashed prior holder rather than a live writer mid-flush.
327
+ */
328
+ function isHolderStale(holder: string): boolean {
329
+ const match = /^\d+/.exec(holder);
330
+ if (!match) return true;
331
+ const pid = Number.parseInt(match[0], 10);
332
+ if (!Number.isFinite(pid) || pid <= 0) return true;
333
+ return !isProcessAlive(pid);
334
+ }
335
+
287
336
  /**
288
337
  * Idempotent unlink of the lock file. Called from the `finally` block so a
289
338
  * crash in the wake path doesn't leave the lock stranded. ENOENT is swallowed
@@ -29,21 +29,18 @@ import { getWorkspaceDir } from "../../util/platform.js";
29
29
  import type { DrizzleDb } from "../db-connection.js";
30
30
  import {
31
31
  type MemoryV2ConceptRowRecord,
32
- type MemoryV2SkillRowRecord,
33
32
  recordMemoryV2ActivationLog,
34
33
  } from "../memory-v2-activation-log-store.js";
35
34
  import {
36
35
  computeOwnActivation,
37
- computeSkillActivation,
38
36
  selectCandidates,
39
37
  selectInjections,
40
- selectSkillInjections,
41
38
  spreadActivation,
42
39
  } from "./activation.js";
43
40
  import { hydrate, save } from "./activation-store.js";
44
41
  import { getEdgeIndex } from "./edge-index.js";
45
42
  import { readPage, renderPageContent } from "./page-store.js";
46
- import { getAllSkillIds, getSkillCapability } from "./skill-store.js";
43
+ import { getSkillCapability, isSkillSlug } from "./skill-store.js";
47
44
  import type { ActivationState, EverInjectedEntry } from "./types.js";
48
45
 
49
46
  const log = getLogger("memory-v2-injection");
@@ -84,6 +81,7 @@ export interface InjectMemoryV2BlockParams {
84
81
  */
85
82
  mode?: InjectMemoryV2Mode;
86
83
  config: AssistantConfig;
84
+ signal?: AbortSignal;
87
85
  }
88
86
 
89
87
  export interface InjectMemoryV2BlockResult {
@@ -127,30 +125,36 @@ export async function injectMemoryV2Block(
127
125
  nowText,
128
126
  messageId,
129
127
  config,
128
+ signal,
130
129
  } = params;
131
130
 
132
131
  const workspaceDir = getWorkspaceDir();
133
132
 
134
133
  // (1) Hydrate. Missing rows are normal at conversation start — proceed
135
134
  // with an effective empty prior state so the first turn can still inject.
135
+ throwIfAborted(signal);
136
136
  const priorState = await hydrate(database, conversationId);
137
137
 
138
138
  // (2) Topology. `getEdgeIndex` walks concept-page frontmatter and caches
139
139
  // the result module-locally; an empty workspace yields an empty index.
140
+ throwIfAborted(signal);
140
141
  const edgeIndex = await getEdgeIndex(workspaceDir);
141
142
 
142
143
  // (3) Candidate set: prior-state survivors above epsilon ∪ ANN top-50.
143
144
  // `selectCandidates` also returns `fromPrior` / `fromAnn` provenance sets so
144
145
  // telemetry can attribute each candidate back to its source.
146
+ throwIfAborted(signal);
145
147
  const { candidates, fromPrior, fromAnn } = await selectCandidates({
146
148
  priorState,
147
149
  userText: userMessage,
148
150
  assistantText: assistantMessage,
149
151
  nowText,
150
152
  config,
153
+ signal,
151
154
  });
152
155
 
153
156
  // (4) Own activation: A_o = d·prev + c_user·sim_u + c_a·sim_a + c_now·sim_n.
157
+ throwIfAborted(signal);
154
158
  const { activation: ownActivation, breakdown: ownBreakdown } =
155
159
  await computeOwnActivation({
156
160
  candidates,
@@ -159,9 +163,11 @@ export async function injectMemoryV2Block(
159
163
  assistantText: assistantMessage,
160
164
  nowText,
161
165
  config,
166
+ signal,
162
167
  });
163
168
 
164
169
  // (5) Spreading activation across the edge graph (k, hops from config).
170
+ throwIfAborted(signal);
165
171
  const { k, hops, top_k, epsilon } = config.memory.v2;
166
172
  const { final: finalActivation, contribution: spreadContribution } =
167
173
  spreadActivation(ownActivation, edgeIndex, k, hops);
@@ -182,25 +188,6 @@ export async function injectMemoryV2Block(
182
188
  });
183
189
  const slugsToRender = mode === "context-load" ? topNow : toInject;
184
190
 
185
- // (6b) Skill pipeline — a sibling pipeline to the concept-page one above.
186
- // Skills are stateless (no decay, no spread, no `everInjected` dedup) and
187
- // the catalog is small, so every known skill is scored every turn. The
188
- // top-K injection slate is re-presented every turn so the agent can drop
189
- // and pick skills up freely; the inspector renders the full ranked list.
190
- const skillCandidates = new Set(getAllSkillIds());
191
- const { activation: skillActivation, breakdown: skillBreakdown } =
192
- await computeSkillActivation({
193
- candidates: skillCandidates,
194
- userText: userMessage,
195
- assistantText: assistantMessage,
196
- nowText,
197
- config,
198
- });
199
- const { topNow: topSkillIds } = selectSkillInjections({
200
- A: skillActivation,
201
- topK: config.memory.v2.top_k_skills,
202
- });
203
-
204
191
  // Build the next persisted state regardless of whether we render anything:
205
192
  // even on a "no new injection" turn, prior-state activations decay via the
206
193
  // candidate-set carry-forward and need to be rewritten so `epsilon`-trimmed
@@ -215,8 +202,10 @@ export async function injectMemoryV2Block(
215
202
  // just rendered all of them); on per-turn it's just the newly added slugs.
216
203
  // We append rather than reset so that compaction-driven eviction
217
204
  // (`evictCompactedTurns`) is the only path that can re-enable a previously-
218
- // injected slug. Skills do NOT enter `everInjected` they are stateless
219
- // and re-presented every turn.
205
+ // injected slug. Skill slugs (`skills/<id>`) participate in this dedup just
206
+ // like concept slugs — once attached on a turn, the cached attachment lives
207
+ // on that user message and the agent keeps seeing it across subsequent turns
208
+ // until compaction evicts the turn.
220
209
  const everInjectedSet = new Set(priorEverInjected.map((entry) => entry.slug));
221
210
  const newlyInjected = slugsToRender.filter(
222
211
  (slug) => !everInjectedSet.has(slug),
@@ -243,7 +232,6 @@ export async function injectMemoryV2Block(
243
232
  const { block, missingSlugs } = await renderInjectionBlock(
244
233
  workspaceDir,
245
234
  slugsToRender,
246
- topSkillIds,
247
235
  );
248
236
  const missingSlugSet = new Set(missingSlugs);
249
237
  if (missingSlugs.length > 0) {
@@ -262,7 +250,6 @@ export async function injectMemoryV2Block(
262
250
  // block memory injection.
263
251
  const toInjectSet = new Set(toInject);
264
252
  const renderedSet = new Set(slugsToRender);
265
- const topSkillIdSet = new Set(topSkillIds);
266
253
  const conceptRows: MemoryV2ConceptRowRecord[] = [...candidates].map(
267
254
  (slug) => {
268
255
  const breakdown = ownBreakdown.get(slug);
@@ -301,6 +288,9 @@ export async function injectMemoryV2Block(
301
288
  simUser: breakdown?.simUser ?? 0,
302
289
  simAssistant: breakdown?.simAssistant ?? 0,
303
290
  simNow: breakdown?.simNow ?? 0,
291
+ simUserRerankBoost: breakdown?.simUserRerankBoost ?? 0,
292
+ simAssistantRerankBoost: breakdown?.simAssistantRerankBoost ?? 0,
293
+ inRerankPool: breakdown?.inRerankPool ?? false,
304
294
  spreadContribution: spreadContribution.get(slug) ?? 0,
305
295
  source:
306
296
  inPrior && inAnn ? "both" : inPrior ? "prior_state" : "ann_top50",
@@ -310,19 +300,6 @@ export async function injectMemoryV2Block(
310
300
  );
311
301
  conceptRows.sort((a, b) => b.finalActivation - a.finalActivation);
312
302
 
313
- const skillRows: MemoryV2SkillRowRecord[] = [...skillCandidates].map((id) => {
314
- const breakdown = skillBreakdown.get(id);
315
- return {
316
- id,
317
- activation: skillActivation.get(id) ?? 0,
318
- simUser: breakdown?.simUser ?? 0,
319
- simAssistant: breakdown?.simAssistant ?? 0,
320
- simNow: breakdown?.simNow ?? 0,
321
- status: topSkillIdSet.has(id) ? "injected" : "not_injected",
322
- };
323
- });
324
- skillRows.sort((a, b) => b.activation - a.activation);
325
-
326
303
  const v2Cfg = config.memory.v2;
327
304
  try {
328
305
  recordMemoryV2ActivationLog({
@@ -330,7 +307,6 @@ export async function injectMemoryV2Block(
330
307
  turn: currentTurn,
331
308
  mode,
332
309
  concepts: conceptRows,
333
- skills: skillRows,
334
310
  config: {
335
311
  d: v2Cfg.d,
336
312
  c_user: v2Cfg.c_user,
@@ -339,7 +315,6 @@ export async function injectMemoryV2Block(
339
315
  k: v2Cfg.k,
340
316
  hops: v2Cfg.hops,
341
317
  top_k: v2Cfg.top_k,
342
- top_k_skills: v2Cfg.top_k_skills,
343
318
  epsilon: v2Cfg.epsilon,
344
319
  },
345
320
  });
@@ -353,6 +328,12 @@ export async function injectMemoryV2Block(
353
328
  return { block, toInject: newlyInjected };
354
329
  }
355
330
 
331
+ function throwIfAborted(signal: AbortSignal | undefined): void {
332
+ if (signal?.aborted) {
333
+ throw new DOMException("Aborted", "AbortError");
334
+ }
335
+ }
336
+
356
337
  // ---------------------------------------------------------------------------
357
338
  // Internal helpers
358
339
  // ---------------------------------------------------------------------------
@@ -380,9 +361,24 @@ interface RenderInjectionBlockResult {
380
361
  }
381
362
 
382
363
  /**
383
- * Render the inner content of the `<memory>` block for a list of slugs and
384
- * a list of ranked skill ids. The caller wraps the result in
385
- * `<memory>...</memory>` exactly once at injection time.
364
+ * Leading instruction line emitted at the top of every non-empty injection
365
+ * block. Tells the agent that what follows are page summaries and that it
366
+ * should read the underlying file when a summary looks relevant. Pages
367
+ * without a `summary` field render in full instead — the agent treats
368
+ * those as inline content and doesn't need to follow up.
369
+ */
370
+ const INJECTION_HEADER =
371
+ "**CRITICAL:** These are page summaries. Read the page file if it looks relevant.";
372
+
373
+ /**
374
+ * Render the inner content of the `<memory>` block for a list of slugs.
375
+ * The caller wraps the result in `<memory>...</memory>` exactly once at
376
+ * injection time.
377
+ *
378
+ * The slug list is partitioned by prefix: slugs starting with `skills/`
379
+ * resolve to a `SkillEntry` via `getSkillCapability` and render under the
380
+ * trailing `### Skills You Can Use` subsection; everything else is read
381
+ * from disk via `readPage` and rendered as a concept-page section.
386
382
  *
387
383
  * Concept pages are read in parallel via `readPage`. Pages whose file has
388
384
  * gone missing between selection and render (e.g. consolidation deleted
@@ -390,30 +386,31 @@ interface RenderInjectionBlockResult {
390
386
  * block but reported back via `missingSlugs` so callers can surface the
391
387
  * divergence.
392
388
  *
393
- * Skill ids are looked up via `getSkillCapability`. Ids that the cache no
394
- * longer knows (e.g. uninstalled mid-run) are silently dropped, mirroring
395
- * the missing-pages behavior.
389
+ * Skill slugs whose entry the cache no longer knows (e.g. uninstalled
390
+ * mid-run) are silently dropped, mirroring the missing-pages behavior but
391
+ * without entering `missingSlugs` — the skill catalog is the source of
392
+ * truth for skill availability, not on-disk concept pages, so a missing
393
+ * skill is an expected catalog-level outcome rather than a stale-index
394
+ * bug.
395
+ *
396
+ * Each concept-page section is rendered as a path header followed by either
397
+ * the page's `summary` (when present in frontmatter) or the full page (the
398
+ * fallback for pages predating the summary field). Skills sit at the end
399
+ * under `### Skills You Can Use`, unchanged. The leading `**CRITICAL:**`
400
+ * line tells the agent how to read the block.
401
+ *
402
+ * **CRITICAL:** These are page summaries. Read the page file if it looks relevant.
396
403
  *
397
- * The block shape is the §5 layout from the design doc, with an optional
398
- * trailing skills subsection. Each concept-page section reproduces the page
399
- * as it lives on disk — frontmatter (`edges`, `ref_files`) plus body — so
400
- * the agent sees the page's edges and any referenced media paths alongside
401
- * the prose:
404
+ * # memory/concepts/<concept-slug-1>.md
405
+ * <summary-1>
402
406
  *
403
- * ### <slug-1>
407
+ * # memory/concepts/<concept-slug-2>.md
404
408
  * ---
405
409
  * edges:
406
410
  * - <neighbor-slug>
407
411
  * ref_files:
408
412
  * - <path/to/asset>
409
413
  * ---
410
- * <body-1>
411
- *
412
- * ### <slug-2>
413
- * ---
414
- * edges: []
415
- * ref_files: []
416
- * ---
417
414
  * <body-2>
418
415
  *
419
416
  * ### Skills You Can Use
@@ -423,10 +420,12 @@ interface RenderInjectionBlockResult {
423
420
  async function renderInjectionBlock(
424
421
  workspaceDir: string,
425
422
  slugs: string[],
426
- skillIds: string[],
427
423
  ): Promise<RenderInjectionBlockResult> {
424
+ const conceptSlugs = slugs.filter((s) => !isSkillSlug(s));
425
+ const skillSlugs = slugs.filter((s) => isSkillSlug(s));
426
+
428
427
  const pages = await Promise.all(
429
- slugs.map(async (slug) => {
428
+ conceptSlugs.map(async (slug) => {
430
429
  const page = await readPage(workspaceDir, slug);
431
430
  return { slug, page };
432
431
  }),
@@ -439,15 +438,23 @@ async function renderInjectionBlock(
439
438
  missingSlugs.push(slug);
440
439
  continue;
441
440
  }
441
+ const summary = page.frontmatter.summary?.trim();
442
+ const path = `memory/concepts/${slug}.md`;
443
+ if (summary && summary.length > 0) {
444
+ sections.push(`# ${path}\n${summary}`);
445
+ continue;
446
+ }
447
+ // Fallback: page predates the `summary` field (or the field was set to
448
+ // empty). Render the full page — frontmatter + body — so retrieval
449
+ // still surfaces the same content the agent saw before this change.
442
450
  const content = renderPageContent(page).trim();
443
451
  if (content.length === 0) continue;
444
- sections.push(`### ${slug}\n${content}`);
452
+ sections.push(`# ${path}\n${content}`);
445
453
  }
446
454
 
447
- // v2's skills collection is skills-only, so the activation suffix always applies.
448
455
  const skillLines: string[] = [];
449
- for (const id of skillIds) {
450
- const entry = getSkillCapability(id);
456
+ for (const slug of skillSlugs) {
457
+ const entry = getSkillCapability(slug);
451
458
  if (!entry) continue;
452
459
  skillLines.push(`- ${entry.content} → use skill_load to activate`);
453
460
  }
@@ -458,7 +465,7 @@ async function renderInjectionBlock(
458
465
  if (sections.length === 0) return { block: null, missingSlugs };
459
466
 
460
467
  return {
461
- block: sections.join("\n\n"),
468
+ block: `${INJECTION_HEADER}\n\n${sections.join("\n\n")}`,
462
469
  missingSlugs,
463
470
  };
464
471
  }
@@ -338,6 +338,45 @@ export async function listPages(workspaceDir: string): Promise<string[]> {
338
338
  return slugs;
339
339
  }
340
340
 
341
+ /**
342
+ * Cheap "do any concept pages exist?" probe — walks the concepts/ tree only
343
+ * far enough to find one `.md` file and returns immediately. Used by the
344
+ * daemon-startup rebuild gate so the empty-after-create recovery path skips
345
+ * a full enumeration of all 1000+ pages just to ask a yes/no question.
346
+ */
347
+ export async function hasConceptPages(workspaceDir: string): Promise<boolean> {
348
+ const root = getConceptsDir(workspaceDir);
349
+ const queue: string[] = [root];
350
+
351
+ while (queue.length > 0) {
352
+ const dir = queue.shift()!;
353
+ let entries;
354
+ try {
355
+ entries = await readdir(dir, { withFileTypes: true });
356
+ } catch (err) {
357
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
358
+ if (dir === root) return false;
359
+ continue;
360
+ }
361
+ throw err;
362
+ }
363
+
364
+ for (const entry of entries) {
365
+ if (entry.name.startsWith(".")) continue;
366
+ if (entry.isDirectory()) {
367
+ queue.push(join(dir, entry.name));
368
+ continue;
369
+ }
370
+ if (!entry.isFile()) continue;
371
+ if (!entry.name.endsWith(PAGE_EXTENSION)) continue;
372
+ if (entry.name.includes(".tmp.")) continue;
373
+ return true;
374
+ }
375
+ }
376
+
377
+ return false;
378
+ }
379
+
341
380
  /**
342
381
  * Delete a concept page. Idempotent — missing files are not an error.
343
382
  *
@@ -16,7 +16,7 @@
16
16
  * the convention established for the sweep prompt.
17
17
  */
18
18
 
19
- import { readFileSync } from "node:fs";
19
+ import { lstatSync, readFileSync } from "node:fs";
20
20
  import { homedir } from "node:os";
21
21
  import { isAbsolute, join } from "node:path";
22
22
 
@@ -28,6 +28,14 @@ const log = getLogger("memory-v2-consolidate-prompt");
28
28
  /** Sentinel substituted with the cutoff timestamp at runtime. */
29
29
  export const CUTOFF_PLACEHOLDER = "{{CUTOFF}}";
30
30
 
31
+ /**
32
+ * Upper bound for the override file. Real consolidation prompts are kilobytes;
33
+ * 1 MiB is generous headroom while preventing a `settings.write` principal from
34
+ * pointing the field at a multi-gigabyte file (or `/dev/zero`-like stream that
35
+ * `lstat` can't size cap on its own) and exfiltrating it through the wake hint.
36
+ */
37
+ const MAX_PROMPT_BYTES = 1 * 1024 * 1024;
38
+
31
39
  /**
32
40
  * Consolidation prompt — live-mode only. The agent runs as itself (full
33
41
  * SOUL.md + IDENTITY.md + persona + memory autoloads) with the standard
@@ -123,6 +131,7 @@ edges:
123
131
  - path/to/sister
124
132
  - path/to/parent
125
133
  ref_files: []
134
+ summary: 1-4 sentences describing what this article is. Plain prose only — no bullets, no newlines, no markdown lists. Lead with the most identifying detail.
126
135
  ---
127
136
  # title
128
137
 
@@ -132,6 +141,8 @@ ref_files: []
132
141
  - **bullet 2.** ...
133
142
  \`\`\`
134
143
 
144
+ The \`summary\` field is required on every new or updated article. Retrieval injects \`path + summary\` into context — the agent reads the full file only when the summary looks relevant — so make the summary specific and terse. Keep it on a single YAML line (no \`|\` block scalars, no embedded newlines).
145
+
135
146
  **Caps:** ~5-8 bullets per topic/concept article. ~10-12 per arc-node (which can use bold inline labels: \`**the open**: ...\`).
136
147
 
137
148
  ## One fact, one home
@@ -277,6 +288,7 @@ edges:
277
288
  - some-named-phrase
278
289
  - objects/some-artifact
279
290
  ref_files: []
291
+ summary: A short prose description of the article — 1-4 sentences, single line.
280
292
  ---
281
293
  \`\`\`
282
294
 
@@ -408,6 +420,7 @@ For each article you touched:
408
420
  9. **Spawn check.** Did you ask "what's recognizable here?" not "what have I earned?" Did you catch any hedging — and spawn anyway? Any fold-into-parent / defer stealth-skips you almost did?
409
421
  10. **Split-not-compress.** If anything went over cap, did you split? If you compressed, can you name the rationale in one sentence?
410
422
  11. **Edges.** Outgoing within tiered caps (atomic ≤10, arc ≤15, gravity well ≤25, hard limit 20 on non-hubs)? No noise-edges to gravity wells from non-arc pages?
423
+ 11a. **Summary present.** Every new or updated article has a \`summary:\` line — 1-4 sentences, single YAML line, lead with the identifying detail.
411
424
  12. **Topic coherence.** Does each article answer ONE question? Gravity wells acting as hubs (pointing at topic articles), not absorbing body?
412
425
  13. **\`recent.md\`** under 2000 chars, today=full / older=one-liners?
413
426
  14. **\`[SOURCE NEEDED]\`** tags surfaced for human review?
@@ -447,6 +460,33 @@ export function resolveConsolidationPrompt(
447
460
  const resolvedPath = resolveOverridePath(overridePath);
448
461
  let contents: string;
449
462
  try {
463
+ const stat = lstatSync(resolvedPath);
464
+ if (!stat.isFile()) {
465
+ log.warn(
466
+ {
467
+ configuredPath: overridePath,
468
+ resolvedPath,
469
+ reason: "not_regular_file",
470
+ fallback: "bundled",
471
+ },
472
+ "consolidation prompt override is not a regular file; using bundled prompt",
473
+ );
474
+ return renderConsolidationPrompt(cutoff);
475
+ }
476
+ if (stat.size > MAX_PROMPT_BYTES) {
477
+ log.warn(
478
+ {
479
+ configuredPath: overridePath,
480
+ resolvedPath,
481
+ size: stat.size,
482
+ limit: MAX_PROMPT_BYTES,
483
+ reason: "oversized_override",
484
+ fallback: "bundled",
485
+ },
486
+ "consolidation prompt override exceeds size limit; using bundled prompt",
487
+ );
488
+ return renderConsolidationPrompt(cutoff);
489
+ }
450
490
  contents = readFileSync(resolvedPath, "utf-8");
451
491
  } catch (err) {
452
492
  const code = (err as NodeJS.ErrnoException).code;