@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
@@ -106,6 +106,7 @@ import {
106
106
  import type { SkillProjectionCache } from "./conversation-skill-tools.js";
107
107
  import {
108
108
  createSurfaceMutex,
109
+ flushPendingSurfaceDataPersists,
109
110
  handleSurfaceAction as handleSurfaceActionImpl,
110
111
  handleSurfaceUndo as handleSurfaceUndoImpl,
111
112
  type SurfaceActionResult,
@@ -117,6 +118,7 @@ import {
117
118
  createToolExecutor,
118
119
  } from "./conversation-tool-setup.js";
119
120
  import { refreshWorkspaceTopLevelContextIfNeeded as refreshWorkspaceImpl } from "./conversation-workspace.js";
121
+ import { canonicalizeTimeZone } from "./date-context.js";
120
122
  import type { HostAppControlProxy } from "./host-app-control-proxy.js";
121
123
  import { HostCuProxy } from "./host-cu-proxy.js";
122
124
  import type {
@@ -136,17 +138,6 @@ import { TraceEmitter } from "./trace-emitter.js";
136
138
 
137
139
  const log = getLogger("conversation");
138
140
 
139
- export interface ConversationMemoryPolicy {
140
- scopeId: string;
141
- includeDefaultFallback: boolean;
142
- }
143
-
144
- export const DEFAULT_MEMORY_POLICY: Readonly<ConversationMemoryPolicy> =
145
- Object.freeze({
146
- scopeId: "default",
147
- includeDefaultFallback: false,
148
- });
149
-
150
141
  export { findLastUndoableUserMessageIndex } from "./conversation-history.js";
151
142
  export type {
152
143
  QueueDrainReason,
@@ -170,6 +161,7 @@ export class Conversation {
170
161
  /** @internal */ eventBus = new EventBus<AssistantDomainEvents>();
171
162
  /** @internal */ workingDir: string;
172
163
  /** @internal */ allowedToolNames?: Set<string>;
164
+ /** @internal */ diskPressureCleanupModeActive?: boolean;
173
165
  /** @internal */ toolsDisabledDepth = 0;
174
166
  /** @internal */ preactivatedSkillIds?: string[];
175
167
  /** @internal */ subagentAllowedTools?: Set<string>;
@@ -318,12 +310,11 @@ export class Conversation {
318
310
  * @internal
319
311
  */
320
312
  hostUsername?: string;
313
+ /** @internal */ clientTimezone?: string;
321
314
  public readonly traceEmitter: TraceEmitter;
322
315
  /** @internal */ hasSystemPromptOverride: boolean;
323
- public memoryPolicy: ConversationMemoryPolicy;
324
316
  /** @internal */ readonly graphMemory: ConversationGraphMemory;
325
317
  /** @internal */ activeContextNodeIds?: string[];
326
- /** @internal */ memoryScopeId?: string;
327
318
  /** @internal */ streamThinking: boolean;
328
319
  /** @internal */ turnCount = 0;
329
320
  public lastAssistantAttachments: AssistantAttachmentDraft[] = [];
@@ -358,7 +349,6 @@ export class Conversation {
358
349
  maxTokens: number | undefined,
359
350
  sendToClient: (msg: ServerMessage) => void,
360
351
  workingDir: string,
361
- memoryPolicy?: ConversationMemoryPolicy,
362
352
  sharedCesClient?: CesClient,
363
353
  speedOverride?: Speed,
364
354
  cacheTtl?: "5m" | "1h",
@@ -369,13 +359,7 @@ export class Conversation {
369
359
  this.provider = provider;
370
360
  this.workingDir = workingDir;
371
361
  this.sendToClient = sendToClient;
372
- this.memoryPolicy = memoryPolicy
373
- ? { ...memoryPolicy }
374
- : { ...DEFAULT_MEMORY_POLICY };
375
- this.graphMemory = new ConversationGraphMemory(
376
- this.memoryPolicy.scopeId,
377
- conversationId,
378
- );
362
+ this.graphMemory = new ConversationGraphMemory(conversationId);
379
363
  this.traceEmitter = new TraceEmitter(conversationId, sendToClient);
380
364
  this.prompter = new PermissionPrompter(sendToClient);
381
365
  this.prompter.setOnStateChanged((requestId, state, source, toolUseId) => {
@@ -764,6 +748,11 @@ export class Conversation {
764
748
  clearTimeout(timer);
765
749
  }
766
750
  this.recentlyCompletedStandaloneSurfaces.clear();
751
+ // Flush any pending debounced surface-data persists for this
752
+ // conversation so updates that arrived inside the debounce window
753
+ // still land in the DB before teardown. Flushing also clears the
754
+ // pending entries, so no separate cancel call is needed.
755
+ flushPendingSurfaceDataPersists(this.conversationId);
767
756
  // Only dispose the per-conversation CU and app-control proxies.
768
757
  // Bash/File/Transfer are singletons — their lifecycle is managed by
769
758
  // static disposeInstance().
@@ -774,7 +763,6 @@ export class Conversation {
774
763
  // Do NOT close it here; the server manages the CES lifecycle.
775
764
  this.cesClient = undefined;
776
765
  this.activeContextNodeIds = this.graphMemory.tracker.getActiveNodeIds();
777
- this.memoryScopeId = this.memoryPolicy.scopeId;
778
766
  this.graphMemory.persistState();
779
767
  disposeConversation(this);
780
768
  }
@@ -1064,7 +1052,7 @@ export class Conversation {
1064
1052
  );
1065
1053
  }
1066
1054
  if (result.compacted) {
1067
- applyCompactionResult(this, result, this.sendToClient, null, {
1055
+ await applyCompactionResult(this, result, this.sendToClient, null, {
1068
1056
  slackContextCompactionWatermarkTs: getSlackCompactionWatermarkForPrefix(
1069
1057
  slackChronologicalContext,
1070
1058
  result.compactedMessages,
@@ -1144,6 +1132,13 @@ export class Conversation {
1144
1132
  }
1145
1133
  }
1146
1134
 
1135
+ applyClientTimezoneFromTransport(
1136
+ transport: ConversationTransportMetadata,
1137
+ ): void {
1138
+ this.clientTimezone =
1139
+ canonicalizeTimeZone(transport.clientTimezone) ?? undefined;
1140
+ }
1141
+
1147
1142
  setAssistantId(assistantId: string | null): void {
1148
1143
  this.assistantId = assistantId ?? undefined;
1149
1144
  }
@@ -14,10 +14,31 @@ export interface TemporalContextOptions {
14
14
  hostTimeZone?: string;
15
15
  /** IANA timezone configured in user settings (if available). */
16
16
  configuredUserTimeZone?: string | null;
17
- /** IANA timezone inferred from user profile/memory (if available). */
17
+ /** IANA timezone reported by the active client for the current turn. */
18
+ clientTimezone?: string | null;
19
+ /** IANA timezone persisted from prior client environment detection. */
20
+ detectedTimezone?: string | null;
21
+ /** Profile timezone candidate accepted by legacy callers; not used for turn resolution. */
18
22
  userTimeZone?: string | null;
19
23
  }
20
24
 
25
+ export type TurnTimezoneSource =
26
+ | "timeZone"
27
+ | "configuredUserTimezone"
28
+ | "clientTimezone"
29
+ | "detectedTimezone"
30
+ | "hostTimezone"
31
+ | "utcFallback";
32
+
33
+ export interface TurnTimezoneContext {
34
+ configuredUserTimezone: string | null;
35
+ clientTimezone: string | null;
36
+ detectedTimezone: string | null;
37
+ hostTimezone: string | null;
38
+ effectiveTimezone: string;
39
+ source: TurnTimezoneSource;
40
+ }
41
+
21
42
  const WEEKDAY_LONG = [
22
43
  "Sunday",
23
44
  "Monday",
@@ -86,7 +107,12 @@ function canonicalizeUtcGmtOffsetToken(offsetToken: string): string | null {
86
107
  ).padStart(2, "0")}`;
87
108
  }
88
109
 
89
- function canonicalizeTimeZone(timeZone: string): string | null {
110
+ export function canonicalizeTimeZone(
111
+ timeZone: string | null | undefined,
112
+ ): string | null {
113
+ if (timeZone == null) {
114
+ return null;
115
+ }
90
116
  const trimmed = timeZone.trim();
91
117
  if (trimmed.length === 0) {
92
118
  return null;
@@ -121,6 +147,17 @@ function canonicalizeTimeZone(timeZone: string): string | null {
121
147
  }
122
148
  }
123
149
 
150
+ function firstResolvedTimezone(
151
+ candidates: Array<[TurnTimezoneSource, string | null]>,
152
+ ): { source: TurnTimezoneSource; timeZone: string } | null {
153
+ for (const [source, timeZone] of candidates) {
154
+ if (timeZone) {
155
+ return { source, timeZone };
156
+ }
157
+ }
158
+ return null;
159
+ }
160
+
124
161
  /**
125
162
  * Common timezone abbreviation → IANA identifier mapping.
126
163
  * Used as a fallback when `Intl.DateTimeFormat` does not recognize the abbreviation.
@@ -289,11 +326,41 @@ function formatLocalDate(date: Date, timeZone: string): string {
289
326
  ).padStart(2, "0")}`;
290
327
  }
291
328
 
329
+ export function resolveTurnTimezoneContext(
330
+ options: TemporalContextOptions = {},
331
+ ): TurnTimezoneContext {
332
+ const configuredUserTimezone = canonicalizeTimeZone(
333
+ options.configuredUserTimeZone,
334
+ );
335
+ const clientTimezone = canonicalizeTimeZone(options.clientTimezone);
336
+ const detectedTimezone = canonicalizeTimeZone(options.detectedTimezone);
337
+ const hostTimezone = canonicalizeTimeZone(
338
+ options.hostTimeZone ?? Intl.DateTimeFormat().resolvedOptions().timeZone,
339
+ );
340
+ const explicitTimezone = canonicalizeTimeZone(options.timeZone);
341
+ const selected = firstResolvedTimezone([
342
+ ["timeZone", explicitTimezone],
343
+ ["configuredUserTimezone", configuredUserTimezone],
344
+ ["clientTimezone", clientTimezone],
345
+ ["detectedTimezone", detectedTimezone],
346
+ ["hostTimezone", hostTimezone],
347
+ ]);
348
+
349
+ return {
350
+ configuredUserTimezone,
351
+ clientTimezone,
352
+ detectedTimezone,
353
+ hostTimezone,
354
+ effectiveTimezone: selected?.timeZone ?? "UTC",
355
+ source: selected?.source ?? "utcFallback",
356
+ };
357
+ }
358
+
292
359
  /**
293
360
  * Format time as HH:MM:SS with UTC offset and timezone name.
294
361
  *
295
362
  * Uses the timezone resolution cascade:
296
- * explicit override → configured user tz → profile user tz → host fallback.
363
+ * explicit override → configured user tz → client tz → detected tz → host fallback.
297
364
  *
298
365
  * Returns format: `2026-04-02 (Thursday) 01:52:33 -05:00 (America/Chicago)`
299
366
  */
@@ -301,24 +368,7 @@ export function formatTurnTimestamp(
301
368
  options: TemporalContextOptions = {},
302
369
  ): string {
303
370
  const now = new Date(options.nowMs ?? Date.now());
304
- const resolvedHostTimeZone =
305
- canonicalizeTimeZone(
306
- options.hostTimeZone ?? Intl.DateTimeFormat().resolvedOptions().timeZone,
307
- ) ?? "UTC";
308
- const resolvedConfiguredUserTimeZone = options.configuredUserTimeZone
309
- ? canonicalizeTimeZone(options.configuredUserTimeZone)
310
- : null;
311
- const resolvedUserTimeZone = options.userTimeZone
312
- ? canonicalizeTimeZone(options.userTimeZone)
313
- : null;
314
- const resolvedTimeZone = options.timeZone
315
- ? canonicalizeTimeZone(options.timeZone)
316
- : null;
317
- const timeZone =
318
- resolvedTimeZone ??
319
- resolvedConfiguredUserTimeZone ??
320
- resolvedUserTimeZone ??
321
- resolvedHostTimeZone;
371
+ const timeZone = resolveTurnTimezoneContext(options).effectiveTimezone;
322
372
 
323
373
  const dateStr = formatLocalDate(now, timeZone);
324
374
  const todayParts = localDateParts(now, timeZone);
@@ -341,4 +391,3 @@ export function formatTurnTimestamp(
341
391
 
342
392
  return `${dateStr} (${dayName}) ${hour}:${minute}:${second} ${offset} (${timeZone})`;
343
393
  }
344
-
@@ -0,0 +1,73 @@
1
+ import {
2
+ type DiskPressureBlockedCapability,
3
+ type DiskPressureStatus,
4
+ getDiskPressureStatus,
5
+ } from "./disk-pressure-guard.js";
6
+
7
+ export type DiskPressureBackgroundGateDecision =
8
+ | { action: "allow"; status: DiskPressureStatus }
9
+ | {
10
+ action: "skip";
11
+ reason: "disk_pressure";
12
+ status: DiskPressureStatus;
13
+ blockedCapability: DiskPressureBlockedCapability;
14
+ };
15
+
16
+ export const DISK_PRESSURE_BACKGROUND_LOG_THROTTLE_MS = 60_000;
17
+
18
+ const lastSkipLogAtByKey = new Map<string, number>();
19
+
20
+ export function checkDiskPressureBackgroundGate(
21
+ blockedCapability: DiskPressureBlockedCapability = "background-work",
22
+ ): DiskPressureBackgroundGateDecision {
23
+ const status = getDiskPressureStatus();
24
+ if (!status.enabled || !status.locked || status.overrideActive) {
25
+ return { action: "allow", status };
26
+ }
27
+ if (!status.effectivelyLocked) {
28
+ return { action: "allow", status };
29
+ }
30
+ return {
31
+ action: "skip",
32
+ reason: "disk_pressure",
33
+ status,
34
+ blockedCapability,
35
+ };
36
+ }
37
+
38
+ export function shouldLogDiskPressureBackgroundSkip(
39
+ key: string,
40
+ nowMs = Date.now(),
41
+ ): boolean {
42
+ const lastLoggedAt = lastSkipLogAtByKey.get(key) ?? 0;
43
+ if (nowMs - lastLoggedAt < DISK_PRESSURE_BACKGROUND_LOG_THROTTLE_MS) {
44
+ return false;
45
+ }
46
+ lastSkipLogAtByKey.set(key, nowMs);
47
+ return true;
48
+ }
49
+
50
+ export function diskPressureBackgroundSkipLogFields(
51
+ decision: Extract<DiskPressureBackgroundGateDecision, { action: "skip" }>,
52
+ ): {
53
+ reason: "disk_pressure";
54
+ thresholdPercent: number;
55
+ usagePercent: number | null;
56
+ blockedCapability: DiskPressureBlockedCapability;
57
+ lockId: string | null;
58
+ path: string | null;
59
+ } {
60
+ return {
61
+ reason: decision.reason,
62
+ thresholdPercent: decision.status.thresholdPercent,
63
+ usagePercent: decision.status.usagePercent,
64
+ blockedCapability: decision.blockedCapability,
65
+ lockId: decision.status.lockId,
66
+ path: decision.status.path,
67
+ };
68
+ }
69
+
70
+ /** @internal */
71
+ export function __resetDiskPressureBackgroundGateForTests(): void {
72
+ lastSkipLogAtByKey.clear();
73
+ }
@@ -0,0 +1,343 @@
1
+ import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
2
+ import { getConfig } from "../config/loader.js";
3
+ import { buildAssistantEvent } from "../runtime/assistant-event.js";
4
+ import { assistantEventHub } from "../runtime/assistant-event-hub.js";
5
+ import { cancelBackgroundTools } from "../tools/background-tool-registry.js";
6
+ import { getDiskUsageInfo } from "../util/disk-usage.js";
7
+ import { getLogger } from "../util/logger.js";
8
+
9
+ export const DISK_PRESSURE_THRESHOLD_PERCENT = 95;
10
+ export const DISK_PRESSURE_CHECK_INTERVAL_MS = 60_000;
11
+ export const DISK_PRESSURE_OVERRIDE_CONFIRMATION = "I understand the risks";
12
+ export const DISK_PRESSURE_BLOCKED_CAPABILITIES = [
13
+ "agent-turns",
14
+ "background-work",
15
+ "remote-ingress",
16
+ ] as const;
17
+
18
+ export type DiskPressureState = "disabled" | "ok" | "critical" | "unknown";
19
+
20
+ export type DiskPressureBlockedCapability =
21
+ (typeof DISK_PRESSURE_BLOCKED_CAPABILITIES)[number];
22
+
23
+ export interface DiskPressureStatus {
24
+ enabled: boolean;
25
+ state: DiskPressureState;
26
+ locked: boolean;
27
+ acknowledged: boolean;
28
+ overrideActive: boolean;
29
+ effectivelyLocked: boolean;
30
+ lockId: string | null;
31
+ usagePercent: number | null;
32
+ thresholdPercent: number;
33
+ path: string | null;
34
+ lastCheckedAt: string | null;
35
+ blockedCapabilities: DiskPressureBlockedCapability[];
36
+ error: string | null;
37
+ }
38
+
39
+ export type DiskPressureTransitionResult =
40
+ | { ok: true; status: DiskPressureStatus }
41
+ | {
42
+ ok: false;
43
+ reason:
44
+ | "not_locked"
45
+ | "already_acknowledged"
46
+ | "already_overridden"
47
+ | "invalid_confirmation";
48
+ message: string;
49
+ status: DiskPressureStatus;
50
+ };
51
+
52
+ interface DiskPressureGuardState {
53
+ timer: ReturnType<typeof setInterval> | null;
54
+ status: DiskPressureStatus;
55
+ }
56
+
57
+ const log = getLogger("disk-pressure-guard");
58
+
59
+ const DISABLED_STATUS: DiskPressureStatus = {
60
+ enabled: false,
61
+ state: "disabled",
62
+ locked: false,
63
+ acknowledged: false,
64
+ overrideActive: false,
65
+ effectivelyLocked: false,
66
+ lockId: null,
67
+ usagePercent: null,
68
+ thresholdPercent: DISK_PRESSURE_THRESHOLD_PERCENT,
69
+ path: null,
70
+ lastCheckedAt: null,
71
+ blockedCapabilities: [],
72
+ error: null,
73
+ };
74
+
75
+ const OPEN_STATUS: DiskPressureStatus = {
76
+ ...DISABLED_STATUS,
77
+ enabled: true,
78
+ state: "ok",
79
+ thresholdPercent: DISK_PRESSURE_THRESHOLD_PERCENT,
80
+ };
81
+
82
+ const state: DiskPressureGuardState = {
83
+ timer: null,
84
+ status: cloneStatus(DISABLED_STATUS),
85
+ };
86
+
87
+ function cloneStatus(status: DiskPressureStatus): DiskPressureStatus {
88
+ return {
89
+ ...status,
90
+ blockedCapabilities: [...status.blockedCapabilities],
91
+ };
92
+ }
93
+
94
+ function statusFingerprint(status: DiskPressureStatus): string {
95
+ const { lastCheckedAt: _lastCheckedAt, ...substantiveStatus } = status;
96
+ return JSON.stringify(substantiveStatus);
97
+ }
98
+
99
+ function publishStatusChangedIfNeeded(previous: DiskPressureStatus): void {
100
+ if (statusFingerprint(previous) === statusFingerprint(state.status)) return;
101
+ const status = cloneStatus(state.status);
102
+ assistantEventHub
103
+ .publish(
104
+ buildAssistantEvent({
105
+ type: "disk_pressure_status_changed",
106
+ status,
107
+ }),
108
+ )
109
+ .catch((err) => {
110
+ log.warn({ err }, "Failed to publish disk pressure status change");
111
+ });
112
+ }
113
+
114
+ function replaceStatus(next: DiskPressureStatus): DiskPressureStatus {
115
+ const previous = cloneStatus(state.status);
116
+ state.status = cloneStatus(next);
117
+ publishStatusChangedIfNeeded(previous);
118
+ return cloneStatus(state.status);
119
+ }
120
+
121
+ function isEnabled(): boolean {
122
+ return isAssistantFeatureFlagEnabled("safe-storage-limits", getConfig());
123
+ }
124
+
125
+ function resetToDisabled(): DiskPressureStatus {
126
+ const previous = cloneStatus(state.status);
127
+ stopDiskPressureGuard();
128
+ state.status = cloneStatus(DISABLED_STATUS);
129
+ publishStatusChangedIfNeeded(previous);
130
+ return cloneStatus(state.status);
131
+ }
132
+
133
+ function ensureEnabledStatus(): DiskPressureStatus | null {
134
+ if (!isEnabled()) return resetToDisabled();
135
+ if (!state.status.enabled) {
136
+ state.status = cloneStatus(OPEN_STATUS);
137
+ }
138
+ return null;
139
+ }
140
+
141
+ function nextLockId(): string {
142
+ return `disk-pressure-${Date.now()}`;
143
+ }
144
+
145
+ function roundPercent(value: number): number {
146
+ return Math.round(value * 100) / 100;
147
+ }
148
+
149
+ function formatError(error: unknown): string {
150
+ return error instanceof Error ? error.message : String(error);
151
+ }
152
+
153
+ function sampleFailureStatus(error: unknown): DiskPressureStatus {
154
+ const now = new Date().toISOString();
155
+ return {
156
+ ...state.status,
157
+ enabled: true,
158
+ state: "unknown",
159
+ locked: false,
160
+ acknowledged: false,
161
+ overrideActive: false,
162
+ effectivelyLocked: false,
163
+ lockId: null,
164
+ usagePercent: null,
165
+ thresholdPercent: DISK_PRESSURE_THRESHOLD_PERCENT,
166
+ path: null,
167
+ lastCheckedAt: now,
168
+ blockedCapabilities: [],
169
+ error: formatError(error),
170
+ };
171
+ }
172
+
173
+ function cancelTerminalBackgroundToolsForLock(): void {
174
+ const cancelled = cancelBackgroundTools(
175
+ (tool) => tool.toolName === "bash" || tool.toolName === "host_bash",
176
+ "disk_pressure",
177
+ );
178
+ if (cancelled.length === 0) return;
179
+ log.info(
180
+ { count: cancelled.length, ids: cancelled.map((tool) => tool.id) },
181
+ "Cancelled background terminal tools during disk pressure lock",
182
+ );
183
+ }
184
+
185
+ function rejectTransition(
186
+ reason: Exclude<DiskPressureTransitionResult, { ok: true }>["reason"],
187
+ message: string,
188
+ status: DiskPressureStatus,
189
+ ): DiskPressureTransitionResult {
190
+ return { ok: false, reason, message, status };
191
+ }
192
+
193
+ export function startDiskPressureGuard(): DiskPressureStatus {
194
+ const disabledStatus = ensureEnabledStatus();
195
+ if (disabledStatus) return disabledStatus;
196
+
197
+ if (!state.timer) {
198
+ state.timer = setInterval(() => {
199
+ void evaluateDiskPressureNow();
200
+ }, DISK_PRESSURE_CHECK_INTERVAL_MS);
201
+ (state.timer as { unref?: () => void }).unref?.();
202
+ }
203
+
204
+ return cloneStatus(state.status);
205
+ }
206
+
207
+ export function stopDiskPressureGuard(): void {
208
+ if (!state.timer) return;
209
+ clearInterval(state.timer);
210
+ state.timer = null;
211
+ }
212
+
213
+ export function evaluateDiskPressureNow(): DiskPressureStatus {
214
+ const disabledStatus = ensureEnabledStatus();
215
+ if (disabledStatus) return disabledStatus;
216
+
217
+ let usageInfo: ReturnType<typeof getDiskUsageInfo>;
218
+ try {
219
+ usageInfo = getDiskUsageInfo();
220
+ } catch (error) {
221
+ return replaceStatus(sampleFailureStatus(error));
222
+ }
223
+
224
+ if (!usageInfo || usageInfo.totalMb <= 0) {
225
+ return replaceStatus(sampleFailureStatus("Disk usage sample unavailable"));
226
+ }
227
+
228
+ const usagePercent = roundPercent(
229
+ (usageInfo.usedMb / usageInfo.totalMb) * 100,
230
+ );
231
+ const isCritical = usagePercent >= DISK_PRESSURE_THRESHOLD_PERCENT;
232
+ const lastCheckedAt = new Date().toISOString();
233
+
234
+ if (!isCritical) {
235
+ return replaceStatus({
236
+ ...OPEN_STATUS,
237
+ usagePercent,
238
+ path: usageInfo.path,
239
+ lastCheckedAt,
240
+ });
241
+ }
242
+
243
+ if (!state.status.locked) {
244
+ cancelTerminalBackgroundToolsForLock();
245
+ }
246
+
247
+ const lockId = state.status.locked ? state.status.lockId : nextLockId();
248
+ return replaceStatus({
249
+ enabled: true,
250
+ state: "critical",
251
+ locked: true,
252
+ acknowledged: state.status.locked ? state.status.acknowledged : false,
253
+ overrideActive: state.status.locked ? state.status.overrideActive : false,
254
+ effectivelyLocked: state.status.locked
255
+ ? !state.status.overrideActive
256
+ : true,
257
+ lockId,
258
+ usagePercent,
259
+ thresholdPercent: DISK_PRESSURE_THRESHOLD_PERCENT,
260
+ path: usageInfo.path,
261
+ lastCheckedAt,
262
+ blockedCapabilities: [...DISK_PRESSURE_BLOCKED_CAPABILITIES],
263
+ error: null,
264
+ });
265
+ }
266
+
267
+ export function getDiskPressureStatus(): DiskPressureStatus {
268
+ if (!isEnabled()) return cloneStatus(DISABLED_STATUS);
269
+ if (!state.status.enabled) return cloneStatus(OPEN_STATUS);
270
+ return cloneStatus(state.status);
271
+ }
272
+
273
+ export function acknowledgeDiskPressureLock(): DiskPressureTransitionResult {
274
+ const disabledStatus = ensureEnabledStatus();
275
+ const status = disabledStatus ?? cloneStatus(state.status);
276
+ if (!status.locked) {
277
+ return rejectTransition(
278
+ "not_locked",
279
+ "No disk pressure lock is active for this assistant.",
280
+ status,
281
+ );
282
+ }
283
+
284
+ if (status.acknowledged) {
285
+ return rejectTransition(
286
+ "already_acknowledged",
287
+ "The disk pressure lock has already been acknowledged.",
288
+ status,
289
+ );
290
+ }
291
+
292
+ const previous = cloneStatus(state.status);
293
+ state.status.acknowledged = true;
294
+ publishStatusChangedIfNeeded(previous);
295
+ return { ok: true, status: cloneStatus(state.status) };
296
+ }
297
+
298
+ export function overrideDiskPressureLock(
299
+ confirmation: string,
300
+ ): DiskPressureTransitionResult {
301
+ const disabledStatus = ensureEnabledStatus();
302
+ const status = disabledStatus ?? cloneStatus(state.status);
303
+ if (!status.locked) {
304
+ return rejectTransition(
305
+ "not_locked",
306
+ "No disk pressure lock is active for this assistant.",
307
+ status,
308
+ );
309
+ }
310
+
311
+ if (status.overrideActive) {
312
+ return rejectTransition(
313
+ "already_overridden",
314
+ "The disk pressure lock has already been overridden.",
315
+ status,
316
+ );
317
+ }
318
+
319
+ if (confirmation.trim() !== DISK_PRESSURE_OVERRIDE_CONFIRMATION) {
320
+ return rejectTransition(
321
+ "invalid_confirmation",
322
+ `Type "${DISK_PRESSURE_OVERRIDE_CONFIRMATION}" to resume normal assistant behavior.`,
323
+ status,
324
+ );
325
+ }
326
+
327
+ const previous = cloneStatus(state.status);
328
+ state.status.overrideActive = true;
329
+ state.status.effectivelyLocked = false;
330
+ publishStatusChangedIfNeeded(previous);
331
+ return { ok: true, status: cloneStatus(state.status) };
332
+ }
333
+
334
+ export function __resetDiskPressureGuardForTests(): void {
335
+ stopDiskPressureGuard();
336
+ state.status = cloneStatus(DISABLED_STATUS);
337
+ }
338
+
339
+ export function __getDiskPressureGuardTimerForTests(): ReturnType<
340
+ typeof setInterval
341
+ > | null {
342
+ return state.timer;
343
+ }