@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
@@ -19,29 +19,28 @@
19
19
  * host_file_* are filtered out for chrome-extension regardless of the
20
20
  * hasNoClient flag.
21
21
  *
22
- * Cross-client exception (Phase 1): host_bash is allowed for non-host-proxy
23
- * interfaces (e.g. "web") when at least one host_bash-capable client is
24
- * connected via the event hub. host_file_* and host_browser remain filtered
25
- * regardless (Phase 2).
22
+ * Cross-client exception: tools whose capabilities are in
23
+ * CROSS_CLIENT_EXPOSED_CAPABILITIES (host_bash, host_file) are allowed for
24
+ * non-host-proxy interfaces (e.g. "web") when at least one capable client
25
+ * is connected via the event hub. host_browser is excluded (chrome-extension
26
+ * is its own executor; web turns have no CDP target model).
26
27
  */
27
28
 
28
29
  import { beforeEach, describe, expect, mock, test } from "bun:test";
29
30
 
30
31
  // ── Module-level mocks ─────────────────────────────────────────────
31
32
 
32
- // Control how many host_bash-capable clients the hub reports.
33
- let mockHostBashClientCount = 0;
33
+ // Control how many capable clients the hub reports per capability.
34
+ const mockClientCountByCapability = new Map<string, number>();
34
35
 
