@vellumai/assistant 0.12.1-staging.1 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (404) hide show
  1. package/AGENTS.md +0 -4
  2. package/ARCHITECTURE.md +12 -11
  3. package/docs/architecture/turn-actor.md +8 -15
  4. package/openapi.yaml +1 -291
  5. package/package.json +1 -1
  6. package/src/__tests__/always-loaded-tools-guard.test.ts +10 -18
  7. package/src/__tests__/app-compiler.test.ts +0 -6
  8. package/src/__tests__/app-control-tool-schemas.test.ts +2 -5
  9. package/src/__tests__/app-dir-path-guard.test.ts +3 -1
  10. package/src/__tests__/app-source-watcher.test.ts +218 -0
  11. package/src/{calls/__tests__ → __tests__}/call-controller.test.ts +82 -503
  12. package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +2 -5
  13. package/src/__tests__/computer-use-tools.test.ts +5 -17
  14. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  15. package/src/__tests__/config-schema.test.ts +29 -28
  16. package/src/__tests__/config-watcher-skill-reseed.test.ts +359 -0
  17. package/src/__tests__/config-watcher.test.ts +8 -23
  18. package/src/__tests__/connection-model-compat.test.ts +2 -2
  19. package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
  20. package/src/__tests__/conversation-pairing.test.ts +0 -26
  21. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +10 -4
  22. package/src/__tests__/conversation-queue.test.ts +207 -48
  23. package/src/__tests__/conversation-retry-discard.test.ts +1 -54
  24. package/src/__tests__/conversation-skill-tools.test.ts +0 -27
  25. package/src/__tests__/conversation-surfaces-app-control.test.ts +3 -8
  26. package/src/__tests__/conversation-surfaces-task-progress.test.ts +0 -67
  27. package/src/__tests__/conversation-title-service.test.ts +10 -14
  28. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +5 -3
  29. package/src/__tests__/conversation-tool-setup-attribution.test.ts +0 -51
  30. package/src/__tests__/credential-record-retire.test.ts +1 -171
  31. package/src/__tests__/credential-routes.test.ts +3 -212
  32. package/src/__tests__/credential-security-invariants.test.ts +1 -2
  33. package/src/__tests__/cu-unified-flow.test.ts +2 -8
  34. package/src/__tests__/drain-kick-guard.test.ts +2 -0
  35. package/src/__tests__/drain-requeue-on-contention.test.ts +6 -0
  36. package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
  37. package/src/__tests__/host-bash-proxy.test.ts +45 -26
  38. package/src/__tests__/host-browser-proxy.test.ts +17 -6
  39. package/src/__tests__/host-file-proxy-targeted.test.ts +0 -23
  40. package/src/__tests__/host-file-proxy.test.ts +23 -11
  41. package/src/__tests__/host-transfer-proxy-targeted.test.ts +9 -30
  42. package/src/__tests__/llm-catalog-parity.test.ts +0 -25
  43. package/src/__tests__/mcp-auth-routes.test.ts +9 -6
  44. package/src/__tests__/mcp-config-secret-boundary.test.ts +136 -78
  45. package/src/__tests__/mcp-health-check.test.ts +11 -8
  46. package/src/__tests__/mcp-list-plugin-servers.test.ts +10 -8
  47. package/src/{calls/__tests__ → __tests__}/media-stream-audio-transcode.test.ts +1 -1
  48. package/src/{calls/__tests__ → __tests__}/media-stream-output.test.ts +8 -37
  49. package/src/{calls/__tests__ → __tests__}/media-stream-parser.test.ts +1 -1
  50. package/src/{calls/__tests__ → __tests__}/media-stream-server-integration.test.ts +57 -150
  51. package/src/{calls/__tests__ → __tests__}/media-stream-stt-session.test.ts +5 -5
  52. package/src/{calls/__tests__ → __tests__}/media-turn-detector.test.ts +1 -1
  53. package/src/__tests__/messages-after-tiebreaker.test.ts +0 -91
  54. package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
  55. package/src/__tests__/messaging-send-tool.test.ts +548 -355
  56. package/src/__tests__/model-intents.test.ts +0 -3
  57. package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
  58. package/src/__tests__/platform-client-verify-credential.test.ts +0 -2
  59. package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -13
  60. package/src/__tests__/script-proxy-certs.test.ts +0 -35
  61. package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
  62. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  63. package/src/__tests__/speaker-identification.test.ts +56 -0
  64. package/src/__tests__/subagent-tool-gate-mode.test.ts +3 -19
  65. package/src/__tests__/subagent-tools.test.ts +2 -3
  66. package/src/__tests__/telephony-tts-guard.test.ts +3 -38
  67. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +0 -4
  68. package/src/{calls/__tests__/voice-session-bridge-turn-flow.test.ts → __tests__/voice-session-bridge.test.ts} +12 -20
  69. package/src/__tests__/watch-retro-tool-availability.test.ts +17 -8
  70. package/src/__tests__/workspace-git-service.test.ts +0 -26
  71. package/src/acp/__tests__/agent-process.test.ts +4 -258
  72. package/src/acp/__tests__/auth-required.test.ts +0 -96
  73. package/src/acp/__tests__/client-handler.test.ts +2 -90
  74. package/src/acp/__tests__/session-manager-persistence.test.ts +1 -5
  75. package/src/acp/__tests__/session-manager-resume.test.ts +6 -270
  76. package/src/acp/__tests__/session-manager-usage.test.ts +2 -4
  77. package/src/acp/__tests__/session-manager.test.ts +4 -895
  78. package/src/acp/agent-process.test.ts +1 -4
  79. package/src/acp/agent-process.ts +9 -94
  80. package/src/acp/auth-required.ts +1 -50
  81. package/src/acp/auto-install.test.ts +7 -9
  82. package/src/acp/client-handler.ts +3 -16
  83. package/src/acp/resolve-agent.test.ts +6 -220
  84. package/src/acp/resolve-agent.ts +23 -88
  85. package/src/acp/session-manager.test.ts +3 -196
  86. package/src/acp/session-manager.ts +18 -441
  87. package/src/acp/types.ts +5 -50
  88. package/src/api/events/app-files-changed.ts +3 -4
  89. package/src/api/index.ts +0 -6
  90. package/src/apps/app-store.ts +1 -1
  91. package/src/bundler/app-compiler.ts +0 -15
  92. package/src/calls/__tests__/voice-control-protocol.test.ts +25 -58
  93. package/src/calls/__tests__/voice-session-bridge.test.ts +1 -37
  94. package/src/calls/__tests__/voice-triage-escalate.test.ts +0 -105
  95. package/src/calls/call-constants.ts +4 -24
  96. package/src/calls/call-controller.ts +199 -499
  97. package/src/calls/call-transport.ts +0 -16
  98. package/src/calls/media-stream-output.ts +0 -32
  99. package/src/calls/media-stream-server.ts +30 -82
  100. package/src/calls/media-stream-stt-session.ts +0 -15
  101. package/src/calls/resolve-call-tts-provider.ts +2 -33
  102. package/src/calls/speaker-identification.ts +233 -0
  103. package/src/calls/tts-call-strategy.ts +5 -9
  104. package/src/calls/types.ts +0 -2
  105. package/src/calls/voice-control-protocol.ts +21 -53
  106. package/src/calls/voice-session-bridge.ts +46 -59
  107. package/src/calls/voice-triage-escalate.ts +56 -175
  108. package/src/cli/commands/apps.help.ts +13 -67
  109. package/src/cli/commands/apps.ts +0 -183
  110. package/src/cli/commands/channels/index.help.ts +2 -70
  111. package/src/cli/commands/channels/index.ts +3 -16
  112. package/src/cli/commands/credentials.help.ts +2 -6
  113. package/src/cli/commands/credentials.ts +2 -2
  114. package/src/cli/commands/mcp.help.ts +8 -8
  115. package/src/cli/commands/mcp.ts +1 -1
  116. package/src/cli/commands/roadmap.help.ts +1 -5
  117. package/src/cli/commands/roadmap.ts +1 -1
  118. package/src/cli/lib/bundled-marketplace.json +90 -103
  119. package/src/config/__tests__/default-profile-catalog.test.ts +0 -22
  120. package/src/config/__tests__/memory-retrospective-schema.test.ts +1 -15
  121. package/src/config/__tests__/platform-rehydration.test.ts +0 -93
  122. package/src/config/acp-defaults.test.ts +3 -13
  123. package/src/config/acp-defaults.ts +6 -23
  124. package/src/config/acp-schema.ts +2 -30
  125. package/src/config/bundled-skills/acp/SKILL.md +14 -22
  126. package/src/config/bundled-skills/acp/TOOLS.json +1 -5
  127. package/src/config/bundled-skills/app-builder/SKILL.md +9 -12
  128. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
  129. package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
  130. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +139 -67
  131. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
  132. package/src/config/bundled-skills/screen-annotation/SKILL.md +1 -8
  133. package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +1 -4
  134. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  135. package/src/config/default-profile-catalog.ts +1 -1
  136. package/src/config/env.ts +2 -2
  137. package/src/config/feature-flag-registry.json +12 -8
  138. package/src/config/memory-v3-gate.test.ts +0 -31
  139. package/src/config/memory-v3-gate.ts +16 -20
  140. package/src/config/platform-rehydration.ts +21 -75
  141. package/src/config/schema.ts +2 -2
  142. package/src/config/schemas/__tests__/live-voice.test.ts +158 -8
  143. package/src/config/schemas/calls.ts +28 -1
  144. package/src/config/schemas/live-voice.ts +173 -1
  145. package/src/config/schemas/mcp.ts +1 -1
  146. package/src/config/schemas/memory-retrospective.ts +0 -10
  147. package/src/config/schemas/skills.ts +14 -0
  148. package/src/credential-execution/process-manager.test.ts +19 -18
  149. package/src/daemon/__tests__/conversation-tool-setup.test.ts +121 -66
  150. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +1 -4
  151. package/src/daemon/app-source-watcher.ts +218 -0
  152. package/src/daemon/config-watcher.ts +194 -13
  153. package/src/daemon/conversation-process.ts +64 -35
  154. package/src/daemon/conversation-skill-tools.ts +31 -13
  155. package/src/daemon/conversation-store.ts +1 -1
  156. package/src/daemon/conversation-surfaces.ts +8 -10
  157. package/src/daemon/conversation-tool-setup.ts +43 -37
  158. package/src/daemon/conversation.ts +3 -0
  159. package/src/daemon/host-bash-proxy.ts +12 -8
  160. package/src/daemon/host-browser-proxy.ts +1 -15
  161. package/src/daemon/host-file-proxy.ts +10 -8
  162. package/src/daemon/host-transfer-proxy.ts +18 -14
  163. package/src/daemon/lifecycle.ts +5 -0
  164. package/src/daemon/mcp-reload-service.ts +5 -5
  165. package/src/daemon/message-types/sync.ts +0 -1
  166. package/src/daemon/providers-setup.ts +1 -1
  167. package/src/daemon/shutdown-handlers.ts +9 -1
  168. package/src/daemon/skill-memory-refresh.ts +0 -64
  169. package/src/daemon/tool-side-effects.ts +18 -7
  170. package/src/hooks/types.ts +0 -33
  171. package/src/inbound/platform-callback-registration.ts +5 -4
  172. package/src/live-voice/__tests__/live-voice-metrics.test.ts +3 -0
  173. package/src/live-voice/__tests__/live-voice-progress.test.ts +15 -15
  174. package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
  175. package/src/{calls → live-voice}/__tests__/progress-narration.test.ts +4 -4
  176. package/src/{calls → live-voice}/__tests__/progress-phrases.test.ts +1 -1
  177. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +69 -13
  178. package/src/live-voice/live-voice-session.ts +693 -213
  179. package/src/{calls → live-voice}/progress-narration.ts +2 -2
  180. package/src/live-voice/protocol.ts +2 -0
  181. package/src/mcp/__tests__/effective-config.test.ts +2 -2
  182. package/src/mcp/effective-config.ts +3 -3
  183. package/src/mcp/mcp-header-store.ts +45 -29
  184. package/src/mcp/startup.ts +4 -8
  185. package/src/messaging/provider.ts +3 -10
  186. package/src/messaging/providers/callback-routing.ts +0 -18
  187. package/src/messaging/providers/channel-transport.ts +0 -56
  188. package/src/messaging/providers/discord/transport.ts +0 -17
  189. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +17 -6
  190. package/src/messaging/providers/slack/adapter.ts +44 -7
  191. package/src/messaging/providers/slack/transport.ts +0 -16
  192. package/src/messaging/providers/telegram-bot/adapter.ts +48 -1
  193. package/src/messaging/providers/telegram-bot/client.ts +26 -0
  194. package/src/messaging/providers/telegram-bot/transport.ts +0 -20
  195. package/src/messaging/providers/whatsapp/adapter.ts +48 -1
  196. package/src/messaging/providers/whatsapp/client.ts +29 -0
  197. package/src/messaging/providers/whatsapp/transport.ts +0 -18
  198. package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +0 -8
  199. package/src/notifications/__tests__/assistant-reply-producer.test.ts +0 -55
  200. package/src/notifications/__tests__/home-feed-side-effect.test.ts +3 -153
  201. package/src/notifications/assistant-reply-producer.ts +3 -8
  202. package/src/notifications/conversation-pairing.ts +16 -36
  203. package/src/notifications/delivered-post-record.ts +1 -11
  204. package/src/notifications/home-feed-side-effect.ts +7 -55
  205. package/src/outbound-proxy/certs.ts +5 -3
  206. package/src/permissions/checker.test.ts +6 -11
  207. package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
  208. package/src/persistence/conversation-crud.ts +63 -71
  209. package/src/persistence/conversation-lineage.ts +4 -12
  210. package/src/persistence/conversation-title-service.ts +26 -9
  211. package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
  212. package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
  213. package/src/persistence/message-reads.ts +0 -30
  214. package/src/persistence/migrations/157-invite-contact-id.ts +6 -18
  215. package/src/persistence/schema/memory-core.ts +0 -5
  216. package/src/platform/client.test.ts +0 -2
  217. package/src/platform/client.ts +10 -2
  218. package/src/plugin-api/constants.ts +0 -2
  219. package/src/plugin-api/index.ts +0 -1
  220. package/src/plugin-api/types.ts +0 -2
  221. package/src/plugins/defaults/index.ts +0 -2
  222. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +0 -46
  223. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +5 -9
  224. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +10 -20
  225. package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +0 -219
  226. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +0 -31
  227. package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +0 -63
  228. package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +0 -4
  229. package/src/plugins/defaults/memory/graph/capability-seed.ts +6 -4
  230. package/src/plugins/defaults/memory/hooks/init.ts +0 -10
  231. package/src/plugins/defaults/memory/hooks/shutdown.ts +0 -2
  232. package/src/plugins/defaults/memory/jobs-worker.ts +0 -13
  233. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +60 -34
  234. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +5 -3
  235. package/src/plugins/defaults/memory/memory-retrospective-job.ts +3 -12
  236. package/src/plugins/defaults/memory/memory-retrospective-state.ts +71 -312
  237. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +1 -2
  238. package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +3 -5
  239. package/src/plugins/defaults/memory/startup.ts +1 -6
  240. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +9 -7
  241. package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
  242. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +0 -89
  243. package/src/plugins/defaults/memory/worker.ts +0 -6
  244. package/src/plugins/mcp-servers.ts +75 -15
  245. package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
  246. package/src/providers/gemini/client.ts +23 -9
  247. package/src/providers/model-catalog.ts +0 -34
  248. package/src/providers/model-intents.ts +5 -10
  249. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +18 -146
  250. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +1 -83
  251. package/src/providers/openai/chat-completions-provider.ts +78 -192
  252. package/src/providers/openai/codex-models.ts +4 -0
  253. package/src/runtime/AGENTS.md +0 -2
  254. package/src/runtime/auth/same-actor.ts +3 -38
  255. package/src/runtime/http-server.ts +151 -160
  256. package/src/runtime/routes/__tests__/acp-routes.test.ts +4 -159
  257. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +1 -108
  258. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +12 -16
  259. package/src/runtime/routes/acp-routes.test.ts +0 -15
  260. package/src/runtime/routes/acp-routes.ts +9 -72
  261. package/src/runtime/routes/app-management-routes.ts +0 -73
  262. package/src/runtime/routes/app-routes.ts +1 -1
  263. package/src/runtime/routes/conversation-list-routes.ts +3 -14
  264. package/src/runtime/routes/conversation-query-routes.ts +0 -40
  265. package/src/runtime/routes/credential-routes.ts +10 -19
  266. package/src/runtime/routes/diagnostics-routes.ts +3 -55
  267. package/src/runtime/routes/index.ts +0 -4
  268. package/src/runtime/routes/log-export-routes.ts +0 -2
  269. package/src/runtime/routes/mcp-auth-routes.ts +47 -39
  270. package/src/runtime/routes/migration-routes.ts +21 -8
  271. package/src/runtime/routes/platform-routes.ts +15 -18
  272. package/src/runtime/services/__tests__/conversation-serializer.test.ts +0 -28
  273. package/src/runtime/services/conversation-serializer.ts +3 -16
  274. package/src/runtime/sync/resource-sync-events.ts +1 -2
  275. package/src/schedule/worker.ts +1 -1
  276. package/src/security/ces-rpc-record-backend.ts +0 -7
  277. package/src/subagent/consult-context.ts +25 -12
  278. package/src/tools/acp/list-agents.test.ts +1 -3
  279. package/src/tools/acp/spawn.test.ts +11 -146
  280. package/src/tools/acp/spawn.ts +3 -21
  281. package/src/tools/acp/status.ts +3 -40
  282. package/src/tools/app-control/skill-proxy-bridge.ts +1 -2
  283. package/src/tools/capability-offer.test.ts +2 -22
  284. package/src/tools/capability-offer.ts +1 -16
  285. package/src/tools/computer-use/definitions.ts +2 -4
  286. package/src/tools/computer-use/skill-proxy-bridge.ts +1 -2
  287. package/src/tools/credentials/metadata-store.ts +0 -100
  288. package/src/tools/host-filesystem/edit.test.ts +1 -2
  289. package/src/tools/host-filesystem/edit.ts +2 -2
  290. package/src/tools/host-filesystem/read.test.ts +1 -2
  291. package/src/tools/host-filesystem/read.ts +2 -2
  292. package/src/tools/host-filesystem/transfer.test.ts +1 -2
  293. package/src/tools/host-filesystem/transfer.ts +2 -2
  294. package/src/tools/host-filesystem/write.test.ts +1 -2
  295. package/src/tools/host-filesystem/write.ts +2 -2
  296. package/src/tools/host-terminal/host-shell.test.ts +0 -16
  297. package/src/tools/host-terminal/host-shell.ts +20 -4
  298. package/src/tools/policy-context.ts +7 -6
  299. package/src/tools/subagent/spawn.ts +8 -5
  300. package/src/tools/watch/watch-retro-report.ts +8 -0
  301. package/src/util/__tests__/language-subtag.test.ts +1 -23
  302. package/src/util/fs-watcher-error.ts +36 -0
  303. package/src/util/language-subtag.ts +0 -16
  304. package/src/util/platform.ts +0 -8
  305. package/src/workspace/git-service.ts +54 -5
  306. package/src/workspace/migrations/registry.ts +0 -6
  307. package/scripts/smoke-desktop-window-theme.ts +0 -186
  308. package/src/__tests__/git-porcelain.test.ts +0 -47
  309. package/src/__tests__/helpers/set-workspace-mcp.ts +0 -11
  310. package/src/__tests__/message-deleted-hook-dispatch.test.ts +0 -119
  311. package/src/__tests__/resolve-app.test.ts +0 -135
  312. package/src/__tests__/source-fingerprint.test.ts +0 -184
  313. package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +0 -336
  314. package/src/acp/__tests__/helpers/acp-model-option.ts +0 -51
  315. package/src/acp/model-config.test.ts +0 -166
  316. package/src/acp/model-config.ts +0 -138
  317. package/src/api/events/acp-session-model-update.test.ts +0 -88
  318. package/src/api/events/acp-session-model-update.ts +0 -46
  319. package/src/apps/resolve-app.ts +0 -81
  320. package/src/apps/source-fingerprint.ts +0 -266
  321. package/src/calls/__tests__/barge-in-guard.test.ts +0 -81
  322. package/src/calls/__tests__/voice-leg-coordinator.test.ts +0 -229
  323. package/src/calls/__tests__/voice-progress-cadence.test.ts +0 -274
  324. package/src/calls/barge-in-guard.ts +0 -118
  325. package/src/calls/voice-leg-coordinator.ts +0 -189
  326. package/src/calls/voice-progress-cadence.ts +0 -423
  327. package/src/cli/commands/__tests__/apps.test.ts +0 -245
  328. package/src/cli/commands/__tests__/roadmap.help.test.ts +0 -19
  329. package/src/cli/commands/channels/__tests__/send.test.ts +0 -218
  330. package/src/cli/commands/channels/send.ts +0 -128
  331. package/src/config/__tests__/platform-identity.test.ts +0 -117
  332. package/src/config/acp-agent-write.ts +0 -43
  333. package/src/config/acp-schema.test.ts +0 -81
  334. package/src/config/platform-identity.ts +0 -144
  335. package/src/config/schemas/__tests__/voice.test.ts +0 -148
  336. package/src/config/schemas/voice.ts +0 -150
  337. package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +0 -133
  338. package/src/daemon/app-change-notify.ts +0 -45
  339. package/src/desktop/__tests__/fake-desktop.ts +0 -119
  340. package/src/desktop/desktop-chrome-policy.test.ts +0 -71
  341. package/src/desktop/desktop-chrome-policy.ts +0 -42
  342. package/src/desktop/desktop-dependencies.test.ts +0 -102
  343. package/src/desktop/desktop-dependencies.ts +0 -306
  344. package/src/desktop/desktop-feature.test.ts +0 -18
  345. package/src/desktop/desktop-feature.ts +0 -19
  346. package/src/desktop/desktop-panel-config.test.ts +0 -135
  347. package/src/desktop/desktop-panel-config.ts +0 -149
  348. package/src/desktop/desktop-session-manager.test.ts +0 -627
  349. package/src/desktop/desktop-session-manager.ts +0 -814
  350. package/src/desktop/desktop-stream-bridge.test.ts +0 -395
  351. package/src/desktop/desktop-stream-bridge.ts +0 -214
  352. package/src/desktop/desktop-wallpaper.test.ts +0 -191
  353. package/src/desktop/desktop-wallpaper.ts +0 -113
  354. package/src/desktop/desktop-window-theme.ts +0 -170
  355. package/src/i18n/AGENTS.md +0 -22
  356. package/src/i18n/__tests__/i18n.test.ts +0 -108
  357. package/src/i18n/index.ts +0 -16
  358. package/src/i18n/locales.ts +0 -93
  359. package/src/i18n/messages.ts +0 -61
  360. package/src/i18n/title.ts +0 -25
  361. package/src/mcp/__tests__/workspace-mcp-config.test.ts +0 -135
  362. package/src/mcp/spec-schema.ts +0 -111
  363. package/src/mcp/workspace-mcp-config.ts +0 -188
  364. package/src/messaging/providers/__tests__/proactive-address.test.ts +0 -95
  365. package/src/persistence/conversation-title-placeholders.ts +0 -23
  366. package/src/persistence/message-cursor.ts +0 -78
  367. package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +0 -149
  368. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +0 -118
  369. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +0 -83
  370. package/src/plugins/defaults/memory/hooks/message-deleted.ts +0 -21
  371. package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +0 -126
  372. package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +0 -79
  373. package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +0 -28
  374. package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +0 -9
  375. package/src/providers/gemini-thought-signature.test.ts +0 -63
  376. package/src/providers/gemini-thought-signature.ts +0 -44
  377. package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +0 -613
  378. package/src/providers/openai/__tests__/google-thought-signature.test.ts +0 -235
  379. package/src/providers/openai/google-thought-signature.ts +0 -320
  380. package/src/runtime/__tests__/channel-send.test.ts +0 -443
  381. package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +0 -89
  382. package/src/runtime/__tests__/live-voice-upgrade.test.ts +0 -58
  383. package/src/runtime/__tests__/runtime-ws-test-utils.ts +0 -72
  384. package/src/runtime/__tests__/watch-stream-upgrade.test.ts +0 -59
  385. package/src/runtime/channel-send.ts +0 -316
  386. package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +0 -205
  387. package/src/runtime/routes/__tests__/channel-send-routes.test.ts +0 -127
  388. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +0 -107
  389. package/src/runtime/routes/channel-send-routes.ts +0 -85
  390. package/src/runtime/routes/desktop-setup-routes.test.ts +0 -74
  391. package/src/runtime/routes/desktop-setup-routes.ts +0 -35
  392. package/src/security/__tests__/ces-http-record-backend.test.ts +0 -137
  393. package/src/security/ces-http-record-backend.ts +0 -120
  394. package/src/skills/__tests__/workspace-skill-md-signature.test.ts +0 -60
  395. package/src/skills/workspace-skill-md-signature.ts +0 -50
  396. package/src/tools/acp/status.test.ts +0 -94
  397. package/src/util/ca-bundle.ts +0 -13
  398. package/src/workspace/git-porcelain.ts +0 -66
  399. package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +0 -258
  400. package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +0 -78
  401. package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +0 -214
  402. package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +0 -102
  403. package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +0 -199
  404. /package/src/{calls → live-voice}/progress-phrases.ts +0 -0
