@vellumai/assistant 0.12.0 → 0.12.1-dev.202609141913.5e16607

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 (441) hide show
  1. package/AGENTS.md +5 -1
  2. package/ARCHITECTURE.md +11 -12
  3. package/docs/architecture/turn-actor.md +15 -8
  4. package/docs/guardian-request-flow.md +16 -0
  5. package/openapi.yaml +292 -50
  6. package/package.json +1 -1
  7. package/scripts/smoke-desktop-window-theme.ts +186 -0
  8. package/src/__tests__/always-loaded-tools-guard.test.ts +18 -10
  9. package/src/__tests__/app-compiler.test.ts +6 -0
  10. package/src/__tests__/app-control-tool-schemas.test.ts +5 -2
  11. package/src/__tests__/app-dir-path-guard.test.ts +1 -3
  12. package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +5 -2
  13. package/src/__tests__/computer-use-tools.test.ts +17 -5
  14. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  15. package/src/__tests__/config-schema.test.ts +28 -29
  16. package/src/__tests__/config-watcher.test.ts +23 -8
  17. package/src/__tests__/connection-model-compat.test.ts +2 -2
  18. package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
  19. package/src/__tests__/conversation-pairing.test.ts +26 -0
  20. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -10
  21. package/src/__tests__/conversation-queue.test.ts +48 -207
  22. package/src/__tests__/conversation-retry-discard.test.ts +54 -1
  23. package/src/__tests__/conversation-skill-tools.test.ts +27 -0
  24. package/src/__tests__/conversation-surfaces-app-control.test.ts +8 -3
  25. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  26. package/src/__tests__/conversation-title-service.test.ts +14 -10
  27. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +3 -5
  28. package/src/__tests__/conversation-tool-setup-attribution.test.ts +51 -0
  29. package/src/__tests__/credential-record-retire.test.ts +171 -1
  30. package/src/__tests__/credential-routes.test.ts +212 -3
  31. package/src/__tests__/credential-security-invariants.test.ts +4 -2
  32. package/src/__tests__/cu-unified-flow.test.ts +8 -2
  33. package/src/__tests__/document-sync-tags.test.ts +25 -0
  34. package/src/__tests__/drain-kick-guard.test.ts +0 -2
  35. package/src/__tests__/drain-requeue-on-contention.test.ts +0 -6
  36. package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
  37. package/src/__tests__/git-porcelain.test.ts +47 -0
  38. package/src/__tests__/helpers/set-workspace-mcp.ts +11 -0
  39. package/src/__tests__/host-bash-proxy.test.ts +26 -45
  40. package/src/__tests__/host-browser-proxy.test.ts +6 -17
  41. package/src/__tests__/host-file-proxy-targeted.test.ts +23 -0
  42. package/src/__tests__/host-file-proxy.test.ts +11 -23
  43. package/src/__tests__/host-transfer-proxy-targeted.test.ts +30 -9
  44. package/src/__tests__/llm-catalog-parity.test.ts +25 -0
  45. package/src/__tests__/mcp-auth-routes.test.ts +6 -9
  46. package/src/__tests__/mcp-config-secret-boundary.test.ts +78 -136
  47. package/src/__tests__/mcp-health-check.test.ts +8 -11
  48. package/src/__tests__/mcp-list-plugin-servers.test.ts +8 -10
  49. package/src/__tests__/message-deleted-hook-dispatch.test.ts +119 -0
  50. package/src/__tests__/messages-after-tiebreaker.test.ts +91 -0
  51. package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
  52. package/src/__tests__/messaging-send-tool.test.ts +355 -548
  53. package/src/__tests__/model-intents.test.ts +3 -0
  54. package/src/__tests__/notification-vellum-adapter.test.ts +85 -2
  55. package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
  56. package/src/__tests__/platform-callback-registration.test.ts +8 -16
  57. package/src/__tests__/platform-client-verify-credential.test.ts +2 -0
  58. package/src/__tests__/plugin-import-boundary-guard.test.ts +13 -0
  59. package/src/__tests__/resolve-app.test.ts +135 -0
  60. package/src/__tests__/script-proxy-certs.test.ts +35 -0
  61. package/src/__tests__/service-account-token.test.ts +247 -0
  62. package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
  63. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  64. package/src/__tests__/source-fingerprint.test.ts +184 -0
  65. package/src/__tests__/subagent-tool-gate-mode.test.ts +19 -3
  66. package/src/__tests__/subagent-tools.test.ts +3 -2
  67. package/src/__tests__/telephony-tts-guard.test.ts +38 -3
  68. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +4 -0
  69. package/src/__tests__/watch-retro-tool-availability.test.ts +8 -17
  70. package/src/__tests__/workspace-git-service.test.ts +26 -0
  71. package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +336 -0
  72. package/src/acp/__tests__/agent-process.test.ts +258 -4
  73. package/src/acp/__tests__/auth-required.test.ts +96 -0
  74. package/src/acp/__tests__/client-handler.test.ts +90 -2
  75. package/src/acp/__tests__/helpers/acp-model-option.ts +51 -0
  76. package/src/acp/__tests__/session-manager-persistence.test.ts +5 -1
  77. package/src/acp/__tests__/session-manager-resume.test.ts +270 -6
  78. package/src/acp/__tests__/session-manager-usage.test.ts +4 -2
  79. package/src/acp/__tests__/session-manager.test.ts +895 -4
  80. package/src/acp/agent-process.test.ts +4 -1
  81. package/src/acp/agent-process.ts +94 -9
  82. package/src/acp/auth-required.ts +50 -1
  83. package/src/acp/auto-install.test.ts +9 -7
  84. package/src/acp/client-handler.ts +16 -3
  85. package/src/acp/model-config.test.ts +166 -0
  86. package/src/acp/model-config.ts +138 -0
  87. package/src/acp/resolve-agent.test.ts +220 -6
  88. package/src/acp/resolve-agent.ts +88 -23
  89. package/src/acp/session-manager.test.ts +196 -3
  90. package/src/acp/session-manager.ts +441 -18
  91. package/src/acp/types.ts +50 -5
  92. package/src/api/events/acp-session-model-update.test.ts +88 -0
  93. package/src/api/events/acp-session-model-update.ts +46 -0
  94. package/src/api/events/app-files-changed.ts +4 -3
  95. package/src/api/events/notification-intent.ts +2 -0
  96. package/src/api/index.ts +6 -0
  97. package/src/apps/app-store.ts +1 -1
  98. package/src/apps/resolve-app.ts +81 -0
  99. package/src/apps/source-fingerprint.ts +266 -0
  100. package/src/bundler/app-compiler.ts +15 -0
  101. package/src/calls/__tests__/barge-in-guard.test.ts +81 -0
  102. package/src/{__tests__ → calls/__tests__}/call-controller.test.ts +503 -82
  103. package/src/{__tests__ → calls/__tests__}/media-stream-audio-transcode.test.ts +1 -1
  104. package/src/{__tests__ → calls/__tests__}/media-stream-output.test.ts +37 -8
  105. package/src/{__tests__ → calls/__tests__}/media-stream-parser.test.ts +1 -1
  106. package/src/{__tests__ → calls/__tests__}/media-stream-server-integration.test.ts +150 -57
  107. package/src/{__tests__ → calls/__tests__}/media-stream-stt-session.test.ts +5 -5
  108. package/src/{__tests__ → calls/__tests__}/media-turn-detector.test.ts +1 -1
  109. package/src/{live-voice → calls}/__tests__/progress-narration.test.ts +4 -4
  110. package/src/{live-voice → calls}/__tests__/progress-phrases.test.ts +1 -1
  111. package/src/calls/__tests__/voice-control-protocol.test.ts +58 -25
  112. package/src/calls/__tests__/voice-leg-coordinator.test.ts +229 -0
  113. package/src/calls/__tests__/voice-progress-cadence.test.ts +274 -0
  114. package/src/{__tests__/voice-session-bridge.test.ts → calls/__tests__/voice-session-bridge-turn-flow.test.ts} +20 -12
  115. package/src/calls/__tests__/voice-session-bridge.test.ts +37 -1
  116. package/src/calls/__tests__/voice-triage-escalate.test.ts +105 -0
  117. package/src/calls/barge-in-guard.ts +118 -0
  118. package/src/calls/call-constants.ts +24 -4
  119. package/src/calls/call-controller.ts +499 -199
  120. package/src/calls/call-transport.ts +16 -0
  121. package/src/calls/media-stream-output.ts +32 -0
  122. package/src/calls/media-stream-server.ts +82 -30
  123. package/src/calls/media-stream-stt-session.ts +15 -0
  124. package/src/{live-voice → calls}/progress-narration.ts +2 -2
  125. package/src/calls/resolve-call-tts-provider.ts +33 -2
  126. package/src/calls/tts-call-strategy.ts +9 -5
  127. package/src/calls/types.ts +2 -0
  128. package/src/calls/voice-control-protocol.ts +53 -21
  129. package/src/calls/voice-leg-coordinator.ts +189 -0
  130. package/src/calls/voice-progress-cadence.ts +423 -0
  131. package/src/calls/voice-session-bridge.ts +59 -46
  132. package/src/calls/voice-triage-escalate.ts +175 -56
  133. package/src/cli/commands/__tests__/apps.test.ts +245 -0
  134. package/src/cli/commands/__tests__/bash.test.ts +53 -0
  135. package/src/cli/commands/__tests__/roadmap.help.test.ts +19 -0
  136. package/src/cli/commands/apps.help.ts +67 -13
  137. package/src/cli/commands/apps.ts +183 -0
  138. package/src/cli/commands/bash.help.ts +8 -13
  139. package/src/cli/commands/bash.ts +7 -25
  140. package/src/cli/commands/channels/__tests__/send.test.ts +218 -0
  141. package/src/cli/commands/channels/index.help.ts +70 -2
  142. package/src/cli/commands/channels/index.ts +16 -3
  143. package/src/cli/commands/channels/send.ts +128 -0
  144. package/src/cli/commands/credentials.help.ts +6 -2
  145. package/src/cli/commands/credentials.ts +2 -2
  146. package/src/cli/commands/mcp.help.ts +8 -8
  147. package/src/cli/commands/mcp.ts +1 -1
  148. package/src/cli/commands/roadmap.help.ts +5 -1
  149. package/src/cli/commands/roadmap.ts +1 -1
  150. package/src/cli/lib/bundled-marketplace.json +103 -90
  151. package/src/config/__tests__/default-profile-catalog.test.ts +22 -0
  152. package/src/config/__tests__/memory-retrospective-schema.test.ts +15 -1
  153. package/src/config/__tests__/platform-identity.test.ts +255 -0
  154. package/src/config/__tests__/platform-rehydration.test.ts +99 -9
  155. package/src/config/acp-agent-write.ts +43 -0
  156. package/src/config/acp-defaults.test.ts +13 -3
  157. package/src/config/acp-defaults.ts +23 -6
  158. package/src/config/acp-schema.test.ts +81 -0
  159. package/src/config/acp-schema.ts +30 -2
  160. package/src/config/bundled-skills/acp/SKILL.md +22 -14
  161. package/src/config/bundled-skills/acp/TOOLS.json +5 -1
  162. package/src/config/bundled-skills/app-builder/SKILL.md +12 -9
  163. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
  164. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  165. package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
  166. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +67 -139
  167. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
  168. package/src/config/bundled-skills/schedule/SKILL.md +3 -4
  169. package/src/config/bundled-skills/screen-annotation/SKILL.md +8 -1
  170. package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +4 -1
  171. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  172. package/src/config/default-profile-catalog.ts +1 -1
  173. package/src/config/env.ts +2 -2
  174. package/src/config/feature-flag-registry.json +24 -12
  175. package/src/config/loader.ts +1 -0
  176. package/src/config/memory-v3-gate.test.ts +31 -0
  177. package/src/config/memory-v3-gate.ts +20 -16
  178. package/src/config/platform-identity.ts +201 -0
  179. package/src/config/platform-rehydration.ts +11 -27
  180. package/src/config/schema.ts +2 -2
  181. package/src/config/schemas/__tests__/live-voice.test.ts +8 -158
  182. package/src/config/schemas/__tests__/voice.test.ts +148 -0
  183. package/src/config/schemas/calls.ts +1 -28
  184. package/src/config/schemas/live-voice.ts +1 -173
  185. package/src/config/schemas/mcp.ts +1 -1
  186. package/src/config/schemas/memory-retrospective.ts +10 -0
  187. package/src/config/schemas/services.ts +8 -0
  188. package/src/config/schemas/skills.ts +0 -14
  189. package/src/config/schemas/voice.ts +150 -0
  190. package/src/credential-execution/process-manager.test.ts +18 -19
  191. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -121
  192. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +4 -1
  193. package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +133 -0
  194. package/src/daemon/app-change-notify.ts +45 -0
  195. package/src/daemon/config-watcher.ts +13 -194
  196. package/src/daemon/conversation-process.ts +35 -64
  197. package/src/daemon/conversation-skill-tools.ts +13 -31
  198. package/src/daemon/conversation-store.ts +1 -1
  199. package/src/daemon/conversation-surfaces.ts +10 -8
  200. package/src/daemon/conversation-tool-setup.ts +37 -43
  201. package/src/daemon/conversation.ts +0 -3
  202. package/src/daemon/host-bash-proxy.ts +8 -12
  203. package/src/daemon/host-browser-proxy.ts +15 -1
  204. package/src/daemon/host-file-proxy.ts +8 -10
  205. package/src/daemon/host-transfer-proxy.ts +14 -18
  206. package/src/daemon/lifecycle.ts +0 -5
  207. package/src/daemon/mcp-reload-service.ts +5 -5
  208. package/src/daemon/message-types/sync.ts +1 -0
  209. package/src/daemon/orphan-reaper.ts +1 -1
  210. package/src/daemon/providers-setup.ts +1 -1
  211. package/src/daemon/shutdown-handlers.ts +1 -9
  212. package/src/daemon/skill-memory-refresh.ts +64 -0
  213. package/src/daemon/tool-side-effects.ts +7 -18
  214. package/src/desktop/__tests__/fake-desktop.ts +119 -0
  215. package/src/desktop/desktop-chrome-policy.test.ts +71 -0
  216. package/src/desktop/desktop-chrome-policy.ts +42 -0
  217. package/src/desktop/desktop-dependencies.test.ts +102 -0
  218. package/src/desktop/desktop-dependencies.ts +306 -0
  219. package/src/desktop/desktop-feature.test.ts +18 -0
  220. package/src/desktop/desktop-feature.ts +19 -0
  221. package/src/desktop/desktop-panel-config.test.ts +135 -0
  222. package/src/desktop/desktop-panel-config.ts +149 -0
  223. package/src/desktop/desktop-session-manager.test.ts +627 -0
  224. package/src/desktop/desktop-session-manager.ts +814 -0
  225. package/src/desktop/desktop-stream-bridge.test.ts +395 -0
  226. package/src/desktop/desktop-stream-bridge.ts +214 -0
  227. package/src/desktop/desktop-wallpaper.test.ts +191 -0
  228. package/src/desktop/desktop-wallpaper.ts +113 -0
  229. package/src/desktop/desktop-window-theme.ts +170 -0
  230. package/src/home/__tests__/feed-writer.test.ts +22 -0
  231. package/src/home/feed-writer.ts +11 -2
  232. package/src/hooks/types.ts +33 -0
  233. package/src/i18n/AGENTS.md +22 -0
  234. package/src/i18n/__tests__/i18n.test.ts +108 -0
  235. package/src/i18n/index.ts +16 -0
  236. package/src/i18n/locales.ts +93 -0
  237. package/src/i18n/messages.ts +61 -0
  238. package/src/i18n/title.ts +25 -0
  239. package/src/inbound/platform-callback-registration.ts +4 -5
  240. package/src/live-voice/__tests__/live-voice-metrics.test.ts +0 -3
  241. package/src/live-voice/__tests__/live-voice-progress.test.ts +15 -15
  242. package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
  243. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +13 -69
  244. package/src/live-voice/live-voice-session.ts +213 -693
  245. package/src/live-voice/protocol.ts +0 -2
  246. package/src/mcp/__tests__/effective-config.test.ts +2 -2
  247. package/src/mcp/__tests__/workspace-mcp-config.test.ts +135 -0
  248. package/src/mcp/effective-config.ts +3 -3
  249. package/src/mcp/mcp-header-store.ts +29 -45
  250. package/src/mcp/spec-schema.ts +111 -0
  251. package/src/mcp/startup.ts +8 -4
  252. package/src/mcp/workspace-mcp-config.ts +188 -0
  253. package/src/messaging/provider.ts +10 -3
  254. package/src/messaging/providers/__tests__/proactive-address.test.ts +95 -0
  255. package/src/messaging/providers/callback-routing.ts +18 -0
  256. package/src/messaging/providers/channel-transport.ts +56 -0
  257. package/src/messaging/providers/discord/transport.ts +17 -0
  258. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +6 -17
  259. package/src/messaging/providers/slack/adapter.ts +7 -44
  260. package/src/messaging/providers/slack/transport.ts +16 -0
  261. package/src/messaging/providers/telegram-bot/adapter.ts +1 -48
  262. package/src/messaging/providers/telegram-bot/client.ts +0 -26
  263. package/src/messaging/providers/telegram-bot/transport.ts +20 -0
  264. package/src/messaging/providers/whatsapp/adapter.ts +1 -48
  265. package/src/messaging/providers/whatsapp/transport.ts +18 -0
  266. package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +8 -0
  267. package/src/notifications/__tests__/assistant-reply-producer.test.ts +55 -0
  268. package/src/notifications/__tests__/guardian-feed-projection.test.ts +158 -2
  269. package/src/notifications/__tests__/home-feed-side-effect.test.ts +153 -3
  270. package/src/notifications/__tests__/schedule-result-producer.test.ts +99 -3
  271. package/src/notifications/adapters/macos.ts +3 -0
  272. package/src/notifications/assistant-reply-producer.ts +8 -3
  273. package/src/notifications/conversation-pairing.ts +36 -16
  274. package/src/notifications/delivered-post-record.ts +11 -1
  275. package/src/notifications/guardian-feed-projection.ts +125 -5
  276. package/src/notifications/home-feed-side-effect.ts +55 -7
  277. package/src/notifications/schedule-result-producer.ts +66 -22
  278. package/src/oauth/__tests__/seed-providers-managed.test.ts +54 -0
  279. package/src/oauth/byo-connection.test.ts +45 -0
  280. package/src/oauth/byo-connection.ts +36 -6
  281. package/src/oauth/connection-resolver.test.ts +73 -0
  282. package/src/oauth/connection-resolver.ts +95 -2
  283. package/src/oauth/seed-providers.ts +165 -0
  284. package/src/outbound-proxy/certs.ts +3 -5
  285. package/src/permissions/checker.test.ts +11 -6
  286. package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
  287. package/src/persistence/conversation-crud.ts +71 -63
  288. package/src/persistence/conversation-lineage.ts +12 -4
  289. package/src/persistence/conversation-title-placeholders.ts +23 -0
  290. package/src/persistence/conversation-title-service.ts +9 -26
  291. package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
  292. package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
  293. package/src/persistence/message-cursor.ts +78 -0
  294. package/src/persistence/message-reads.ts +30 -0
  295. package/src/persistence/migrations/157-invite-contact-id.ts +18 -6
  296. package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +149 -0
  297. package/src/persistence/schema/memory-core.ts +5 -0
  298. package/src/platform/client.test.ts +4 -2
  299. package/src/platform/client.ts +2 -10
  300. package/src/plugin-api/constants.ts +2 -0
  301. package/src/plugin-api/index.ts +1 -0
  302. package/src/plugin-api/types.ts +2 -0
  303. package/src/plugins/defaults/index.ts +2 -0
  304. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +46 -0
  305. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +118 -0
  306. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +83 -0
  307. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +9 -5
  308. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +20 -10
  309. package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +219 -0
  310. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +31 -0
  311. package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +63 -0
  312. package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +4 -0
  313. package/src/plugins/defaults/memory/graph/capability-seed.ts +4 -6
  314. package/src/plugins/defaults/memory/hooks/init.ts +10 -0
  315. package/src/plugins/defaults/memory/hooks/message-deleted.ts +21 -0
  316. package/src/plugins/defaults/memory/hooks/shutdown.ts +2 -0
  317. package/src/plugins/defaults/memory/jobs-worker.ts +13 -0
  318. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +34 -60
  319. package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +126 -0
  320. package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +79 -0
  321. package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +28 -0
  322. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +3 -5
  323. package/src/plugins/defaults/memory/memory-retrospective-job.ts +12 -3
  324. package/src/plugins/defaults/memory/memory-retrospective-state.ts +312 -71
  325. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +2 -1
  326. package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +5 -3
  327. package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +9 -0
  328. package/src/plugins/defaults/memory/startup.ts +6 -1
  329. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +7 -9
  330. package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
  331. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +89 -0
  332. package/src/plugins/defaults/memory/worker.ts +6 -0
  333. package/src/plugins/mcp-servers.ts +15 -75
  334. package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
  335. package/src/providers/gemini/client.ts +9 -23
  336. package/src/providers/gemini-thought-signature.test.ts +63 -0
  337. package/src/providers/gemini-thought-signature.ts +44 -0
  338. package/src/providers/inference/auth.ts +2 -3
  339. package/src/providers/inference/connection-availability.ts +27 -8
  340. package/src/providers/inference/resolve-auth.ts +22 -4
  341. package/src/providers/inference/service-account-token.ts +248 -0
  342. package/src/providers/model-catalog.ts +34 -0
  343. package/src/providers/model-intents.ts +10 -5
  344. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +146 -18
  345. package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +613 -0
  346. package/src/providers/openai/__tests__/google-thought-signature.test.ts +235 -0
  347. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +83 -1
  348. package/src/providers/openai/chat-completions-provider.ts +192 -78
  349. package/src/providers/openai/codex-models.ts +0 -4
  350. package/src/providers/openai/google-thought-signature.ts +320 -0
  351. package/src/runtime/AGENTS.md +2 -0
  352. package/src/runtime/__tests__/channel-send.test.ts +443 -0
  353. package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +89 -0
  354. package/src/runtime/__tests__/live-voice-upgrade.test.ts +58 -0
  355. package/src/runtime/__tests__/runtime-ws-test-utils.ts +72 -0
  356. package/src/runtime/__tests__/watch-stream-upgrade.test.ts +59 -0
  357. package/src/runtime/auth/same-actor.ts +38 -3
  358. package/src/runtime/channel-send.ts +316 -0
  359. package/src/runtime/http-server.ts +160 -151
  360. package/src/runtime/routes/__tests__/acp-routes.test.ts +159 -4
  361. package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +205 -0
  362. package/src/runtime/routes/__tests__/channel-send-routes.test.ts +127 -0
  363. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +108 -1
  364. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +2 -2
  365. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +107 -0
  366. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +16 -12
  367. package/src/runtime/routes/acp-routes.test.ts +15 -0
  368. package/src/runtime/routes/acp-routes.ts +72 -9
  369. package/src/runtime/routes/app-management-routes.ts +73 -0
  370. package/src/runtime/routes/app-routes.ts +1 -1
  371. package/src/runtime/routes/channel-send-routes.ts +85 -0
  372. package/src/runtime/routes/conversation-list-routes.ts +14 -3
  373. package/src/runtime/routes/conversation-query-routes.ts +40 -0
  374. package/src/runtime/routes/credential-routes.ts +19 -10
  375. package/src/runtime/routes/desktop-setup-routes.test.ts +74 -0
  376. package/src/runtime/routes/desktop-setup-routes.ts +35 -0
  377. package/src/runtime/routes/diagnostics-routes.ts +55 -3
  378. package/src/runtime/routes/index.ts +4 -2
  379. package/src/runtime/routes/log-export-routes.ts +2 -0
  380. package/src/runtime/routes/mcp-auth-routes.ts +39 -47
  381. package/src/runtime/routes/migration-routes.ts +6 -20
  382. package/src/runtime/routes/platform-routes.ts +18 -15
  383. package/src/runtime/services/__tests__/conversation-serializer.test.ts +28 -0
  384. package/src/runtime/services/conversation-serializer.ts +16 -3
  385. package/src/runtime/sync/resource-sync-events.ts +2 -1
  386. package/src/schedule/worker.ts +1 -1
  387. package/src/security/__tests__/ces-http-record-backend.test.ts +137 -0
  388. package/src/security/ces-http-record-backend.ts +120 -0
  389. package/src/security/ces-rpc-record-backend.ts +7 -0
  390. package/src/skills/__tests__/workspace-skill-md-signature.test.ts +60 -0
  391. package/src/skills/workspace-skill-md-signature.ts +50 -0
  392. package/src/subagent/consult-context.ts +12 -25
  393. package/src/tools/acp/list-agents.test.ts +3 -1
  394. package/src/tools/acp/spawn.test.ts +146 -11
  395. package/src/tools/acp/spawn.ts +21 -3
  396. package/src/tools/acp/status.test.ts +94 -0
  397. package/src/tools/acp/status.ts +40 -3
  398. package/src/tools/app-control/skill-proxy-bridge.ts +2 -1
  399. package/src/tools/capability-offer.test.ts +22 -2
  400. package/src/tools/capability-offer.ts +16 -1
  401. package/src/tools/computer-use/definitions.ts +4 -2
  402. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -1
  403. package/src/tools/credentials/metadata-store.ts +100 -0
  404. package/src/tools/host-filesystem/edit.test.ts +2 -1
  405. package/src/tools/host-filesystem/edit.ts +2 -2
  406. package/src/tools/host-filesystem/read.test.ts +2 -1
  407. package/src/tools/host-filesystem/read.ts +2 -2
  408. package/src/tools/host-filesystem/transfer.test.ts +2 -1
  409. package/src/tools/host-filesystem/transfer.ts +2 -2
  410. package/src/tools/host-filesystem/write.test.ts +2 -1
  411. package/src/tools/host-filesystem/write.ts +2 -2
  412. package/src/tools/host-terminal/host-shell.test.ts +16 -0
  413. package/src/tools/host-terminal/host-shell.ts +4 -20
  414. package/src/tools/policy-context.ts +6 -7
  415. package/src/tools/subagent/spawn.ts +5 -8
  416. package/src/tools/terminal/__tests__/safe-env.test.ts +6 -6
  417. package/src/tools/terminal/__tests__/sanitized-bash.test.ts +57 -0
  418. package/src/tools/terminal/safe-env.ts +23 -3
  419. package/src/tools/terminal/sanitized-bash.ts +105 -0
  420. package/src/tools/watch/watch-retro-report.ts +0 -8
  421. package/src/util/__tests__/language-subtag.test.ts +23 -1
  422. package/src/util/ca-bundle.ts +13 -0
  423. package/src/util/language-subtag.ts +16 -0
  424. package/src/util/platform.ts +8 -0
  425. package/src/workspace/git-porcelain.ts +66 -0
  426. package/src/workspace/git-service.ts +5 -54
  427. package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +258 -0
  428. package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +78 -0
  429. package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +214 -0
  430. package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +102 -0
  431. package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +199 -0
  432. package/src/workspace/migrations/registry.ts +6 -0
  433. package/src/__tests__/app-source-watcher.test.ts +0 -218
  434. package/src/__tests__/config-watcher-skill-reseed.test.ts +0 -359
  435. package/src/__tests__/speaker-identification.test.ts +0 -56
  436. package/src/calls/speaker-identification.ts +0 -233
  437. package/src/daemon/app-source-watcher.ts +0 -218
  438. package/src/messaging/providers/whatsapp/client.ts +0 -29
  439. package/src/runtime/routes/debug-bash-routes.ts +0 -173
  440. package/src/util/fs-watcher-error.ts +0 -36
  441. /package/src/{live-voice → calls}/progress-phrases.ts +0 -0
