@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
@@ -44,6 +44,106 @@ const conversationThresholdCache = new Map<
44
44
  >();
45
45
  const CONVERSATION_CACHE_TTL_MS = 5_000;
46
46
 
47
+ // ── Failure-coalescing log helper ────────────────────────────────────────────
48
+ // When the gateway IPC socket is broken (e.g. the path was unlinked from
49
+ // disk), every threshold lookup fails with ENOENT on the hot path. Without
50
+ // coalescing the per-call WARN drowns the actual signal ("Strict-when-
51
+ // Relaxed because the gateway lost its socket") in its own log spam.
52
+ //
53
+ // Each `op` (e.g. "conversation_threshold", "global_thresholds") emits at
54
+ // most one WARN per {@link DEFAULT_FAILURE_WARN_INTERVAL_MS} window. The
55
+ // first failure in a streak WARNs immediately so failures aren't lost. When
56
+ // the IPC starts working again, an INFO records the streak duration and
57
+ // how many calls were swallowed — that's the cue dashboards should alert
58
+ // on.
59
+
60
+ interface FailureState {
61
+ consecutiveFailures: number;
62
+ firstFailureAt: number;
63
+ lastWarnAt: number;
64
+ }
65
+
66
+ const DEFAULT_FAILURE_WARN_INTERVAL_MS = 30_000;
67
+ let failureWarnIntervalMs = DEFAULT_FAILURE_WARN_INTERVAL_MS;
68
+ const failureStateByOp = new Map<string, FailureState>();
69
+
70
+ function noteFailure(
71
+ op: string,
72
+ fields: Record<string, unknown>,
73
+ message: string,
74
+ ): void {
75
+ const now = Date.now();
76
+ const state = failureStateByOp.get(op);
77
+ if (!state) {
78
+ failureStateByOp.set(op, {
79
+ consecutiveFailures: 1,
80
+ firstFailureAt: now,
81
+ lastWarnAt: now,
82
+ });
83
+ log.warn(
84
+ {
85
+ ...fields,
86
+ op,
87
+ consecutiveFailures: 1,
88
+ event: "ipc_threshold_failure",
89
+ },
90
+ message,
91
+ );
92
+ return;
93
+ }
94
+ state.consecutiveFailures += 1;
95
+ if (now - state.lastWarnAt >= failureWarnIntervalMs) {
96
+ log.warn(
97
+ {
98
+ ...fields,
99
+ op,
100
+ consecutiveFailures: state.consecutiveFailures,
101
+ streakDurationMs: now - state.firstFailureAt,
102
+ event: "ipc_threshold_failure",
103
+ },
104
+ message,
105
+ );
106
+ state.lastWarnAt = now;
107
+ }
108
+ }
109
+
110
+ function noteSuccess(op: string): void {
111
+ const state = failureStateByOp.get(op);
112
+ if (!state) return;
113
+ log.info(
114
+ {
115
+ op,
116
+ swallowedFailures: state.consecutiveFailures,
117
+ streakDurationMs: Date.now() - state.firstFailureAt,
118
+ event: "ipc_threshold_recovered",
119
+ },
120
+ "Gateway IPC threshold call recovered after failure streak",
121
+ );
122
+ failureStateByOp.delete(op);
123
+ }
124
+
125
+ /** Test-only: clear the failure-coalescing state. */
126
+ export function _resetFailureCoalesceForTesting(): void {
127
+ failureStateByOp.clear();
128
+ failureWarnIntervalMs = DEFAULT_FAILURE_WARN_INTERVAL_MS;
129
+ }
130
+
131
+ /**
132
+ * Test-only: read a snapshot of the failure-coalescing state for a given
133
+ * op. Returns `undefined` when no streak is in progress.
134
+ */
135
+ export function _getFailureStateForTesting(
136
+ op: string,
137
+ ): Readonly<FailureState> | undefined {
138
+ const state = failureStateByOp.get(op);
139
+ return state ? { ...state } : undefined;
140
+ }
141
+
142
+ /** Test-only: override the WARN cadence. Pass {@link DEFAULT_FAILURE_WARN_INTERVAL_MS} to reset. */
143
+ export function _setFailureWarnIntervalForTesting(intervalMs: number): void {
144
+ failureWarnIntervalMs = intervalMs;
145
+ }
146
+
47
147
  /**
48
148
  * Clear the global threshold cache. Exported for testing.
49
149
  */