35
36
  mock.module("../../runtime/assistant-event-hub.js", () => ({
36
37
  assistantEventHub: {
37
38
  listClientsByCapability: (cap: string) => {
38
- if (cap === "host_bash") {
39
- return Array.from({ length: mockHostBashClientCount }, (_, i) => ({
40
- clientId: `mock-client-${i}`,
41
- capabilities: ["host_bash"],
42
- }));
43
- }
44
- return [];
39
+ const count = mockClientCountByCapability.get(cap) ?? 0;
40
+ return Array.from({ length: count }, (_, i) => ({
41
+ clientId: `mock-${cap}-client-${i}`,
42
+ capabilities: [cap],
43
+ }));
45
44
  },
46
45
  },
47
46
  broadcastMessage: () => {},
@@ -72,7 +71,7 @@ function makeCtx(
72
71
  }
73
72
 
74
73
  beforeEach(() => {
75
- mockHostBashClientCount = 0;
74
+ mockClientCountByCapability.clear();
76
75
  });
77
76
 
78
77
  describe("isToolActiveForContext — host tool capability gating", () => {
@@ -213,7 +212,7 @@ describe("isToolActiveForContext — cross-client exception (Phase 1: host_bash)
213
212
  test("host_bash is active for web transport when a host_bash-capable client is connected", () => {
214
213
  // Cross-client path: a web turn should see host_bash when a macOS client
215
214
  // with host_bash capability is connected via the event hub.
216
- mockHostBashClientCount = 1;
215
+ mockClientCountByCapability.set("host_bash", 1);
217
216
  expect(
218
217
  isToolActiveForContext(
219
218
  "host_bash",
@@ -224,7 +223,7 @@ describe("isToolActiveForContext — cross-client exception (Phase 1: host_bash)
224
223
 
225
224
  test("host_bash is NOT active for web transport when no capable client is connected", () => {
226
225
  // No cross-client fallback: hub has no host_bash-capable subscribers.
227
- mockHostBashClientCount = 0;
226
+ mockClientCountByCapability.set("host_bash", 0);
228
227
  expect(
229
228
  isToolActiveForContext(
230
229
  "host_bash",
@@ -233,11 +232,11 @@ describe("isToolActiveForContext — cross-client exception (Phase 1: host_bash)
233
232
  ).toBe(false);
234
233
  });
235
234
 
236
- test("host_file_read is NOT active for web transport even when a capable client is connected (Phase 2 gate)", () => {
237
- // The cross-client exception is scoped to host_bash only.
238
- // host_file_* remain filtered for non-host-proxy interfaces regardless
239
- // of connected clients until Phase 2 lands.
240
- mockHostBashClientCount = 1;
235
+ test("host_file_read is NOT active for web transport when only a host_bash client is connected", () => {
236
+ // The cross-client exception is per-capability: a host_bash-capable
237
+ // client in the hub does not satisfy host_file's exposure check, since
238
+ // listClientsByCapability is queried with the tool's actual capability.
239
+ mockClientCountByCapability.set("host_bash", 1);
241
240
  expect(
242
241
  isToolActiveForContext(
243
242
  "host_file_read",
@@ -249,7 +248,7 @@ describe("isToolActiveForContext — cross-client exception (Phase 1: host_bash)
249
248
  test("host_bash for macos transport is unaffected by the cross-client exception", () => {
250
249
  // macos natively supports host_bash via host proxy — the supportsHostProxy
251
250
  // check passes, so the cross-client branch is never reached.
252
- mockHostBashClientCount = 0;
251
+ mockClientCountByCapability.set("host_bash", 0);
253
252
  expect(
254
253
  isToolActiveForContext(
255
254
  "host_bash",
@@ -262,7 +261,7 @@ describe("isToolActiveForContext — cross-client exception (Phase 1: host_bash)
262
261
  // Even with a capable client in the hub, the macos SSE path takes
263
262
  // precedence — it passes the supportsHostProxy check, bypasses the
264
263
  // cross-client branch, and reaches the hasNoClient gate.
265
- mockHostBashClientCount = 1;
264
+ mockClientCountByCapability.set("host_bash", 1);
266
265
  expect(
267
266
  isToolActiveForContext(
268
267
  "host_bash",
@@ -275,7 +274,7 @@ describe("isToolActiveForContext — cross-client exception (Phase 1: host_bash)
275
274
  // Security boundary: chrome-extension only gets host_browser. The
276
275
  // cross-client exception explicitly excludes chrome-extension transport
277
276
  // regardless of how many host_bash-capable clients are in the hub.
278
- mockHostBashClientCount = 1;
277
+ mockClientCountByCapability.set("host_bash", 1);
279
278
  expect(
280
279
  isToolActiveForContext(
281
280
  "host_bash",
@@ -287,7 +286,7 @@ describe("isToolActiveForContext — cross-client exception (Phase 1: host_bash)
287
286
  test("host_bash is NOT active for web transport when hasNoClient is true (no approval UI)", () => {
288
287
  // hasNoClient gate: no interactive approval UI available for this turn.
289
288
  // Cross-client exception must not bypass this gate.
290
- mockHostBashClientCount = 1;
289
+ mockClientCountByCapability.set("host_bash", 1);
291
290
  expect(
292
291
  isToolActiveForContext(
293
292
  "host_bash",
@@ -297,6 +296,68 @@ describe("isToolActiveForContext — cross-client exception (Phase 1: host_bash)
297
296
  });
298
297
  });
299
298
 
299
+ describe("isToolActiveForContext — cross-client exposure for host_file_*", () => {
300
+ const HOST_FILE_TOOLS = [
301
+ "host_file_read",
302
+ "host_file_write",
303
+ "host_file_edit",
304
+ "host_file_transfer",
305
+ ] as const;
306
+
307
+ for (const tool of HOST_FILE_TOOLS) {
308
+ test(`${tool} is exposed for web transport when a host_file client is connected`, () => {
309
+ mockClientCountByCapability.set("host_file", 1);
310
+ expect(
311
+ isToolActiveForContext(
312
+ tool,
313
+ makeCtx({ hasNoClient: false, transportInterface: "web" }),
314
+ ),
315
+ ).toBe(true);
316
+ });
317
+
318
+ test(`${tool} is NOT exposed for web when no host_file client is connected`, () => {
319
+ mockClientCountByCapability.set("host_file", 0);
320
+ expect(
321
+ isToolActiveForContext(
322
+ tool,
323
+ makeCtx({ hasNoClient: false, transportInterface: "web" }),
324
+ ),
325
+ ).toBe(false);
326
+ });
327
+
328
+ test(`${tool} is NOT exposed for chrome-extension (security boundary)`, () => {
329
+ mockClientCountByCapability.set("host_file", 1);
330
+ expect(
331
+ isToolActiveForContext(
332
+ tool,
333
+ makeCtx({ hasNoClient: true, transportInterface: "chrome-extension" }),
334
+ ),
335
+ ).toBe(false);
336
+ });
337
+
338
+ test(`${tool} is NOT exposed when hasNoClient is true (no approval UI)`, () => {
339
+ mockClientCountByCapability.set("host_file", 1);
340
+ expect(
341
+ isToolActiveForContext(
342
+ tool,
343
+ makeCtx({ hasNoClient: true, transportInterface: "web" }),
344
+ ),
345
+ ).toBe(false);
346
+ });
347
+ }
348
+
349
+ test("listClientsByCapability is queried with the actual capability, not host_bash (regression guard for D5 latent bug)", () => {
350
+ mockClientCountByCapability.set("host_bash", 0);
351
+ mockClientCountByCapability.set("host_file", 1);
352
+ expect(
353
+ isToolActiveForContext(
354
+ "host_file_transfer",
355
+ makeCtx({ hasNoClient: false, transportInterface: "web" }),
356
+ ),
357
+ ).toBe(true);
358
+ });
359
+ });
360
+
300
361
  describe("HOST_TOOL_NAMES derivation", () => {
301
362
  test("HOST_TOOL_NAMES is derived from HOST_TOOL_TO_CAPABILITY", () => {
302
363
  // Sanity check: every tool in the names set has a capability mapping.
@@ -125,7 +125,7 @@ export function classifyKind(mimeType: string): "image" | "video" | "document" {
125
125
  // Validation / cap enforcement
126
126
  // ---------------------------------------------------------------------------
127
127
 
128
- export interface ValidatedDrafts {
128
+ interface ValidatedDrafts {
129
129
  accepted: AssistantAttachmentDraft[];
130
130
  warnings: string[];
131
131
  }
@@ -171,13 +171,13 @@ export interface DirectiveRequest {
171
171
  mimeType: string | undefined;
172
172
  }
173
173
 
174
- export interface DirectiveParseResult {
174
+ interface DirectiveParseResult {
175
175
  cleanText: string;
176
176
  directiveRequests: DirectiveRequest[];
177
177
  parseWarnings: string[];
178
178
  }
179
179
 
180
- export interface DirectiveDisplayDrainResult {
180
+ interface DirectiveDisplayDrainResult {
181
181
  emitText: string;
182
182
  bufferedRemainder: string;
183
183
  }
@@ -362,7 +362,7 @@ export function drainDirectiveDisplayBuffer(
362
362
  // Sandbox file resolution
363
363
  // ---------------------------------------------------------------------------
364
364
 
365
- export interface ResolveResult {
365
+ interface ResolveResult {
366
366
  draft: AssistantAttachmentDraft | null;
367
367
  warning: string | null;
368
368
  }
@@ -8,7 +8,9 @@ import {
8
8
  type FSWatcher,
9
9
  mkdirSync,
10
10
  readdirSync,
11
+ unwatchFile,
11
12
  watch,
13
+ watchFile,
12
14
  } from "node:fs";
13
15
  import { join } from "node:path";
14
16
 
@@ -17,7 +19,6 @@ import type { MemoryCleanupConfig } from "../config/schemas/memory-lifecycle.js"
17
19
  import { resetCleanupScheduleThrottle } from "../memory/cleanup-schedule-state.js";
18
20
  import { clearEmbeddingBackendCache } from "../memory/embedding-backend.js";
19
21
  import { initializeProviders } from "../providers/registry.js";
20
- import { handleBashSignal } from "../signals/bash.js";
21
22
  import { handleCancelSignal } from "../signals/cancel.js";
22
23
  import { handleConversationUndoSignal } from "../signals/conversation-undo.js";
23
24
  import { handleEmitEventSignal } from "../signals/emit-event.js";
@@ -47,19 +48,44 @@ function attachWatcherErrorHandler(watcher: FSWatcher, dir: string): void {
47
48
  });
48
49
  }
49
50
 
51
+ /**
52
+ * Poll interval for `fs.watchFile()`. Use the stat-polling watcher
53
+ * because Bun's per-file `fs.watch()` doesn't detect renames on Linux
54
+ * (seemingly works on macOS). See https://github.com/oven-sh/bun/issues/15010.
55
+ */
56
+ const WATCH_FILE_POLL_MS = 2_000;
57
+
50
58
  export class ConfigWatcher {
51
59
  private watchers: FSWatcher[] = [];
52
- private debounceTimers = new DebouncerMap({
53
- defaultDelayMs: 200,
54
- maxEntries: 1000,
55
- protectedKeyPrefix: "__",
56
- });
60
+ private watchedFiles: Set<string> = new Set();
61
+ private stopped = false;
62
+ private debounceTimers: DebouncerMap;
57
63
  private suppressReload = false;
58
64
  lastFingerprint = "";
65
+ private lastConfig: ReturnType<typeof getConfig> | null = null;
59
66
  private lastRefreshTime = 0;
60
67
 
61
68
  static readonly REFRESH_INTERVAL_MS = 30_000;
62
69
 
70
+ /**
71
+ * @param pollIntervalMs Per-file stat poll interval (passed to
72
+ * `fs.watchFile`). Default `WATCH_FILE_POLL_MS` (2s); tests pass a
73
+ * smaller value for fast turnaround.
74
+ * @param debounceMs Debounce window applied to any detected file
75
+ * change before invoking its handler. Default 200ms; tests pass a
76
+ * smaller value to avoid sleeping unnecessarily.
77
+ */
78
+ constructor(
79
+ private readonly pollIntervalMs: number = WATCH_FILE_POLL_MS,
80
+ debounceMs = 200,
81
+ ) {
82
+ this.debounceTimers = new DebouncerMap({
83
+ defaultDelayMs: debounceMs,
84
+ maxEntries: 1000,
85
+ protectedKeyPrefix: "__",
86
+ });
87
+ }
88
+
63
89
  /** Expose the debounce timers so handlers can schedule debounced work. */
64
90
  get timers(): DebouncerMap {
65
91
  return this.debounceTimers;
@@ -88,12 +114,15 @@ export class ConfigWatcher {
88
114
 
89
115
  /** Initialize the config fingerprint (call after first config load). */
90
116
  initFingerprint(config: ReturnType<typeof getConfig>): void {
117
+ this.lastConfig = config;
91
118
  this.lastFingerprint = this.configFingerprint(config);
92
119
  }
93
120
 
94
121
  /** Update the fingerprint to match the current config. */
95
122
  updateFingerprint(): void {
96
- this.lastFingerprint = this.configFingerprint(getConfig());
123
+ const config = getConfig();
124
+ this.lastConfig = config;
125
+ this.lastFingerprint = this.configFingerprint(config);
97
126
  this.lastRefreshTime = Date.now();
98
127
  }
99
128
 
@@ -103,7 +132,7 @@ export class ConfigWatcher {
103
132
  * Returns true if config actually changed.
104
133
  */
105
134
  async refreshConfigFromSources(): Promise<boolean> {
106
- const prevCleanup = safeGetCleanupConfig();
135
+ const prevCleanup = this.lastConfig?.memory?.cleanup;
107
136
  invalidateConfigCache();
108
137
  const config = getConfig();
109
138
  const fingerprint = this.configFingerprint(config);
@@ -120,6 +149,7 @@ export class ConfigWatcher {
120
149
  }
121
150
  const isFirstInit = this.lastFingerprint === "";
122
151
  await initializeProviders(config);
152
+ this.lastConfig = config;
123
153
  this.lastFingerprint = fingerprint;
124
154
  return !isFirstInit;
125
155
  }
@@ -136,19 +166,24 @@ export class ConfigWatcher {
136
166
  onAvatarChanged?: () => void,
137
167
  onConfigChanged?: () => void,
138
168
  ): void {
169
+ // Reset the stopped flag so a stop()→start() cycle on the same
170
+ // instance resumes hot-reload instead of silently bailing in every
171
+ // watchFile callback. This matters because getConfigWatcher() is a
172
+ // module-level singleton — a daemon restart path that reuses it
173
+ // would otherwise be permanently mute.
174
+ this.stopped = false;
139
175
  const workspaceDir = getWorkspaceDir();
140
176
 
141
177
  const workspaceHandlers: Record<string, () => void> = {
142
178
  "config.json": async () => {
143
179
  if (this.suppressReload) return;
144
180
  try {
145
- const prevConfig = getConfig();
146
- const prevMcpFingerprint = JSON.stringify(prevConfig.mcp ?? {});
181
+ const prevMcpFingerprint = JSON.stringify(this.lastConfig?.mcp ?? {});
147
182
  const changed = await this.refreshConfigFromSources();
148
183
  if (changed) {
149
184
  onConversationEvict();
150
185
  onConfigChanged?.();
151
- const newConfig = getConfig();
186
+ const newConfig = this.lastConfig ?? getConfig();
152
187
  const newMcpFingerprint = JSON.stringify(newConfig.mcp ?? {});
153
188
  if (newMcpFingerprint !== prevMcpFingerprint) {
154
189
  reloadMcpServers().catch((err: unknown) => {
@@ -170,37 +205,11 @@ export class ConfigWatcher {
170
205
  },
171
206
  };
172
207
 
173
- const watchDir = (
174
- dir: string,
175
- handlers: Record<string, () => void>,
176
- label: string,
177
- ): void => {
178
- try {
179
- const watcher = watch(dir, (_eventType, filename) => {
180
- if (!filename) return;
181
- const file = String(filename);
182
- if (!handlers[file]) return;
183
- this.debounceTimers.schedule(`file:${file}`, () => {
184
- log.info({ file }, "File changed, reloading");
185
- handlers[file]();
186
- });
187
- });
188
- attachWatcherErrorHandler(watcher, dir);
189
- this.watchers.push(watcher);
190
- log.info({ dir }, `Watching ${label}`);
191
- } catch (err) {
192
- log.warn(
193
- { err, dir },
194
- `Failed to watch ${label}. Hot-reload will be unavailable.`,
195
- );
196
- }
197
- };
198
-
199
- watchDir(
200
- workspaceDir,
201
- workspaceHandlers,
202
- "workspace directory for config/prompt changes",
203
- );
208
+ // Per-file watches; don't watch the workspace directory itself because
209
+ // it contains socket files.
210
+ for (const [filename, handler] of Object.entries(workspaceHandlers)) {
211
+ this.watchFile(join(workspaceDir, filename), handler, filename);
212
+ }
204
213
 
205
214
  if (onSoundsConfigChanged) {
206
215
  this.startSoundsWatcher(onSoundsConfigChanged);
@@ -215,13 +224,46 @@ export class ConfigWatcher {
215
224
  }
216
225
 
217
226
  stop(): void {
227
+ this.stopped = true;
218
228
  this.debounceTimers.cancelAll();
229
+ for (const filePath of this.watchedFiles) {
230
+ unwatchFile(filePath);
231
+ }
232
+ this.watchedFiles.clear();
219
233
  for (const watcher of this.watchers) {
220
234
  watcher.close();
221
235
  }
222
236
  this.watchers = [];
223
237
  }
224
238
 
239
+ private watchFile(
240
+ filePath: string,
241
+ handler: () => void,
242
+ label: string,
243
+ ): void {
244
+ // Match the defensive pattern used by every other startXWatcher in
245
+ // this file: log the failure and continue. Per AGENTS.md, the daemon
246
+ // must never block startup — a watchFile() throw on some platform
247
+ // edge case must not propagate up to DaemonServer.start().
248
+ try {
249
+ watchFile(filePath, { interval: this.pollIntervalMs }, (curr, prev) => {
250
+ if (this.stopped) return;
251
+ if (curr.ino === prev.ino && curr.mtimeMs === prev.mtimeMs) return;
252
+ this.debounceTimers.schedule(`file:${filePath}`, () => {
253
+ log.info({ file: filePath }, "File changed, reloading");
254
+ handler();
255
+ });
256
+ });
257
+ this.watchedFiles.add(filePath);
258
+ log.info({ file: filePath }, `Watching ${label}`);
259
+ } catch (err) {
260
+ log.warn(
261
+ { err, file: filePath },
262
+ `Failed to watch ${label}. Hot-reload will be unavailable until restart.`,
263
+ );
264
+ }
265
+ }
266
+
225
267
  private startSoundsWatcher(onSoundsConfigChanged: () => void): void {
226
268
  const soundsDir = getSoundsDir();
227
269
  try {
@@ -341,7 +383,6 @@ export class ConfigWatcher {
341
383
  string,
342
384
  (filename: string) => void | Promise<void>
343
385
  > = {
344
- "bash.": handleBashSignal,
345
386
  "user-message.": handleUserMessageSignal,
346
387
  };
347
388
 
@@ -486,20 +527,6 @@ export class ConfigWatcher {
486
527
  }
487
528
  }
488
529
 
489
- /**
490
- * Snapshot the current cleanup config so we can compare it against the
491
- * post-reload value. Tolerant of config-load failures — if the config can't
492
- * be read (e.g. first-load), returns undefined so the comparison below
493
- * treats it as "no previous value".
494
- */
495
- function safeGetCleanupConfig(): MemoryCleanupConfig | undefined {
496
- try {
497
- return getConfig().memory?.cleanup;
498
- } catch {
499
- return undefined;
500
- }
501
- }
502
-
503
530
  /**
504
531
  * Return true if any cleanup field the user can change via the UI differs
505
532
  * between the previous and next config snapshots. Used to decide whether to
@@ -530,6 +557,7 @@ export function cleanupSettingsChanged(
530
557
  return (
531
558
  prev.llmRequestLogRetentionMs !== next.llmRequestLogRetentionMs ||
532
559
  prev.conversationRetentionDays !== next.conversationRetentionDays ||
560
+ prev.traceEventRetentionDays !== next.traceEventRetentionDays ||
533
561
  prev.enabled !== next.enabled
534
562
  );
535
563
  }
@@ -137,6 +137,8 @@ export interface EventHandlerState {
137
137
  readonly directiveWarnings: string[];
138
138
  readonly toolUseIdToName: Map<string, string>;
139
139
  currentTurnToolNames: string[];
140
+ /** Sticky for the whole run: this turn created/refreshed an app. */
141
+ appBuildToolUsedThisRun: boolean;
140
142
  /** Tracks whether the first text delta has been emitted this turn for activity state transitions. */
141
143
  firstTextDeltaEmitted: boolean;
142
144
  /** Tracks whether a thinking delta has been emitted this turn for activity state transitions. */
@@ -168,6 +170,16 @@ export interface EventHandlerState {
168
170
  approvalMode?: string;
169
171
  approvalReason?: string;
170
172
  riskThreshold?: string;
173
+ /** Display-only regex ladder for the rule editor (narrowest → broadest). */
174
+ riskScopeOptions?: Array<{ pattern: string; label: string }>;
175
+ /** Minimatch save patterns for the rule editor (narrowest → broadest). */
176
+ riskAllowlistOptions?: Array<{
177
+ label: string;
178
+ description: string;
179
+ pattern: string;
180
+ }>;
181
+ /** Directory scope ladder for the rule editor. */
182
+ riskDirectoryScopeOptions?: Array<{ scope: string; label: string }>;
171
183
  }
172
184
  >;
173
185
  /** tool_use_ids emitted in the current turn (populated in handleToolUse, cleared after annotation). */
@@ -219,6 +231,7 @@ export function createEventHandlerState(): EventHandlerState {
219
231
  directiveWarnings: [],
220
232
  toolUseIdToName: new Map(),
221
233
  currentTurnToolNames: [],
234
+ appBuildToolUsedThisRun: false,
222
235
  firstTextDeltaEmitted: false,
223
236
  firstThinkingDeltaEmitted: false,
224
237
  lastCompletedToolName: undefined,
@@ -365,6 +378,9 @@ export function handleToolUse(
365
378
  ): void {
366
379
  state.toolUseIdToName.set(event.id, event.name);
367
380
  state.currentTurnToolNames.push(event.name);
381
+ if (event.name === "app_create" || event.name === "app_refresh") {
382
+ state.appBuildToolUsedThisRun = true;
383
+ }
368
384
  state.toolCallTimestamps.set(event.id, { startedAt: Date.now() });
369
385
  state.currentToolUseId = event.id;
370
386
  state.currentTurnToolUseIds.push(event.id);
@@ -548,6 +564,14 @@ export function handleToolResult(
548
564
  approvalMode: event.approvalMode,
549
565
  approvalReason: event.approvalReason,
550
566
  riskThreshold: event.riskThreshold,
567
+ // Capture the 3 risk-option arrays so the persisted tool_use block
568
+ // carries the same chip ladder as the live tool_result event. Without
569
+ // these, hydrated chips from chat history fall back to the synthesized
570
+ // `*` allowlist and an empty scope ladder (see the comment on
571
+ // `synthesizeFallbackOption` in web's RuleEditorModal).
572
+ riskScopeOptions: event.riskScopeOptions,
573
+ riskAllowlistOptions: event.riskAllowlistOptions,
574
+ riskDirectoryScopeOptions: event.riskDirectoryScopeOptions,
551
575
  });
552
576
  }
553
577
 
@@ -627,6 +651,7 @@ export function handleToolResult(
627
651
  matchedTrustRuleId: event.matchedTrustRuleId,
628
652
  isContainerized: event.isContainerized,
629
653
  riskScopeOptions: event.riskScopeOptions,
654
+ riskAllowlistOptions: event.riskAllowlistOptions,
630
655
  riskDirectoryScopeOptions: event.riskDirectoryScopeOptions,
631
656
  approvalMode: event.approvalMode,
632
657
  approvalReason: event.approvalReason,
@@ -688,6 +713,19 @@ function annotatePersistedAssistantMessage(
688
713
  if (risk.approvalMode) rec._approvalMode = risk.approvalMode;
689
714
  if (risk.approvalReason) rec._approvalReason = risk.approvalReason;
690
715
  if (risk.riskThreshold) rec._riskThreshold = risk.riskThreshold;
716
+ // Persist the 3 risk-option arrays so the rule editor's chip ladder
717
+ // survives chat-history reload. These mirror the same-named fields
718
+ // on the live `tool_result` event; clients should read them back via
719
+ // `shared.ts` and treat them identically to the live values.
720
+ if (risk.riskScopeOptions && risk.riskScopeOptions.length > 0)
721
+ rec._riskScopeOptions = risk.riskScopeOptions;
722
+ if (risk.riskAllowlistOptions && risk.riskAllowlistOptions.length > 0)
723
+ rec._riskAllowlistOptions = risk.riskAllowlistOptions;
724
+ if (
725
+ risk.riskDirectoryScopeOptions &&
726
+ risk.riskDirectoryScopeOptions.length > 0
727
+ )
728
+ rec._riskDirectoryScopeOptions = risk.riskDirectoryScopeOptions;
691
729
  modified = true;
692
730
  }
693
731
  }