@@ -0,0 +1,247 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+
3
+ // ── Mutable state for injected vault ────────────────────────────────────────
4
+
5
+ const vaultStore = new Map<string, string>();
6
+ const writtenKeys = new Map<string, string>();
7
+
8
+ // ── Import under test ────────────────────────────────────────────────────────
9
+ //
10
+ // No mock.module — prevents all bun worker contamination. Vault access is
11
+ // injected through _injectVaultAccessors / _resetVaultAccessors instead.
12
+
13
+ const {
14
+ getValidServiceAccountToken,
15
+ parseServiceAccountKey,
16
+ _resetServiceAccountMutex,
17
+ _injectVaultAccessors,
18
+ _resetVaultAccessors,
19
+ } = await import("../providers/inference/service-account-token.js");
20
+
21
+ // ── Hooks ────────────────────────────────────────────────────────────────────
22
+
23
+ beforeEach(() => {
24
+ vaultStore.clear();
25
+ writtenKeys.clear();
26
+ _resetServiceAccountMutex();
27
+ _injectVaultAccessors(
28
+ async (key: string) => vaultStore.get(key),
29
+ async (key: string, value: string) => {
30
+ vaultStore.set(key, value);
31
+ writtenKeys.set(key, value);
32
+ return true;
33
+ },
34
+ );
35
+ });
36
+
37
+ afterEach(() => {
38
+ _resetVaultAccessors();
39
+ });
40
+
41
+ // ── Real 2048-bit RSA PKCS#8 key generated offline for unit tests only ───────
42
+
43
+ const TEST_PRIVATE_KEY =
44
+ "-----BEGIN PRIVATE KEY-----\n" +
45
+ "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDFnEC0wI/cNKyO\n" +
46
+ "XzkqOGAvsTTx4Q+DPjDBPVoy3yHVk7cGYIf5sg3nsrQlE4ARMEQ9MXKUvw+VY59c\n" +
47
+ "zzpy9/JnCN9v5ymZUrowF/87Xn0muklHOhYPFz+73BunncH0Dk0mgCRWEfd/xoGB\n" +
48
+ "bMI40mI2hPMN5aGU4moTmVzTsgvatp0tAk90oaUQBZ/Y3PVjoR+SHbKhQtZW8eiO\n" +
49
+ "zUGyBhzmlmMZcquWxHui8QbO//kEKg7jv2+7npkdW/S0meoDeKZzqRNACKci1CQB\n" +
50
+ "N/skFqy6B8qYekhVYtAaIQOLtuD+VdLL7azMBqIHZSVQQXD6KisN8XSj95k/Fyeb\n" +
51
+ "UEmhvlwrAgMBAAECggEALKr7PWBhpGcLskr4AwEnTOCDlUFIXmRKZQPXRbSGU17G\n" +
52
+ "elyLn/eNiRgt/EuCIWCFrEdnHRRrE1unhDUbgy1kz8GDsCnSj2PRzlk+Sk87fuuQ\n" +
53
+ "8NbCcbrRn0dLwQG5+87XxaIllIcPKQTX9Ud+R5XWuXgn8LY9Zggf7jo2b2vJnjBO\n" +
54
+ "2SIS4M24a8x/1hHLjrTKtyO1yF3TI/9z72z4+yYiXAHxBugCcg6kv27pMEtOl5hv\n" +
55
+ "HW6ogBIDKVIVp3DmjaWxPawy1ytxpI1mR1lfGKoNI3iutLNfuHl3MwinYL7oCiL/\n" +
56
+ "rejuz37rXicFZ40YTKHDqKy+brLS6Ahg0R7XxSNtcQKBgQD7i4oh4stD+geFoPoJ\n" +
57
+ "o/grtOazn2GKuMOrcbBSaTfs4PZfcSRT8Egzhl3Vf/3Z30T3ChugFKkpN9naDEL3\n" +
58
+ "0SEH5sR5oMhGzzE3ncwTPR4fClpiX61JIY6iVL5AQukXIK+v+hCuhRu9YepMbGWq\n" +
59
+ "ndl/uH4LofuEctJYaovnghKedwKBgQDJHC7NiqNhGB6YzwgvYK2GVrFLdPpcfLbl\n" +
60
+ "y5DZKaVYCs3cLFCH7qHEdsmVEWkQ9UkRhxNPGpgQadvYNAd9Bbf2au2lfNuCt0Kn\n" +
61
+ "dYDgkxHAo4PByEznOBgQYoG5IjF8bdubQJbJHQh/bj5EkzeKDggD37ED0fTdWIiJ\n" +
62
+ "Nhh0/q2Y7QKBgGlLMsoWbjGJMqbcCG4KpfJdqghcWe9Agh982mrUwmU0kczHxoYg\n" +
63
+ "WYi/4P1iszcE/5BNOd0baOfpChb89PdZIfN1xJ6JD+ycBCUct3UCK16g7c6p8BZh\n" +
64
+ "ppwKzwyFNZ7JvuDx0xPKgh8XIoVVBoWRx55v8ssZxffePHBoQSHqT7LnAoGAB1vO\n" +
65
+ "opq2C7F0PScmJ4WkE8xMRHSNTKELmDDwpeGOOUB789hzQeYLaULncVrnu0UeXVPR\n" +
66
+ "8w0ID2GwN6L/uyVga9XRyEJfTdMizznPc2guOBxDj+2iUruMtdzE3l9P7Dug03dz\n" +
67
+ "jPxQ+UixYpT4bUbCKFIhCgC+svKwXETsdCt1cqkCgYBVSSabJNSvFfqYz/VJbRK7\n" +
68
+ "cvxEr3mcV/I/+5BoyTmTKQGkIRl6xCMhqZk019hXbdN66jMQ9zg6wni4UKA43cXI\n" +
69
+ "/3NHY86IvdpLORlYqKVHycZsqSVQE6Z3V1G4C2wVKUZtWsoT6Wr+Ind+V+VzbDD2\n" +
70
+ "wHbW4FhxHN9iwdHfmJyq+g==\n" +
71
+ "-----END PRIVATE KEY-----\n";
72
+
73
+ const TEST_SERVICE_ACCOUNT = JSON.stringify({
74
+ type: "service_account",
75
+ client_email: "test@example.com",
76
+ private_key: TEST_PRIVATE_KEY,
77
+ token_uri: "https://oauth2.googleapis.com/token",
78
+ });
79
+
80
+ // ── Tests ────────────────────────────────────────────────────────────────────
81
+
82
+ describe("service-account-token", () => {
83
+ // -- parseServiceAccountKey ------------------------------------------------
84
+
85
+ describe("parseServiceAccountKey", () => {
86
+ test("returns null for empty string", () => {
87
+ expect(parseServiceAccountKey("")).toBeNull();
88
+ });
89
+
90
+ test("returns null for invalid JSON", () => {
91
+ expect(parseServiceAccountKey("not-json")).toBeNull();
92
+ });
93
+
94
+ test("returns null when required fields are missing", () => {
95
+ expect(
96
+ parseServiceAccountKey(
97
+ JSON.stringify({ client_email: "test@example.com" }),
98
+ ),
99
+ ).toBeNull();
100
+ });
101
+
102
+ test("returns the key when all required fields are present", () => {
103
+ const key = parseServiceAccountKey(TEST_SERVICE_ACCOUNT);
104
+ expect(key).not.toBeNull();
105
+ expect(key?.client_email).toBe("test@example.com");
106
+ });
107
+ });
108
+
109
+ // -- getValidServiceAccountToken -------------------------------------------
110
+
111
+ test("returns not_found when no credential is stored", async () => {
112
+ const result = await getValidServiceAccountToken("credential/vertex-ai");
113
+ expect(result).toEqual({ ok: false, reason: "not_found" });
114
+ });
115
+
116
+ test("returns invalid_config when credential is not valid JSON", async () => {
117
+ vaultStore.set("credential/vertex-ai", "not-json");
118
+ const result = await getValidServiceAccountToken("credential/vertex-ai");
119
+ expect(result).toEqual({ ok: false, reason: "invalid_config" });
120
+ });
121
+
122
+ test("returns invalid_config when credential JSON is missing required fields", async () => {
123
+ vaultStore.set(
124
+ "credential/vertex-ai",
125
+ JSON.stringify({ client_email: "missing-other-fields@example.com" }),
126
+ );
127
+ const result = await getValidServiceAccountToken("credential/vertex-ai");
128
+ expect(result).toEqual({ ok: false, reason: "invalid_config" });
129
+ });
130
+
131
+ test("returns cached token when the cache blob is still fresh", async () => {
132
+ const futureExpiry = Math.floor(Date.now() / 1000) + 7200;
133
+ vaultStore.set("credential/vertex-ai", TEST_SERVICE_ACCOUNT);
134
+ vaultStore.set(
135
+ "credential/vertex-ai/token_cache",
136
+ JSON.stringify({
137
+ access_token: "cached-token",
138
+ expires_at: futureExpiry,
139
+ }),
140
+ );
141
+
142
+ const result = await getValidServiceAccountToken("credential/vertex-ai");
143
+ expect(result).toEqual({ ok: true, token: "cached-token" });
144
+ // Nothing new should have been written (no exchange performed).
145
+ expect(writtenKeys.size).toBe(0);
146
+ });
147
+
148
+ test("returns cached token when cache blob has no expiry", async () => {
149
+ vaultStore.set("credential/vertex-ai", TEST_SERVICE_ACCOUNT);
150
+ vaultStore.set(
151
+ "credential/vertex-ai/token_cache",
152
+ JSON.stringify({ access_token: "no-expiry-token" }),
153
+ );
154
+
155
+ const result = await getValidServiceAccountToken("credential/vertex-ai");
156
+ expect(result).toEqual({ ok: true, token: "no-expiry-token" });
157
+ });
158
+
159
+ test("coalesces concurrent callers for the same credential onto one exchange", async () => {
160
+ const pastExpiry = Math.floor(Date.now() / 1000) - 600;
161
+ vaultStore.set("credential/vertex-ai", TEST_SERVICE_ACCOUNT);
162
+ vaultStore.set(
163
+ "credential/vertex-ai/token_cache",
164
+ JSON.stringify({ access_token: "stale-token", expires_at: pastExpiry }),
165
+ );
166
+
167
+ let fetchCallCount = 0;
168
+ const originalFetch = globalThis.fetch;
169
+ globalThis.fetch = (async () => {
170
+ fetchCallCount += 1;
171
+ return {
172
+ ok: true,
173
+ json: async () => ({ access_token: "new-token", expires_in: 3600 }),
174
+ } as Response;
175
+ }) as unknown as typeof fetch;
176
+
177
+ try {
178
+ const [r1, r2] = await Promise.all([
179
+ getValidServiceAccountToken("credential/vertex-ai"),
180
+ getValidServiceAccountToken("credential/vertex-ai"),
181
+ ]);
182
+
183
+ expect(r1).toEqual({ ok: true, token: "new-token" });
184
+ expect(r2).toEqual({ ok: true, token: "new-token" });
185
+ expect(fetchCallCount).toBe(1);
186
+ } finally {
187
+ globalThis.fetch = originalFetch;
188
+ }
189
+ });
190
+
191
+ test("concurrent callers for different credentials exchange independently", async () => {
192
+ vaultStore.set("credential/vertex-a", TEST_SERVICE_ACCOUNT);
193
+ vaultStore.set("credential/vertex-b", TEST_SERVICE_ACCOUNT);
194
+
195
+ let fetchCallCount = 0;
196
+ const originalFetch = globalThis.fetch;
197
+ globalThis.fetch = (async () => {
198
+ fetchCallCount += 1;
199
+ return {
200
+ ok: true,
201
+ json: async () => ({
202
+ access_token: `token-${fetchCallCount}`,
203
+ expires_in: 3600,
204
+ }),
205
+ } as Response;
206
+ }) as unknown as typeof fetch;
207
+
208
+ try {
209
+ const [r1, r2] = await Promise.all([
210
+ getValidServiceAccountToken("credential/vertex-a"),
211
+ getValidServiceAccountToken("credential/vertex-b"),
212
+ ]);
213
+
214
+ expect(r1.ok).toBe(true);
215
+ expect(r2.ok).toBe(true);
216
+ // Each credential triggers its own exchange.
217
+ expect(fetchCallCount).toBe(2);
218
+ } finally {
219
+ globalThis.fetch = originalFetch;
220
+ }
221
+ });
222
+
223
+ test("caches token+expiry as a single JSON blob", async () => {
224
+ vaultStore.set("credential/vertex-ai", TEST_SERVICE_ACCOUNT);
225
+
226
+ const originalFetch = globalThis.fetch;
227
+ globalThis.fetch = (async () => ({
228
+ ok: true,
229
+ json: async () => ({ access_token: "fresh-token", expires_in: 3600 }),
230
+ })) as unknown as typeof fetch;
231
+
232
+ try {
233
+ await getValidServiceAccountToken("credential/vertex-ai");
234
+
235
+ const blob = vaultStore.get("credential/vertex-ai/token_cache");
236
+ expect(blob).toBeDefined();
237
+ const parsed = JSON.parse(blob!) as {
238
+ access_token: string;
239
+ expires_at: number;
240
+ };
241
+ expect(parsed.access_token).toBe("fresh-token");
242
+ expect(typeof parsed.expires_at).toBe("number");
243
+ } finally {
244
+ globalThis.fetch = originalFetch;
245
+ }
246
+ });
247
+ });
@@ -60,7 +60,7 @@ function makeConfig(overrides: Partial<AssistantConfig> = {}): AssistantConfig {
60
60
  return {
61
61
  skills: {
62
62
  entries: {},
63
- load: { extraDirs: [], watch: true, watchDebounceMs: 250 },
63
+ load: { extraDirs: [] },
64
64
  install: { nodeManager: "npm" },
65
65
  allowBundled: null,
66
66
  remoteProviders: {
@@ -28,7 +28,7 @@ function makeConfig(overrides: Partial<AssistantConfig> = {}): AssistantConfig {
28
28
  return {
29
29
  skills: {
30
30
  entries: {},
31
- load: { extraDirs: [], watch: true, watchDebounceMs: 250 },
31
+ load: { extraDirs: [] },
32
32
  install: { nodeManager: "npm" },
33
33
  allowBundled: null,
34
34
  remoteProviders: {
@@ -233,7 +233,7 @@ describe("resolveSkillStates with feature flags", () => {
233
233
  const config = makeConfig({
234
234
  skills: {
235
235
  entries: { [DECLARED_SKILL_ID]: { enabled: true } },
236
- load: { extraDirs: [], watch: true, watchDebounceMs: 250 },
236
+ load: { extraDirs: [] },
237
237
  install: { nodeManager: "npm" },
238
238
  allowBundled: null,
239
239
  remoteProviders: {
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Tests for app source fingerprinting: walk exclusions, comparison, inspect
3
+ * statuses, and the compile-time sidecar format.
4
+ */
5
+
6
+ import {
7
+ mkdirSync,
8
+ mkdtempSync,
9
+ readFileSync,
10
+ rmSync,
11
+ symlinkSync,
12
+ writeFileSync,
13
+ } from "node:fs";
14
+ import { tmpdir } from "node:os";
15
+ import { join } from "node:path";
16
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
17
+
18
+ import {
19
+ compareAppSourceFingerprint,
20
+ computeAppSourceFingerprint,
21
+ inspectAppSource,
22
+ parseAppSourceFingerprint,
23
+ SOURCE_FINGERPRINT_FILENAME,
24
+ writeAppSourceFingerprint,
25
+ } from "../apps/source-fingerprint.js";
26
+
27
+ let root: string;
28
+
29
+ beforeEach(() => {
30
+ root = mkdtempSync(join(tmpdir(), "app-source-fingerprint-"));
31
+ });
32
+
33
+ afterEach(() => {
34
+ rmSync(root, { recursive: true, force: true });
35
+ });
36
+
37
+ function write(rel: string, contents: string): void {
38
+ const abs = join(root, rel);
39
+ mkdirSync(join(abs, ".."), { recursive: true });
40
+ writeFileSync(abs, contents);
41
+ }
42
+
43
+ describe("computeAppSourceFingerprint", () => {
44
+ test("digests source files with POSIX-relative keys", () => {
45
+ write("src/main.tsx", "export const x = 1;");
46
+ write("src/index.html", "<html></html>");
47
+
48
+ const fp = computeAppSourceFingerprint(root);
49
+
50
+ expect(Object.keys(fp.files).sort()).toEqual([
51
+ "src/index.html",
52
+ "src/main.tsx",
53
+ ]);
54
+ expect(fp.algorithm).toBe("sha256");
55
+ expect(fp.files["src/main.tsx"]).toMatch(/^[0-9a-f]{64}$/);
56
+ });
57
+
58
+ test("skips dist, records, node_modules, and .git", () => {
59
+ write("src/main.tsx", "src");
60
+ write("dist/main.js", "compiled");
61
+ write("records/rec-1.json", "{}");
62
+ write("node_modules/pkg/index.js", "dep");
63
+ write("src/node_modules/nested/index.js", "nested");
64
+ write(".git/HEAD", "ref");
65
+ write("src/.git/config", "nope");
66
+
67
+ const fp = computeAppSourceFingerprint(root);
68
+
69
+ expect(Object.keys(fp.files)).toEqual(["src/main.tsx"]);
70
+ });
71
+
72
+ test("skips symlinks", () => {
73
+ write("src/real.tsx", "hello");
74
+ mkdirSync(join(root, "src"), { recursive: true });
75
+ symlinkSync(join(root, "src/real.tsx"), join(root, "src/link.tsx"));
76
+
77
+ const fp = computeAppSourceFingerprint(root);
78
+
79
+ expect(Object.keys(fp.files)).toEqual(["src/real.tsx"]);
80
+ });
81
+ });
82
+
83
+ describe("compareAppSourceFingerprint", () => {
84
+ test("reports modified, added, and removed paths", () => {
85
+ write("src/main.tsx", "one");
86
+ write("src/keep.tsx", "keep");
87
+ const baseline = computeAppSourceFingerprint(root);
88
+
89
+ write("src/main.tsx", "two");
90
+ write("src/new.tsx", "new");
91
+ rmSync(join(root, "src/keep.tsx"));
92
+
93
+ const diff = compareAppSourceFingerprint(root, baseline);
94
+ expect(diff.clean).toBe(false);
95
+ expect(diff.modified).toEqual(["src/main.tsx"]);
96
+ expect(diff.added).toEqual(["src/new.tsx"]);
97
+ expect(diff.removed).toEqual(["src/keep.tsx"]);
98
+ });
99
+
100
+ test("is clean when source matches the baseline", () => {
101
+ write("src/main.tsx", "same");
102
+ const baseline = computeAppSourceFingerprint(root);
103
+ const diff = compareAppSourceFingerprint(root, baseline);
104
+ expect(diff.clean).toBe(true);
105
+ expect(diff.modified).toEqual([]);
106
+ expect(diff.added).toEqual([]);
107
+ expect(diff.removed).toEqual([]);
108
+ });
109
+ });
110
+
111
+ describe("inspectAppSource", () => {
112
+ test("reports never_compiled when dist index is missing even if a fingerprint exists", () => {
113
+ write("src/main.tsx", "src");
114
+ mkdirSync(join(root, "dist"), { recursive: true });
115
+ writeAppSourceFingerprint(root, join(root, "dist"));
116
+
117
+ const result = inspectAppSource(root);
118
+ expect(result.status).toBe("never_compiled");
119
+ });
120
+
121
+ test("reports unknown_baseline when dist exists without a fingerprint", () => {
122
+ write("src/main.tsx", "src");
123
+ write("dist/index.html", "<html></html>");
124
+ const result = inspectAppSource(root);
125
+ expect(result.status).toBe("unknown_baseline");
126
+ });
127
+
128
+ test("reports clean after writing a fingerprint that matches source", () => {
129
+ write("src/main.tsx", "src");
130
+ mkdirSync(join(root, "dist"), { recursive: true });
131
+ writeFileSync(join(root, "dist", "index.html"), "<html></html>");
132
+ writeAppSourceFingerprint(root, join(root, "dist"));
133
+
134
+ const result = inspectAppSource(root);
135
+ expect(result.status).toBe("clean");
136
+ expect(result.compiledAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
137
+ });
138
+
139
+ test("reports stale after a source edit", () => {
140
+ write("src/main.tsx", "before");
141
+ mkdirSync(join(root, "dist"), { recursive: true });
142
+ writeFileSync(join(root, "dist", "index.html"), "<html></html>");
143
+ writeAppSourceFingerprint(root, join(root, "dist"));
144
+ write("src/main.tsx", "after");
145
+
146
+ const result = inspectAppSource(root);
147
+ expect(result.status).toBe("stale");
148
+ expect(result.modified).toEqual(["src/main.tsx"]);
149
+ });
150
+ });
151
+
152
+ describe("parseAppSourceFingerprint", () => {
153
+ test("accepts a compile sidecar with compiledAt", () => {
154
+ const parsed = parseAppSourceFingerprint({
155
+ algorithm: "sha256",
156
+ compiledAt: "2026-09-09T00:00:00.000Z",
157
+ files: { "src/main.tsx": "abc" },
158
+ });
159
+ expect(parsed).toEqual({
160
+ algorithm: "sha256",
161
+ compiledAt: "2026-09-09T00:00:00.000Z",
162
+ files: { "src/main.tsx": "abc" },
163
+ });
164
+ });
165
+
166
+ test("returns null for an invalid sidecar", () => {
167
+ expect(parseAppSourceFingerprint({ algorithm: "md5", files: {} })).toBe(
168
+ null,
169
+ );
170
+ expect(parseAppSourceFingerprint(null)).toBeNull();
171
+ });
172
+
173
+ test("writeAppSourceFingerprint records a sidecar the parser accepts", () => {
174
+ write("src/main.tsx", "src");
175
+ mkdirSync(join(root, "dist"), { recursive: true });
176
+ writeAppSourceFingerprint(root, join(root, "dist"));
177
+ const raw = JSON.parse(
178
+ readFileSync(join(root, "dist", SOURCE_FINGERPRINT_FILENAME), "utf-8"),
179
+ );
180
+ expect(parseAppSourceFingerprint(raw)?.files["src/main.tsx"]).toMatch(
181
+ /^[0-9a-f]{64}$/,
182
+ );
183
+ });
184
+ });
@@ -249,14 +249,23 @@ describe("createResolveToolsCallback — toolContextPin", () => {
249
249
  });
250
250
  }
251
251
 
252
- test("control: without a pin, a clientless fork drops every client-gated tool from the wire", () => {
252
+ test("control: without a pin, a clientless fork drops client-gated tools but keeps host and UI tools", () => {
253
253
  projectedSkillToolNames = [];
254
254
  const resolve = createResolveToolsCallback(
255
255
  CLIENT_GATED_DEFS,
256
256
  clientlessExecutionCtx(),
257
257
  )!;
258
258
 
259
- expect(resolve(EMPTY_HISTORY).map((t) => t.name)).toEqual(["remember"]);
259
+ // Host tool definitions stay on the wire for background turns, and
260
+ // ui_show stays because background UI surfaces persist and return. The
261
+ // executor rejects host tools on explicitly non-interactive turns before
262
+ // dispatch; ask_question and request_system_permission remain
263
+ // client-gated at resolution.
264
+ expect(resolve(EMPTY_HISTORY).map((t) => t.name)).toEqual([
265
+ "remember",
266
+ "host_bash",
267
+ "ui_show",
268
+ ]);
260
269
  });
261
270
 
262
271
  test("a desktop-source pin restores the host/UI/client tool defs on the wire", () => {
@@ -291,7 +300,14 @@ describe("createResolveToolsCallback — toolContextPin", () => {
291
300
  }),
292
301
  )!;
293
302
 
294
- expect(resolve(EMPTY_HISTORY).map((t) => t.name)).toEqual(["remember"]);
303
+ // Host tool definitions stay on the wire for background turns, and
304
+ // ui_show survives the clientless pin: it persists and returns. The
305
+ // executor rejects host tools on explicitly non-interactive turns.
306
+ expect(resolve(EMPTY_HISTORY).map((t) => t.name)).toEqual([
307
+ "remember",
308
+ "host_bash",
309
+ "ui_show",
310
+ ]);
295
311
  });
296
312
 
297
313
  test("invariant: a pinned-in tool is on the wire but can never execute", async () => {
@@ -3447,8 +3447,9 @@ describe("Subagent advisor-role consult", () => {
3447
3447
  });
3448
3448
 
3449
3449
  test("advisor consults still run when the parent conversation is gone", async () => {
3450
- // The parent is looked up only for its warm skill catalog, so an evicted
3451
- // conversation costs a section of the context pack, not the consult.
3450
+ // Advisor consults do not look up the parent conversation for a skill
3451
+ // catalog snapshot. An evicted parent still gets a consult; the context
3452
+ // pack loads the catalog from disk independently.
3452
3453
  mockFindConversation = () => undefined;
3453
3454
  const { captured, restore } = stubSpawn();
3454
3455
  try {
@@ -455,10 +455,11 @@ describe("resolveCallTtsProvider with requiresPcmAudio", () => {
455
455
  expect(result.provider?.id).toBe("elevenlabs");
456
456
  });
457
457
 
458
- test("without requiresPcmAudio the configured provider is not swapped", async () => {
458
+ test("without requiresPcmAudio the configured provider is not swapped for playability", async () => {
459
459
  testConfig.services.tts.provider = "deepgram";
460
- // No credentials at all — the CR transport path does not consult the
461
- // media-stream playability capability.
460
+ storedKeys["deepgram"] = "dg-key";
461
+ // The CR transport path does not consult the media-stream playability
462
+ // capability.
462
463
  registerStubProvider("deepgram", async () => {
463
464
  return { audio: Buffer.from("x"), contentType: "audio/mpeg" };
464
465
  });
@@ -468,6 +469,37 @@ describe("resolveCallTtsProvider with requiresPcmAudio", () => {
468
469
  expect(result.useSynthesizedPath).toBe(true);
469
470
  expect(result.audioFormat).toBe("mp3");
470
471
  });
472
+
473
+ test("managed speech stands in for a configured provider whose credential is missing", async () => {
474
+ testConfig.services.tts.provider = "deepgram";
475
+ // No deepgram key, managed speech available: the phone speaks through
476
+ // managed speech, the same substitution live voice makes.
477
+ registerStubProvider("deepgram", async () => {
478
+ throw new Error("should not be used");
479
+ });
480
+ registerStubProvider("vellum", async () => {
481
+ return { audio: Buffer.from("x"), contentType: "audio/mpeg" };
482
+ });
483
+
484
+ const result = await resolveCallTtsProvider();
485
+ expect(result.provider?.id).toBe("vellum");
486
+ expect(result.useSynthesizedPath).toBe(true);
487
+ });
488
+
489
+ test("without managed speech a credential-less configured provider is kept", async () => {
490
+ mockManagedSpeechAvailable = false;
491
+ try {
492
+ testConfig.services.tts.provider = "deepgram";
493
+ registerStubProvider("deepgram", async () => {
494
+ return { audio: Buffer.from("x"), contentType: "audio/mpeg" };
495
+ });
496
+
497
+ const result = await resolveCallTtsProvider();
498
+ expect(result.provider?.id).toBe("deepgram");
499
+ } finally {
500
+ mockManagedSpeechAvailable = true;
501
+ }
502
+ });
471
503
  });
472
504
 
473
505
  // ---------------------------------------------------------------------------
@@ -584,6 +616,7 @@ describe("speakSystemPrompt mid-stream failure after audio started", () => {
584
616
  test("non-PCM transport: skips the native text fallback — no full-prompt re-speak", async () => {
585
617
  testConfig.services.tts.provider = "fish-audio";
586
618
  testConfig.services.tts.providers["fish-audio"].referenceId = "ref-123";
619
+ storedKeys["fish-audio"] = "fa-key";
587
620
 
588
621
  const fishStream = jest.fn(failAfterFirstChunk());
589
622
  registerStreamingStubProvider("fish-audio", fishStream);
@@ -657,6 +690,7 @@ describe("speakSystemPrompt aborted synthesis", () => {
657
690
  test("non-PCM transport with native-fallback provider: abort skips the text fallback and end-of-turn", async () => {
658
691
  testConfig.services.tts.provider = "fish-audio";
659
692
  testConfig.services.tts.providers["fish-audio"].referenceId = "ref-123";
693
+ storedKeys["fish-audio"] = "fa-key";
660
694
 
661
695
  const controller = new AbortController();
662
696
  controller.abort();
@@ -742,6 +776,7 @@ describe("speakSystemPrompt aborted synthesis", () => {
742
776
  test("provider AbortError without any signal is a failure — non-PCM native fallback still runs", async () => {
743
777
  testConfig.services.tts.provider = "fish-audio";
744
778
  testConfig.services.tts.providers["fish-audio"].referenceId = "ref-123";
779
+ storedKeys["fish-audio"] = "fa-key";
745
780
 
746
781
  const fishSynthesize = jest.fn(async () => {
747
782
  throw new DOMException("socket torn down", "AbortError");
@@ -136,6 +136,10 @@ describe("vellum-self-knowledge skill", () => {
136
136
  const result = await executeSkillLoad({ skill: "vellum-self-knowledge" });
137
137
  expect(result.content).toContain("## Critical Rule");
138
138
  expect(result.content).toContain("Never answer from memory");
139
+ expect(result.content).toContain("distinct, legacy Vellum product");
140
+ expect(result.content).toContain(
141
+ "Never use it to answer questions about this Vellum",
142
+ );
139
143
  });
140
144
 
141
145
  // ── Resolution order ─────────────────────────────────────────────────
@@ -1,13 +1,10 @@
1
1
  /**
2
2
  * The gate that decides whether a watch retrospective can report at all.
3
3
  *
4
- * A retrospective runs as a `clientless` wake, which pins the turn
5
- * non-interactive. `conversation-tool-setup` gates the whole `ui_surface`
6
- * family on a client being present, so in that turn `ui_show` is not denied,
7
- * it is absent: a retrospective told to call it can only tell the user it
8
- * cannot. Nothing about the card's schema, its renderer, or the prompt's
9
- * wording reveals that, which is why it is asserted here against the real
10
- * registry rather than assumed anywhere else.
4
+ * A retrospective runs as a `clientless` wake, but its report continues to
5
+ * use the post-turn renderer so the report lands after its tool call has been
6
+ * persisted. The core UI tools remain available on that turn for other
7
+ * background work, so this file pins both contracts against the real registry.
11
8
  */
12
9
 
13
10
  import { afterAll, describe, expect, test } from "bun:test";
@@ -54,20 +51,14 @@ describe("watch retrospective tool availability", () => {
54
51
  ).toBe(true);
55
52
  });
56
53
 
57
- test("ui_show is absent from a clientless turn", async () => {
54
+ test("core UI tools stay available on a clientless turn", async () => {
58
55
  await initializeTools();
59
56
  const ctx = clientlessContext();
60
57
 
61
- // The reason the report does not go through `ui_show`. If this ever flips
62
- // to true, the extra tool above can be reconsidered; while it is false,
63
- // routing the retrospective's card through `ui_show` produces a turn that
64
- // cannot report at all.
65
- expect(isToolActiveForContext("ui_show", ctx)).toBe(false);
66
- expect(isToolActiveForContext("ui_update", ctx)).toBe(false);
67
- expect(isToolActiveForContext("ui_dismiss", ctx)).toBe(false);
58
+ for (const name of ["ui_show", "ui_update", "ui_dismiss"]) {
59
+ expect(isToolActiveForContext(name, ctx)).toBe(true);
60
+ }
68
61
 
69
- // And the gate really is about the client, not about the tools being
70
- // unregistered: with one attached, the same names are active.
71
62
  const withClient = clientfulContext();
72
63
  expect(isToolActiveForContext("ui_show", withClient)).toBe(true);
73
64
  });