@@ -112,18 +212,24 @@ export async function getAutoApproveThreshold(
112
212
  })) as ConversationThreshold | null | undefined;
113
213
 
114
214
  if (result === undefined) {
115
- log.warn(
215
+ noteFailure(
216
+ "conversation_threshold",
116
217
  { conversationId },
117
218
  "IPC call failed for conversation threshold override, falling through to global",
118
219
  );
119
220
  // Fall through to global threshold fetch below.
120
- } else if (result && isValidThreshold(result.threshold)) {
121
- conversationThresholdCache.set(conversationId, {
122
- threshold: result.threshold,
123
- timestamp: Date.now(),
124
- });
125
- return result.threshold;
126
221
  } else {
222
+ // Any defined response (including a null "no override") is a
223
+ // successful round-trip — clear any in-progress failure streak so
224
+ // dashboards see the recovery.
225
+ noteSuccess("conversation_threshold");
226
+ if (result && isValidThreshold(result.threshold)) {
227
+ conversationThresholdCache.set(conversationId, {
228
+ threshold: result.threshold,
229
+ timestamp: Date.now(),
230
+ });
231
+ return result.threshold;
232
+ }
127
233
  // result === null (or an unexpected shape) — cache the negative result
128
234
  // and fall through to global defaults.
129
235
  conversationThresholdCache.set(conversationId, {
@@ -151,7 +257,8 @@ export async function getAutoApproveThreshold(
151
257
  } catch (err) {
152
258
  // Gateway unreachable — default to "none" (Strict) so no tools are
153
259
  // silently auto-approved when the gateway is down.
154
- log.warn(
260
+ noteFailure(
261
+ "global_thresholds",
155
262
  { error: String(err) },
156
263
  "Failed to fetch global thresholds, defaulting to none",
157
264
  );
@@ -176,6 +283,7 @@ async function fetchGlobalThresholds(): Promise<GlobalThresholds> {
176
283
  throw new Error("Gateway IPC returned no result for global thresholds");
177
284
  }
178
285
 
286
+ noteSuccess("global_thresholds");
179
287
  cachedGlobalThresholds = result;
180
288
  cachedGlobalTimestamp = Date.now();
181
289
  return result;
@@ -11,19 +11,14 @@ import type { AllowlistOption, ScopeOption, UserDecision } from "./types.js";
11
11
 
12
12
  const log = getLogger("permission-prompter");
13
13
 
14
- interface PendingPrompt {
15
- resolve: (value: {
16
- decision: UserDecision;
17
- selectedPattern?: string;
18
- selectedScope?: string;
19
- decisionContext?: string;
20
- wasTimeout?: boolean;
21
- wasSystemCancel?: boolean;
22
- }) => void;
23
- reject: (reason: Error) => void;
24
- timer: ReturnType<typeof setTimeout>;
25
- toolUseId?: string;
26
- }
14
+ type ConfirmResult = {
15
+ decision: UserDecision;
16
+ selectedPattern?: string;
17
+ selectedScope?: string;
18
+ decisionContext?: string;
19
+ wasTimeout?: boolean;
20
+ wasSystemCancel?: boolean;
21
+ };
27
22
 
28
23
  export type ConfirmationStateCallback = (
29
24
  requestId: string,
@@ -33,7 +28,13 @@ export type ConfirmationStateCallback = (
33
28
  ) => void;
34
29
 
35
30
  export class PermissionPrompter {
36
- private pending = new Map<string, PendingPrompt>();
31
+ /**
32
+ * Tracks which requestIds belong to this prompter instance so that
33
+ * denyAllPending / dispose can scope their cleanup to this conversation.
34
+ * The full per-request state (callbacks, timer, toolUseId) lives in
35
+ * pendingInteractions, matching the host proxy pattern.
36
+ */
37
+ private ownedIds = new Set<string>();
37
38
  private sendToClient: (msg: ServerMessage) => void;
38
39
  private onStateChanged?: ConfirmationStateCallback;
39
40
 
@@ -69,74 +70,68 @@ export class PermissionPrompter {
69
70
  riskReason?: string,
70
71
  isContainerized?: boolean,
71
72
  directoryScopeOptions?: readonly { scope: string; label: string }[],
72
- ): Promise<{
73
- decision: UserDecision;
74
- selectedPattern?: string;
75
- selectedScope?: string;
76
- decisionContext?: string;
77
- wasTimeout?: boolean;
78
- wasSystemCancel?: boolean;
79
- wasAbort?: boolean;
80
- }> {
73
+ ): Promise<ConfirmResult & { wasAbort?: boolean }> {
81
74
  if (signal?.aborted) return { decision: "deny", wasAbort: true };
82
75
 
83
76
  const requestId = uuid();
84
77
 
85
- // Self-register in pendingInteractions so /v1/confirm can route the
86
- // response to this conversation without going through broadcastMessage.
87
- if (conversationId) {
88
- pendingInteractions.register(requestId, {
89
- conversationId,
90
- kind: "confirmation",
91
- confirmationDetails: {
92
- toolName,
93
- input: redactSensitiveFields(input),
94
- riskLevel,
95
- executionTarget,
96
- allowlistOptions: allowlistOptions.map((o) => ({
97
- label: o.label,
98
- description: o.description,
99
- pattern: o.pattern,
100
- })),
101
- scopeOptions: scopeOptions.map((o) => ({
102
- label: o.label,
103
- scope: o.scope,
104
- })),
105
- persistentDecisionsAllowed: persistentDecisionsAllowed ?? true,
106
- },
107
- });
108
- }
109
-
110
78
  return new Promise((resolve, reject) => {
111
79
  const timeoutMs = getConfig().timeouts.permissionTimeoutSec * 1000;
80
+
112
81
  const timer = setTimeout(() => {
113
- this.pending.delete(requestId);
114
- pendingInteractions.resolve(requestId);
82
+ const interaction = pendingInteractions.resolve(requestId);
83
+ this.ownedIds.delete(requestId);
115
84
  log.warn(
116
85
  { requestId, toolName },
117
86
  "Permission prompt timed out, defaulting to deny",
118
87
  );
119
88
  this.onStateChanged?.(requestId, "timed_out", "timeout", toolUseId);
120
- resolve({
121
- decision: "deny",
122
- wasTimeout: true,
123
- decisionContext: `The permission prompt for the "${toolName}" tool timed out. The user did not explicitly deny this request — they may have been away or busy. You may retry this tool call if it is still needed for the current task.`,
124
- });
89
+ (interaction?.rpcResolve as ((v: ConfirmResult) => void) | undefined)?.(
90
+ {
91
+ decision: "deny",
92
+ wasTimeout: true,
93
+ decisionContext: `The permission prompt for the "${toolName}" tool timed out. The user did not explicitly deny this request — they may have been away or busy. You may retry this tool call if it is still needed for the current task.`,
94
+ },
95
+ );
125
96
  }, timeoutMs);
126
97
 
127
- this.pending.set(requestId, {
128
- resolve,
129
- reject,
130
- timer,
131
- toolUseId,
132
- });
98
+ // Register all lifecycle state in pendingInteractions — same pattern as
99
+ // host proxies. The prompter tracks ownership via ownedIds.
100
+ // Always register unconditionally so rpcResolve/rpcReject/timer
101
+ // are reachable by resolveConfirmation, denyAllPending, and the timeout
102
+ // handler even when conversationId is absent. Routes return 404 for
103
+ // interactions with an empty conversationId, which is correct behaviour.
104
+ pendingInteractions.register(requestId, {
105
+ conversationId: conversationId ?? "",
106
+ kind: "confirmation",
107
+ confirmationDetails: {
108
+ toolName,
109
+ input: redactSensitiveFields(input),
110
+ riskLevel,
111
+ executionTarget,
112
+ allowlistOptions: allowlistOptions.map((o) => ({
113
+ label: o.label,
114
+ description: o.description,
115
+ pattern: o.pattern,
116
+ })),
117
+ scopeOptions: scopeOptions.map((o) => ({
118
+ label: o.label,
119
+ scope: o.scope,
120
+ })),
121
+ persistentDecisionsAllowed: persistentDecisionsAllowed ?? true,
122
+ },
123
+ rpcResolve: resolve as (value: unknown) => void,
124
+ rpcReject: reject,
125
+ timer,
126
+ toolUseId,
127
+ });
128
+ this.ownedIds.add(requestId);
133
129
 
134
130
  if (signal) {
135
131
  const onAbort = () => {
136
- if (this.pending.has(requestId)) {
137
- clearTimeout(timer);
138
- this.pending.delete(requestId);
132
+ if (this.ownedIds.has(requestId)) {
139
133
  pendingInteractions.resolve(requestId);
134
+ this.ownedIds.delete(requestId);
140
135
  resolve({ decision: "deny", wasAbort: true });
141
136
  }
142
137
  };
@@ -175,17 +170,17 @@ export class PermissionPrompter {
175
170
  }
176
171
 
177
172
  hasPendingRequest(requestId: string): boolean {
178
- return this.pending.has(requestId);
173
+ return this.ownedIds.has(requestId);
179
174
  }
180
175
 
181
176
  /** Returns all currently pending request IDs. */
182
177
  getPendingRequestIds(): string[] {
183
- return [...this.pending.keys()];
178
+ return [...this.ownedIds];
184
179
  }
185
180
 
186
181
  /** Returns the toolUseId associated with a pending request, if any. */
187
182
  getToolUseId(requestId: string): string | undefined {
188
- return this.pending.get(requestId)?.toolUseId;
183
+ return pendingInteractions.get(requestId)?.toolUseId;
189
184
  }
190
185
 
191
186
  resolveConfirmation(
@@ -195,22 +190,17 @@ export class PermissionPrompter {
195
190
  selectedScope?: string,
196
191
  decisionContext?: string,
197
192
  ): void {
198
- const pending = this.pending.get(requestId);
199
- if (!pending) {
193
+ if (!this.ownedIds.has(requestId)) {
200
194
  log.warn({ requestId }, "No pending prompt for confirmation response");
201
195
  return;
202
196
  }
203
- clearTimeout(pending.timer);
204
- this.pending.delete(requestId);
205
- // Idempotent approval-routes already calls pendingInteractions.resolve()
206
- // before routing here, but we call it defensively for non-route paths.
207
- pendingInteractions.resolve(requestId);
208
- pending.resolve({
209
- decision,
210
- selectedPattern,
211
- selectedScope,
212
- decisionContext,
213
- });
197
+ // The prompter owns deregistration; all callers use get() to peek before
198
+ // routing to resolveConfirmation, which fires the rpcResolve callback.
199
+ const interaction = pendingInteractions.resolve(requestId);
200
+ this.ownedIds.delete(requestId);
201
+ (interaction?.rpcResolve as ((v: ConfirmResult) => void) | undefined)?.(
202
+ { decision, selectedPattern, selectedScope, decisionContext },
203
+ );
214
204
  }
215
205
 
216
206
  /**
@@ -219,31 +209,31 @@ export class PermissionPrompter {
219
209
  * see the denial and can re-request if still needed.
220
210
  */
221
211
  denyAllPending(): void {
222
- for (const [requestId, pending] of this.pending) {
223
- clearTimeout(pending.timer);
224
- this.pending.delete(requestId);
225
- pendingInteractions.resolve(requestId);
226
- pending.resolve({
227
- decision: "deny",
228
- wasSystemCancel: true,
229
- decisionContext:
230
- "The user sent a new message instead of responding to this permission prompt. Stop what you are doing and respond to the user's new message. Do NOT retry this tool or request permission again until the user asks you to.",
231
- });
212
+ for (const requestId of [...this.ownedIds]) {
213
+ const interaction = pendingInteractions.resolve(requestId);
214
+ this.ownedIds.delete(requestId);
215
+ (interaction?.rpcResolve as ((v: ConfirmResult) => void) | undefined)?.(
216
+ {
217
+ decision: "deny",
218
+ wasSystemCancel: true,
219
+ decisionContext:
220
+ "The user sent a new message instead of responding to this permission prompt. Stop what you are doing and respond to the user's new message. Do NOT retry this tool or request permission again until the user asks you to.",
221
+ },
222
+ );
232
223
  }
233
224
  }
234
225
 
235
226
  get hasPending(): boolean {
236
- return this.pending.size > 0;
227
+ return this.ownedIds.size > 0;
237
228
  }
238
229
 
239
230
  dispose(): void {
240
- for (const [requestId, pending] of this.pending) {
241
- clearTimeout(pending.timer);
242
- pendingInteractions.resolve(requestId);
243
- pending.reject(
231
+ for (const requestId of [...this.ownedIds]) {
232
+ const interaction = pendingInteractions.resolve(requestId);
233
+ this.ownedIds.delete(requestId);
234
+ interaction?.rpcReject?.(
244
235
  new AssistantError("Prompter disposed", ErrorCode.INTERNAL_ERROR),
245
236
  );
246
237
  }
247
- this.pending.clear();
248
238
  }
249
239
  }
@@ -20,12 +20,6 @@ export interface SecretPromptResult {
20
20
  error?: "unsupported_channel";
21
21
  }
22
22
 
23
- interface PendingSecretPrompt {
24
- resolve: (result: SecretPromptResult) => void;
25
- reject: (reason: Error) => void;
26
- timer: ReturnType<typeof setTimeout>;
27
- }
28
-
29
23
  export interface SecretPrompterChannelContext {
30
24
  /** The channel the conversation was initiated from (e.g. "slack", "macos"). */
31
25
  channel?: string;
@@ -34,7 +28,13 @@ export interface SecretPrompterChannelContext {
34
28
  }
35
29
 
36
30
  export class SecretPrompter {
37
- private pending = new Map<string, PendingSecretPrompt>();
31
+ /**
32
+ * Tracks which requestIds belong to this prompter instance so that
33
+ * dispose can scope its cleanup to this conversation.
34
+ * The full per-request state (callbacks, timer) lives in pendingInteractions,
35
+ * matching the host proxy and PermissionPrompter pattern.
36
+ */
37
+ private ownedIds = new Set<string>();
38
38
  private channelContext?: SecretPrompterChannelContext;
39
39
 
40
40
  setChannelContext(ctx: SecretPrompterChannelContext | undefined): void {
@@ -45,12 +45,9 @@ export class SecretPrompter {
45
45
  * Broadcast a secret_request to all connected clients and wait for a
46
46
  * response.
47
47
  *
48
- * The request is always published to the SSE hub via
49
- * {@link broadcastMessage} so any connected client (desktop, web) can
50
- * display the secure prompt dialog.
51
- *
52
- * Pending interaction registration is handled by {@link broadcastMessage}
53
- * when the secret_request event is published to the hub.
48
+ * Registers all lifecycle state (rpcResolve, rpcReject, timer) in
49
+ * pendingInteractions before broadcasting identical to the host proxy
50
+ * and PermissionPrompter pattern.
54
51
  *
55
52
  * SECURITY: Logs only metadata (requestId, service, field) — never the
56
53
  * returned secret value. The timeout path also returns a null value
@@ -72,21 +69,24 @@ export class SecretPrompter {
72
69
 
73
70
  return new Promise((resolve, reject) => {
74
71
  const timeoutMs = getConfig().timeouts.permissionTimeoutSec * 1000;
72
+
75
73
  const timer = setTimeout(() => {
76
- this.pending.delete(requestId);
77
74
  pendingInteractions.resolve(requestId);
75
+ this.ownedIds.delete(requestId);
78
76
  log.warn({ requestId, service, field }, "Secret prompt timed out");
79
77
  resolve({ value: null, delivery: "store" });
80
78
  }, timeoutMs);
81
79
 
82
- this.pending.set(requestId, { resolve, reject, timer });
83
-
84
- // Self-register in pendingInteractions so /v1/secret can route the
85
- // response to this conversation without relying on broadcastMessage.
80
+ // Register all lifecycle state in pendingInteractions — same pattern as
81
+ // host proxies and PermissionPrompter. The prompter tracks ownership via ownedIds.
86
82
  pendingInteractions.register(requestId, {
87
83
  conversationId: effectiveConversationId,
88
84
  kind: "secret",
85
+ rpcResolve: resolve as (value: unknown) => void,
86
+ rpcReject: reject,
87
+ timer,
89
88
  });
89
+ this.ownedIds.add(requestId);
90
90
 
91
91
  const config = getConfig();
92
92
  const msg: SecretRequestMessage = {
@@ -109,7 +109,7 @@ export class SecretPrompter {
109
109
  }
110
110
 
111
111
  hasPendingRequest(requestId: string): boolean {
112
- return this.pending.has(requestId);
112
+ return this.ownedIds.has(requestId);
113
113
  }
114
114
 
115
115
  /**
@@ -124,26 +124,26 @@ export class SecretPrompter {
124
124
  value?: string,
125
125
  delivery?: SecretDelivery,
126
126
  ): void {
127
- const pending = this.pending.get(requestId);
128
- if (!pending) {
127
+ if (!this.ownedIds.has(requestId)) {
129
128
  log.warn({ requestId }, "No pending prompt for secret response");
130
129
  return;
131
130
  }
132
- clearTimeout(pending.timer);
133
- this.pending.delete(requestId);
134
- // Clean up the global map (may already be removed by approval-routes).
135
- pendingInteractions.resolve(requestId);
136
- pending.resolve({ value: value ?? null, delivery: delivery ?? "store" });
131
+ // approval-routes calls pendingInteractions.get() before routing here;
132
+ // the prompter owns deregistration so it fires the Promise callback cleanly.
133
+ const interaction = pendingInteractions.resolve(requestId);
134
+ this.ownedIds.delete(requestId);
135
+ (interaction?.rpcResolve as ((v: SecretPromptResult) => void) | undefined)?.(
136
+ { value: value ?? null, delivery: delivery ?? "store" },
137
+ );
137
138
  }
138
139
 
139
140
  dispose(): void {
140
- for (const [requestId, pending] of this.pending) {
141
- clearTimeout(pending.timer);
142
- pendingInteractions.resolve(requestId);
143
- pending.reject(
141
+ for (const requestId of [...this.ownedIds]) {
142
+ const interaction = pendingInteractions.resolve(requestId);
143
+ this.ownedIds.delete(requestId);
144
+ interaction?.rpcReject?.(
144
145
  new AssistantError("Prompter disposed", ErrorCode.INTERNAL_ERROR),
145
146
  );
146
147
  }
147
- this.pending.clear();
148
148
  }
149
149
  }
@@ -3,7 +3,7 @@
3
3
  * drives the per-turn injection sequence consumed by
4
4
  * `applyRuntimeInjections`.
5
5
  *
6
- * Each of the eight default injectors reads its per-turn inputs from
6
+ * Each default injector reads its per-turn inputs from
7
7
  * `ctx.injectionInputs` (see {@link TurnInjectionInputs}), runs its gating
8
8
  * conditions (injection mode, feature flags, channel type, null-input
9
9
  * short-circuits), and returns an {@link InjectionBlock} with a
@@ -12,6 +12,7 @@
12
12
  *
13
13
  * | name | order | placement |
14
14
  * | ------------------------ | ----- | ----------------------- |
15
+ * | `disk-pressure-warning` | 5 | prepend-user-tail |
15
16
  * | `workspace-context` | 10 | prepend-user-tail |
16
17
  * | `unified-turn-context` | 20 | prepend-user-tail |
17
18
  * | `pkb-context` | 30 | after-memory-prefix |
@@ -45,10 +46,10 @@
45
46
 
46
47
  import { resolve } from "node:path";
47
48
 
49
+ import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
48
50
  import { getConfig } from "../../config/loader.js";
49
51
  import { getInContextPkbPaths } from "../../daemon/pkb-context-tracker.js";
50
52
  import { buildPkbReminder } from "../../daemon/pkb-reminder-builder.js";
51
- import { isMemoryV2ReadActive } from "../../memory/context-search/sources/memory-v2.js";
52
53
  import { searchPkbFiles } from "../../memory/pkb/pkb-search.js";
53
54
  import { getLogger } from "../../util/logger.js";
54
55
  import { registerPlugin } from "../registry.js";
@@ -74,7 +75,7 @@ const PKB_HINT_THRESHOLD = 0.5;
74
75
  const PKB_HINT_ARCHIVE_THRESHOLD = 0.7;
75
76
 
76
77
  /**
77
- * Fixed order values for the eight default injectors. Exported so tests —
78
+ * Fixed order values for the default injectors. Exported so tests —
78
79
  * and any future integration code — can assert ordering without re-deriving
79
80
  * the constants.
80
81
  *
@@ -83,6 +84,7 @@ const PKB_HINT_ARCHIVE_THRESHOLD = 0.7;
83
84
  * without renumbering the defaults.
84
85
  */
85
86
  export const DEFAULT_INJECTOR_ORDER = {
87
+ diskPressureWarning: 5,
86
88
  workspaceContext: 10,
87
89
  unifiedTurnContext: 20,
88
90
  pkbContext: 30,
@@ -98,6 +100,35 @@ function readInjectionInputs(ctx: TurnContext): TurnInjectionInputs {
98
100
  return ctx.injectionInputs ?? {};
99
101
  }
100
102
 
103
+ export const DISK_PRESSURE_WARNING_PROMPT = `<disk_pressure_warning>
104
+ Disk usage is critically low: this assistant is in storage cleanup mode because the workspace volume is at least 95% full.
105
+
106
+ In your first paragraph, warn the user that storage is critically low and that normal work is suspended until space is freed.
107
+
108
+ Then help the user clean up storage. Prefer safe inspection steps first, such as checking available space and finding large directories. Ask before deleting files or caches unless the user has already clearly approved the specific cleanup action.
109
+
110
+ Do not work on unrelated tasks until disk usage drops below the critical threshold or the user explicitly overrides the lock. Background processes and messages from trusted contacts are blocked while this cleanup mode is active.
111
+ </disk_pressure_warning>`;
112
+
113
+ function isSafeStorageLimitsEnabled(): boolean {
114
+ return isAssistantFeatureFlagEnabled("safe-storage-limits", getConfig());
115
+ }
116
+
117
+ const diskPressureWarningInjector: Injector = {
118
+ name: "disk-pressure-warning",
119
+ order: DEFAULT_INJECTOR_ORDER.diskPressureWarning,
120
+ async produce(ctx: TurnContext): Promise<InjectionBlock | null> {
121
+ if (!isSafeStorageLimitsEnabled()) return null;
122
+ const inputs = readInjectionInputs(ctx);
123
+ if (!inputs.diskPressureContext?.cleanupModeActive) return null;
124
+ return {
125
+ id: "disk-pressure-warning",
126
+ text: DISK_PRESSURE_WARNING_PROMPT,
127
+ placement: "prepend-user-tail",
128
+ };
129
+ },
130
+ };
131
+
101
132
  /**
102
133
  * v2 read-side cutover guard. The `pkb-context` injector silences itself
103
134
  * under v2 because the `<knowledge_base>` block surfaces PKB content the v2
@@ -107,7 +138,7 @@ function readInjectionInputs(ctx: TurnContext): TurnInjectionInputs {
107
138
  * state independent of PKB and fires unchanged.
108
139
  */
109
140
  function isPkbInjectionSilencedByV2(): boolean {
110
- return isMemoryV2ReadActive(getConfig());
141
+ return getConfig().memory.v2.enabled;
111
142
  }
112
143
 
113
144
  /**
@@ -520,6 +551,7 @@ export const defaultInjectorsPlugin: Plugin = {
520
551
  },
521
552
  },
522
553
  injectors: [
554
+ diskPressureWarningInjector,
523
555
  workspaceContextInjector,
524
556
  unifiedTurnContextInjector,
525
557
  pkbContextInjector,
@@ -72,12 +72,6 @@ export interface GraphMemoryPayload {
72
72
  * Passed as a second argument to {@link runDefaultMemoryRetrieval} rather
73
73
  * than threaded through {@link MemoryArgs} to keep the plugin-facing
74
74
  * pipeline surface minimal.
75
- *
76
- * The per-turn abort signal lives on {@link MemoryArgs.signal} instead of
77
- * here so the pipeline runner's `linkAbortSignal` can swap it for an
78
- * internally-linked signal — that way a plugin timeout actually cancels
79
- * the underlying `prepareMemory` work instead of letting it run after
80
- * `Promise.race` has already rejected.
81
75
  */
82
76
  export interface DefaultMemoryRetrievalDeps {
83
77
  /** Live message list for this turn (pre-injection). */
@@ -101,6 +95,11 @@ export interface DefaultMemoryRetrievalDeps {
101
95
  * trusted) or a single {@link GraphMemoryPayload} wrapping the graph
102
96
  * retriever's full output. The agent loop narrows via
103
97
  * {@link DEFAULT_MEMORY_GRAPH_KIND} to consume it.
98
+ *
99
+ * Memory retrieval blocks the turn — there is no soft timeout here. Memory
100
+ * is critical context, and silently dropping it produces a worse outcome
101
+ * than a slower turn. Cancellation still works via `args.signal`, which is
102
+ * threaded into `prepareMemory`.
104
103
  */
105
104
  export async function runDefaultMemoryRetrieval(
106
105
  args: MemoryArgs,
@@ -784,6 +784,8 @@ export interface TurnInjectionInputs {
784
784
  * context (unified turn context, etc.). Drives per-injector gating.
785
785
  */
786
786
  readonly mode?: InjectionMode;
787
+ /** Disk-pressure cleanup-mode context or null to skip the warning. */
788
+ readonly diskPressureContext?: DiskPressureInjectionContext | null;
787
789
  /** Workspace top-level context text (`<workspace>...`) or null to skip. */
788
790
  readonly workspaceTopLevelContext?: string | null;
789
791
  /** Pre-built unified-turn-context text (`<turn_context>...`) or null to skip. */
@@ -860,6 +862,11 @@ export interface TurnInjectionInputs {
860
862
  readonly isNonInteractive?: boolean;
861
863
  }
862
864
 
865
+ export interface DiskPressureInjectionContext {
866
+ /** True when the current turn is allowed to run only for storage cleanup. */
867
+ readonly cleanupModeActive: boolean;
868
+ }
869
+
863
870
  /**
864
871
  * Per-turn execution context threaded through every middleware invocation.
865
872
  *