@@ -1,7 +1,30 @@
1
1
  /**
2
- * Tests for host-tool schema visibility. Host tool definitions stay stable
3
- * across interactive and background turns. Chrome extension remains limited
4
- * to host_browser because it is the only host capability it implements.
2
+ * Tests for `isToolActiveForContext` host-tool capability gating.
3
+ *
4
+ * Scenarios verified:
5
+ * - chrome-extension is its own executor and is exempt from the hasNoClient
6
+ * gate (the extension's own popup UI gates commands; there is no SSE
7
+ * interactive approval channel, and chrome-extension turns intentionally
8
+ * run with `hasNoClient: true` because chrome-extension is not in
9
+ * `INTERACTIVE_INTERFACES`).
10
+ * - macos requires a connected SSE client for host tools that flow through
11
+ * the proxy (e.g. host_bash, host_file_*, host_browser), so
12
+ * `hasNoClient: true` denies those on macos.
13
+ * - host_browser IS in the macos capability set — the proxy routes
14
+ * host_browser_request frames to the desktop client via SSE (or via the
15
+ * Chrome extension registry when an extension connection is present).
16
+ *
17
+ * The per-capability check (`supportsHostProxy(transport, capability)`) runs
18
+ * first and is authoritative for structural support, so host_bash and
19
+ * host_file_* are filtered out for chrome-extension regardless of the
20
+ * hasNoClient flag.
21
+ *
22
+ * Cross-client exception: tools whose capabilities are in
23
+ * CROSS_CLIENT_EXPOSED_CAPABILITIES (host_bash, host_file, host_browser)
24
+ * are allowed for non-host-proxy interactive interfaces ("web", "ios")
25
+ * when at least one capable client is connected via the event hub.
26
+ * chrome-extension is excluded as a security boundary, regardless of
27
+ * whether the capability is technically supported elsewhere.
5
28
  */
6
29
 
7
30
  import { beforeEach, describe, expect, mock, test } from "bun:test";
@@ -237,7 +260,7 @@ describe("isToolActiveForContext - Slack task_progress UI exception", () => {
237
260
  expect(isToolActiveForContext("ui_update", ctx)).toBe(true);
238
261
  });
239
262
 
240
- test("ui_dismiss stays active for Slack without dynamic UI support", () => {
263
+ test("ui_dismiss remains hidden for Slack without dynamic UI support", () => {
241
264
  expect(
242
265
  isToolActiveForContext(
243
266
  "ui_dismiss",
@@ -249,45 +272,42 @@ describe("isToolActiveForContext - Slack task_progress UI exception", () => {
249
272
  },
250
273
  }),
251
274
  ),
252
- ).toBe(true);
275
+ ).toBe(false);
253
276
  });
254
277
 
255
- test("Slack UI tools stay active for a clientless turn", () => {
256
- for (const name of ["ui_show", "ui_update", "ui_dismiss"]) {
257
- expect(
258
- isToolActiveForContext(
259
- name,
260
- makeCtx({
261
- hasNoClient: true,
262
- channelCapabilities: {
263
- channel: "slack",
264
- supportsDynamicUi: false,
265
- },
266
- }),
267
- ),
268
- ).toBe(true);
269
- }
278
+ test("Slack task_progress UI tools still require a connected client path", () => {
279
+ expect(
280
+ isToolActiveForContext(
281
+ "ui_show",
282
+ makeCtx({
283
+ hasNoClient: true,
284
+ channelCapabilities: {
285
+ channel: "slack",
286
+ supportsDynamicUi: false,
287
+ },
288
+ }),
289
+ ),
290
+ ).toBe(false);
270
291
  });
271
292
 
272
- test("other non-dynamic channels keep UI surface tools on the wire", () => {
273
- for (const name of ["ui_show", "ui_update", "ui_dismiss"]) {
274
- expect(
275
- isToolActiveForContext(
276
- name,
277
- makeCtx({
278
- hasNoClient: false,
279
- channelCapabilities: {
280
- channel: "telegram",
281
- supportsDynamicUi: false,
282
- },
283
- }),
284
- ),
285
- ).toBe(true);
286
- }
293
+ test("other non-dynamic channels still hide UI surface tools", () => {
294
+ expect(
295
+ isToolActiveForContext(
296
+ "ui_show",
297
+ makeCtx({
298
+ hasNoClient: false,
299
+ channelCapabilities: {
300
+ channel: "telegram",
301
+ supportsDynamicUi: false,
302
+ },
303
+ }),
304
+ ),
305
+ ).toBe(false);
287
306
  });
288
307
  });
289
308
 
290
309
  describe("isToolActiveForContext — host tool capability gating", () => {
310
+ // macOS transport: SSE-based interactive approval required.
291
311
  test("host_bash is active for macOS with a connected client", () => {
292
312
  expect(
293
313
  isToolActiveForContext(
@@ -297,22 +317,27 @@ describe("isToolActiveForContext — host tool capability gating", () => {
297
317
  ).toBe(true);
298
318
  });
299
319
 
300
- test("host_bash remains active for a background macOS turn", () => {
320
+ test("host_bash is NOT active for macOS when hasNoClient is true (security invariant)", () => {
321
+ // macOS uses an SSE-based interactive approval channel. Without a
322
+ // connected client the guardian auto-approve path could execute host
323
+ // commands unattended, so host tools must be denied.
301
324
  expect(
302
325
  isToolActiveForContext(
303
326
  "host_bash",
304
327
  makeCtx({ hasNoClient: true, transportInterface: "macos" }),
305
328
  ),
306
- ).toBe(true);
329
+ ).toBe(false);
307
330
  });
308
- test("host_file_read remains active for a background macOS turn", () => {
331
+
332
+ test("host_file_read is NOT active for macOS when hasNoClient is true", () => {
309
333
  expect(
310
334
  isToolActiveForContext(
311
335
  "host_file_read",
312
336
  makeCtx({ hasNoClient: true, transportInterface: "macos" }),
313
337
  ),
314
- ).toBe(true);
338
+ ).toBe(false);
315
339
  });
340
+
316
341
  test("host_browser is active for macOS with a connected client", () => {
317
342
  // macOS supports host_browser — the proxy routes host_browser_request
318
343
  // frames to the desktop client via SSE (or via the Chrome extension
@@ -325,14 +350,19 @@ describe("isToolActiveForContext — host tool capability gating", () => {
325
350
  ).toBe(true);
326
351
  });
327
352
 
328
- test("host_browser remains active for a background macOS turn", () => {
353
+ test("host_browser is NOT active for macOS when hasNoClient is true", () => {
354
+ // macOS supports host_browser structurally, but without a connected
355
+ // client the host_browser_request frames have no consumer, so the tool
356
+ // is denied.
329
357
  expect(
330
358
  isToolActiveForContext(
331
359
  "host_browser",
332
360
  makeCtx({ hasNoClient: true, transportInterface: "macos" }),
333
361
  ),
334
- ).toBe(true);
362
+ ).toBe(false);
335
363
  });
364
+
365
+ // chrome-extension transport: the extension is its own executor.
336
366
  test("host_browser is active for chrome-extension even when hasNoClient is true", () => {
337
367
  // chrome-extension turns run with `hasNoClient: true` by design because
338
368
  // chrome-extension is not in `INTERACTIVE_INTERFACES` — it is not an
@@ -388,7 +418,10 @@ describe("isToolActiveForContext — host tool capability gating", () => {
388
418
  ).toBe(false);
389
419
  });
390
420
 
391
- test("host_bash remains active without transport metadata for a connected turn", () => {
421
+ // Backwards-compat fallback: no transport plumbed through.
422
+ test("host_bash falls back to hasNoClient gate when transport is undefined (client connected)", () => {
423
+ // Without a transport interface we cannot run the per-capability check,
424
+ // so we fall back to the coarse-grained `hasNoClient` behavior.
392
425
  expect(
393
426
  isToolActiveForContext(
394
427
  "host_bash",
@@ -397,18 +430,20 @@ describe("isToolActiveForContext — host tool capability gating", () => {
397
430
  ).toBe(true);
398
431
  });
399
432
 
400
- test("host_bash remains active without transport metadata for a background turn", () => {
433
+ test("host_bash falls back to hasNoClient gate when transport is undefined (no client)", () => {
401
434
  expect(
402
435
  isToolActiveForContext(
403
436
  "host_bash",
404
437
  makeCtx({ hasNoClient: true, transportInterface: undefined }),
405
438
  ),
406
- ).toBe(true);
439
+ ).toBe(false);
407
440
  });
408
441
  });
409
442
 
410
443
  describe("isToolActiveForContext — cross-client exception (Phase 1: host_bash)", () => {
411
444
  test("host_bash is active for web transport when a host_bash-capable client is connected", () => {
445
+ // Cross-client path: a web turn should see host_bash when a macOS client
446
+ // with host_bash capability is connected via the event hub.
412
447
  mockClientCountByCapability.set("host_bash", 1);
413
448
  expect(
414
449
  isToolActiveForContext(
@@ -418,24 +453,30 @@ describe("isToolActiveForContext — cross-client exception (Phase 1: host_bash)
418
453
  ).toBe(true);
419
454
  });
420
455
 
421
- test("host_bash remains active for web when no capable client is connected", () => {
456
+ test("host_bash is NOT active for web transport when no capable client is connected", () => {
457
+ // No cross-client fallback: hub has no host_bash-capable subscribers.
422
458
  mockClientCountByCapability.set("host_bash", 0);
423
459
  expect(
424
460
  isToolActiveForContext(
425
461
  "host_bash",
426
462
  makeCtx({ hasNoClient: false, transportInterface: "web" }),
427
463
  ),
428
- ).toBe(true);
464
+ ).toBe(false);
429
465
  });
430
- test("host_file_read remains active for web without host_file clients", () => {
466
+
467
+ test("host_file_read is NOT active for web transport when only a host_bash client is connected", () => {
468
+ // The cross-client exception is per-capability: a host_bash-capable
469
+ // client in the hub does not satisfy host_file's exposure check, since
470
+ // listClientsByCapability is queried with the tool's actual capability.
431
471
  mockClientCountByCapability.set("host_bash", 1);
432
472
  expect(
433
473
  isToolActiveForContext(
434
474
  "host_file_read",
435
475
  makeCtx({ hasNoClient: false, transportInterface: "web" }),
436
476
  ),
437
- ).toBe(true);
477
+ ).toBe(false);
438
478
  });
479
+
439
480
  test("host_bash for macos transport is unaffected by the cross-client exception", () => {
440
481
  // macos natively supports host_bash via host proxy — the supportsHostProxy
441
482
  // check passes, so the cross-client branch is never reached.
@@ -448,15 +489,19 @@ describe("isToolActiveForContext — cross-client exception (Phase 1: host_bash)
448
489
  ).toBe(true);
449
490
  });
450
491
 
451
- test("host_bash remains active for a background macOS turn", () => {
492
+ test("host_bash for macos with no client is still denied (security invariant unaffected)", () => {
493
+ // Even with a capable client in the hub, the macos SSE path takes
494
+ // precedence — it passes the supportsHostProxy check, bypasses the
495
+ // cross-client branch, and reaches the hasNoClient gate.
452
496
  mockClientCountByCapability.set("host_bash", 1);
453
497
  expect(
454
498
  isToolActiveForContext(
455
499
  "host_bash",
456
500
  makeCtx({ hasNoClient: true, transportInterface: "macos" }),
457
501
  ),
458
- ).toBe(true);
502
+ ).toBe(false);
459
503
  });
504
+
460
505
  test("host_bash is NOT active for chrome-extension even when a capable client is connected", () => {
461
506
  // Security boundary: chrome-extension only gets host_browser. The
462
507
  // cross-client exception explicitly excludes chrome-extension transport
@@ -470,14 +515,16 @@ describe("isToolActiveForContext — cross-client exception (Phase 1: host_bash)
470
515
  ).toBe(false);
471
516
  });
472
517
 
473
- test("host_bash remains active for a background web turn", () => {
518
+ test("host_bash is NOT active for web transport when hasNoClient is true (no approval UI)", () => {
519
+ // hasNoClient gate: no interactive approval UI available for this turn.
520
+ // Cross-client exception must not bypass this gate.
474
521
  mockClientCountByCapability.set("host_bash", 1);
475
522
  expect(
476
523
  isToolActiveForContext(
477
524
  "host_bash",
478
525
  makeCtx({ hasNoClient: true, transportInterface: "web" }),
479
526
  ),
480
- ).toBe(true);
527
+ ).toBe(false);
481
528
  });
482
529
  });
483
530
 
@@ -500,14 +547,14 @@ describe("isToolActiveForContext — cross-client exposure for host_file_*", ()
500
547
  ).toBe(true);
501
548
  });
502
549
 
503
- test(`${tool} remains active for web when no host_file client is connected`, () => {
550
+ test(`${tool} is NOT exposed for web when no host_file client is connected`, () => {
504
551
  mockClientCountByCapability.set("host_file", 0);
505
552
  expect(
506
553
  isToolActiveForContext(
507
554
  tool,
508
555
  makeCtx({ hasNoClient: false, transportInterface: "web" }),
509
556
  ),
510
- ).toBe(true);
557
+ ).toBe(false);
511
558
  });
512
559
 
513
560
  test(`${tool} is NOT exposed for chrome-extension (security boundary)`, () => {
@@ -523,19 +570,20 @@ describe("isToolActiveForContext — cross-client exposure for host_file_*", ()
523
570
  ).toBe(false);
524
571
  });
525
572
 
526
- test(`${tool} remains active for a background web turn`, () => {
573
+ test(`${tool} is NOT exposed when hasNoClient is true (no approval UI)`, () => {
527
574
  mockClientCountByCapability.set("host_file", 1);
528
575
  expect(
529
576
  isToolActiveForContext(
530
577
  tool,
531
578
  makeCtx({ hasNoClient: true, transportInterface: "web" }),
532
579
  ),
533
- ).toBe(true);
580
+ ).toBe(false);
534
581
  });
535
582
  }
536
583
 
537
- test("host_file_transfer does not depend on the live host_file roster", () => {
538
- mockClientCountByCapability.set("host_file", 0);
584
+ test("listClientsByCapability is queried with the actual capability, not host_bash (regression guard for D5 latent bug)", () => {
585
+ mockClientCountByCapability.set("host_bash", 0);
586
+ mockClientCountByCapability.set("host_file", 1);
539
587
  expect(
540
588
  isToolActiveForContext(
541
589
  "host_file_transfer",
@@ -574,33 +622,37 @@ describe("isToolActiveForContext — cross-client exposure for host_browser", ()
574
622
  ).toBe(true);
575
623
  });
576
624
 
577
- test("host_browser remains active for web when no host_browser client is connected", () => {
625
+ test("host_browser is NOT exposed for web when no host_browser client is connected", () => {
578
626
  mockClientCountByCapability.set("host_browser", 0);
579
627
  expect(
580
628
  isToolActiveForContext(
581
629
  "host_browser",
582
630
  makeCtx({ hasNoClient: false, transportInterface: "web" }),
583
631
  ),
584
- ).toBe(true);
632
+ ).toBe(false);
585
633
  });
586
- test("host_browser remains active for ios when no host_browser client is connected", () => {
634
+
635
+ test("host_browser is NOT exposed for ios when no host_browser client is connected", () => {
587
636
  mockClientCountByCapability.set("host_browser", 0);
588
637
  expect(
589
638
  isToolActiveForContext(
590
639
  "host_browser",
591
640
  makeCtx({ hasNoClient: false, transportInterface: "ios" }),
592
641
  ),
593
- ).toBe(true);
642
+ ).toBe(false);
594
643
  });
595
- test("host_browser remains active for a background web turn", () => {
644
+
645
+ test("host_browser is NOT exposed when hasNoClient is true (no approval UI)", () => {
646
+ // hasNoClient gate: cross-client exception must not bypass this.
596
647
  mockClientCountByCapability.set("host_browser", 1);
597
648
  expect(
598
649
  isToolActiveForContext(
599
650
  "host_browser",
600
651
  makeCtx({ hasNoClient: true, transportInterface: "web" }),
601
652
  ),
602
- ).toBe(true);
653
+ ).toBe(false);
603
654
  });
655
+
604
656
  test("host_browser for macos transport is unaffected by the cross-client exception", () => {
605
657
  // macos natively supports host_browser via host proxy — the
606
658
  // supportsHostProxy check passes, so the cross-client branch is never
@@ -629,7 +681,10 @@ describe("isToolActiveForContext — cross-client exposure for host_browser", ()
629
681
  ).toBe(true);
630
682
  });
631
683
 
632
- test("host_browser does not depend on the live host_browser roster", () => {
684
+ test("listClientsByCapability is queried with host_browser, not host_bash or host_file (per-capability invariant)", () => {
685
+ // Defense against any future regression that hardcodes a different
686
+ // capability in the cross-client check. Only host_browser-capable
687
+ // clients should satisfy host_browser exposure.
633
688
  mockClientCountByCapability.set("host_bash", 1);
634
689
  mockClientCountByCapability.set("host_file", 1);
635
690
  mockClientCountByCapability.set("host_browser", 0);
@@ -638,7 +693,7 @@ describe("isToolActiveForContext — cross-client exposure for host_browser", ()
638
693
  "host_browser",
639
694
  makeCtx({ hasNoClient: false, transportInterface: "web" }),
640
695
  ),
641
- ).toBe(true);
696
+ ).toBe(false);
642
697
  });
643
698
  });
644
699
 
@@ -41,13 +41,10 @@ mock.module("../../mcp/mcp-header-store.js", () => ({
41
41
  }));
42
42
 
43
43
  mock.module("../../config/loader.js", () => ({
44
+ getConfig: () => ({ mcp: { servers: {} } }),
44
45
  invalidateConfigCache: () => {},
45
46
  }));
46
47
 
47
- mock.module("../../mcp/workspace-mcp-config.js", () => ({
48
- loadWorkspaceMcpConfig: () => ({ servers: {} }),
49
- }));
50
-
51
48
  mock.module("../../tools/registry.js", () => ({
52
49
  registerMcpTools: () => [],
53
50
  unregisterAllMcpTools: () => {},
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Filesystem watcher for app source directories.
3
+ *
4
+ * Watches the apps root directory recursively using fs.watch (FSEvents on
5
+ * macOS). When a source file changes, debounces per app ID and calls the
6
+ * provided callback so the server can recompile + refresh surfaces.
7
+ *
8
+ * This catches ALL modification sources (file_edit, file_write, bash, etc.)
9
+ * without relying on individual tool hooks.
10
+ */
11
+
12
+ import { existsSync, type FSWatcher, watch } from "node:fs";
13
+
14
+ import {
15
+ getApp,
16
+ getAppDirPath,
17
+ getAppsDir,
18
+ resolveAppIdByDirName,
19
+ } from "../apps/app-store.js";
20
+ import { compileApp } from "../bundler/app-compiler.js";
21
+ import { broadcastMessage } from "../runtime/assistant-event-hub.js";
22
+ import { publishAppsChanged } from "../runtime/sync/resource-sync-events.js";
23
+ import { updatePublishedAppDeployment } from "../services/published-app-updater.js";
24
+ import { DebouncerMap } from "../util/debounce.js";
25
+ import { attachFsWatcherErrorHandler } from "../util/fs-watcher-error.js";
26
+ import { getLogger } from "../util/logger.js";
27
+ import { allConversations } from "./conversation-registry.js";
28
+ import { refreshSurfacesForApp } from "./conversation-surfaces.js";
29
+
30
+ const log = getLogger("app-source-watcher");
31
+
32
+ const APP_REFRESH_DEBOUNCE_MS = 500;
33
+
34
+ export type AppSourceChangeCallback = (appId: string) => void;
35
+
36
+ /** Process-level singleton; created by {@link startAppSourceWatcher}. */
37
+ let instance: AppSourceWatcher | null = null;
38
+
39
+ /**
40
+ * Ensure the watcher is running. Called by tool-side-effects after the apps
41
+ * directory may have been created (e.g. on first app_create), since the
42
+ * watcher is skipped at startup when the directory doesn't exist yet.
43
+ */
44
+ export function ensureAppSourceWatcher(): void {
45
+ instance?.ensureStarted();
46
+ }
47
+
48
+ /**
49
+ * Resolve app ID from a relative path within the apps directory.
50
+ * Returns null if the path is not an app source file (e.g. dist/, records/).
51
+ */
52
+ function resolveAppIdFromRelPath(relPath: string): string | null {
53
+ const slashIdx = relPath.indexOf("/");
54
+ if (slashIdx === -1) {
55
+ return null;
56
+ } // file directly in apps/ (e.g. .json definition)
57
+
58
+ const dirName = relPath.slice(0, slashIdx);
59
+ const innerPath = relPath.slice(slashIdx + 1);
60
+
61
+ // Skip a git repo at the root of the apps directory. Older installs may
62
+ // still carry a legacy .git there; its objects/index/refs churn constantly,
63
+ // and each event would otherwise hit resolveAppIdByDirName -> existsSync,
64
+ // which is needless work on a path that can never be an app.
65
+ if (dirName === ".git") {
66
+ return null;
67
+ }
68
+
69
+ // Skip non-source directories (include bare directory names for fs.watch events)
70
+ if (
71
+ innerPath === "records" ||
72
+ innerPath.startsWith("records/") ||
73
+ innerPath === "dist" ||
74
+ innerPath.startsWith("dist/")
75
+ ) {
76
+ return null;
77
+ }
78
+
79
+ return resolveAppIdByDirName(dirName);
80
+ }
81
+
82
+ export class AppSourceWatcher {
83
+ private watcher: FSWatcher | null = null;
84
+ private onChange: AppSourceChangeCallback | null = null;
85
+ private debouncer = new DebouncerMap({
86
+ defaultDelayMs: APP_REFRESH_DEBOUNCE_MS,
87
+ maxEntries: 50,
88
+ });
89
+
90
+ start(onChange: AppSourceChangeCallback): void {
91
+ this.onChange = onChange;
92
+ this.tryWatch();
93
+ }
94
+
95
+ /**
96
+ * Ensure the watcher is running. Call after app creation so the watcher
97
+ * starts if the apps directory was created after daemon startup.
98
+ */
99
+ ensureStarted(): void {
100
+ if (this.watcher || !this.onChange) {
101
+ return;
102
+ }
103
+ this.tryWatch();
104
+ }
105
+
106
+ private tryWatch(): void {
107
+ if (this.watcher) {
108
+ return;
109
+ }
110
+
111
+ let appsDir: string;
112
+ try {
113
+ appsDir = getAppsDir();
114
+ } catch {
115
+ log.warn(
116
+ "Could not resolve apps directory; app source watching disabled",
117
+ );
118
+ return;
119
+ }
120
+
121
+ if (!existsSync(appsDir)) {
122
+ log.info("Apps directory does not exist yet; skipping source watcher");
123
+ return;
124
+ }
125
+
126
+ const onChange = this.onChange;
127
+ if (!onChange) {
128
+ return;
129
+ }
130
+
131
+ try {
132
+ this.watcher = watch(
133
+ appsDir,
134
+ { recursive: true },
135
+ (_eventType, filename) => {
136
+ if (!filename) {
137
+ return;
138
+ }
139
+
140
+ const appId = resolveAppIdFromRelPath(filename);
141
+ if (!appId) {
142
+ return;
143
+ }
144
+
145
+ this.debouncer.schedule(`app:${appId}`, () => {
146
+ onChange(appId);
147
+ });
148
+ },
149
+ );
150
+ // Recursive watches over app trees (incl. node_modules) can exhaust the
151
+ // inotify watch limit and emit ENOSPC asynchronously. Without an 'error'
152
+ // listener that unhandled emitter error crashes the daemon.
153
+ attachFsWatcherErrorHandler(this.watcher, log, appsDir);
154
+ log.info("App source watcher started");
155
+ } catch (err) {
156
+ log.warn(
157
+ { err },
158
+ "Failed to watch apps directory; source watching disabled",
159
+ );
160
+ }
161
+ }
162
+
163
+ stop(): void {
164
+ this.debouncer.cancelAll();
165
+ if (this.watcher) {
166
+ this.watcher.close();
167
+ this.watcher = null;
168
+ }
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Handle a detected app source file change. Recompiles the app and refreshes
174
+ * surfaces across ALL conversations.
175
+ */
176
+ function handleAppSourceChange(appId: string): void {
177
+ const app = getApp(appId);
178
+ if (!app) {
179
+ return;
180
+ }
181
+
182
+ const doRefresh = () => {
183
+ for (const conversation of allConversations()) {
184
+ refreshSurfacesForApp(conversation, appId, { fileChange: true });
185
+ }
186
+ broadcastMessage({ type: "app_files_changed", appId });
187
+ publishAppsChanged();
188
+ void updatePublishedAppDeployment(appId);
189
+ };
190
+
191
+ const appDir = getAppDirPath(appId);
192
+ void compileApp(appDir)
193
+ .then((result) => {
194
+ if (!result.ok) {
195
+ log.warn(
196
+ { appId, errors: result.errors },
197
+ "Recompile failed on app source change",
198
+ );
199
+ }
200
+ doRefresh();
201
+ })
202
+ .catch((err) => {
203
+ log.warn({ appId, err }, "Recompile threw on app source change");
204
+ doRefresh();
205
+ });
206
+ }
207
+
208
+ /** Start watching app source directories for the life of the daemon. */
209
+ export function startAppSourceWatcher(): void {
210
+ instance = new AppSourceWatcher();
211
+ instance.start(handleAppSourceChange);
212
+ }
213
+
214
+ /** Stop the app source watcher during daemon shutdown. */
215
+ export function stopAppSourceWatcher(): void {
216
+ instance?.stop();
217
+ instance = null;
218
+ }