@vellumai/assistant 0.12.1 → 0.12.2-staging.2

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 (577) hide show
  1. package/AGENTS.md +4 -0
  2. package/ARCHITECTURE.md +20 -21
  3. package/docs/architecture/turn-actor.md +15 -8
  4. package/docs/guardian-request-flow.md +16 -0
  5. package/node_modules/@vellumai/slack-text/src/index.ts +13 -8
  6. package/node_modules/@vellumai/slack-text/src/label-resolution-entities.test.ts +95 -0
  7. package/openapi.yaml +360 -51
  8. package/package.json +3 -1
  9. package/scripts/smoke-desktop-window-theme.ts +186 -0
  10. package/src/__tests__/always-loaded-tools-guard.test.ts +18 -10
  11. package/src/__tests__/anthropic-provider.test.ts +0 -63
  12. package/src/__tests__/app-compiler.test.ts +6 -0
  13. package/src/__tests__/app-control-tool-schemas.test.ts +5 -2
  14. package/src/__tests__/app-dir-path-guard.test.ts +1 -3
  15. package/src/__tests__/assistant-event-hub-machine-name.test.ts +6 -7
  16. package/src/__tests__/assistant-event-hub.test.ts +44 -0
  17. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
  18. package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +5 -2
  19. package/src/__tests__/computer-use-tools.test.ts +19 -7
  20. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  21. package/src/__tests__/config-schema.test.ts +28 -29
  22. package/src/__tests__/config-watcher.test.ts +23 -8
  23. package/src/__tests__/connection-model-compat.test.ts +2 -2
  24. package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
  25. package/src/__tests__/conversation-pairing.test.ts +26 -0
  26. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -10
  27. package/src/__tests__/conversation-queue.test.ts +48 -207
  28. package/src/__tests__/conversation-retry-discard.test.ts +54 -1
  29. package/src/__tests__/conversation-runtime-assembly.test.ts +28 -0
  30. package/src/__tests__/conversation-skill-tools.test.ts +27 -0
  31. package/src/__tests__/conversation-surfaces-app-control.test.ts +8 -3
  32. package/src/__tests__/conversation-surfaces-point-at-budget.test.ts +1 -0
  33. package/src/__tests__/conversation-surfaces-point-at-capability.test.ts +1 -0
  34. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  35. package/src/__tests__/conversation-title-service.test.ts +14 -10
  36. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +3 -5
  37. package/src/__tests__/conversation-tool-setup-attribution.test.ts +51 -0
  38. package/src/__tests__/credential-record-retire.test.ts +171 -1
  39. package/src/__tests__/credential-routes.test.ts +250 -3
  40. package/src/__tests__/credential-security-invariants.test.ts +5 -3
  41. package/src/__tests__/cu-unified-flow.test.ts +14 -4
  42. package/src/__tests__/document-sync-tags.test.ts +25 -0
  43. package/src/__tests__/drain-kick-guard.test.ts +0 -2
  44. package/src/__tests__/drain-requeue-on-contention.test.ts +0 -6
  45. package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
  46. package/src/__tests__/git-porcelain.test.ts +47 -0
  47. package/src/__tests__/heartbeat-service.test.ts +56 -40
  48. package/src/__tests__/helpers/set-workspace-mcp.ts +11 -0
  49. package/src/__tests__/host-bash-proxy.test.ts +26 -45
  50. package/src/__tests__/host-browser-proxy.test.ts +6 -17
  51. package/src/__tests__/host-cu-proxy.test.ts +596 -0
  52. package/src/__tests__/host-file-proxy-targeted.test.ts +23 -0
  53. package/src/__tests__/host-file-proxy.test.ts +11 -23
  54. package/src/__tests__/host-transfer-proxy-targeted.test.ts +30 -9
  55. package/src/__tests__/list-messages-camera-frame.test.ts +121 -0
  56. package/src/__tests__/llm-catalog-parity.test.ts +25 -0
  57. package/src/__tests__/mcp-auth-routes.test.ts +6 -9
  58. package/src/__tests__/mcp-config-secret-boundary.test.ts +78 -136
  59. package/src/__tests__/mcp-health-check.test.ts +8 -11
  60. package/src/__tests__/mcp-list-plugin-servers.test.ts +8 -10
  61. package/src/__tests__/message-deleted-hook-dispatch.test.ts +119 -0
  62. package/src/__tests__/messages-after-tiebreaker.test.ts +91 -0
  63. package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
  64. package/src/__tests__/messaging-send-tool.test.ts +355 -548
  65. package/src/__tests__/model-intents.test.ts +3 -0
  66. package/src/__tests__/notification-vellum-adapter.test.ts +85 -2
  67. package/src/__tests__/oauth-commands-routes.test.ts +168 -0
  68. package/src/__tests__/oauth-provider-profiles.test.ts +18 -0
  69. package/src/__tests__/oauth-provider-serializer.test.ts +22 -0
  70. package/src/__tests__/oauth-providers-routes.test.ts +1 -0
  71. package/src/__tests__/openai-provider.test.ts +1 -0
  72. package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
  73. package/src/__tests__/platform-callback-registration.test.ts +8 -16
  74. package/src/__tests__/platform-client-verify-credential.test.ts +2 -0
  75. package/src/__tests__/plugin-import-boundary-guard.test.ts +14 -0
  76. package/src/__tests__/qdrant-manager.test.ts +132 -0
  77. package/src/__tests__/resolve-app.test.ts +135 -0
  78. package/src/__tests__/scaffold-managed-skill-tool.test.ts +229 -6
  79. package/src/__tests__/script-proxy-certs.test.ts +35 -0
  80. package/src/__tests__/secret-routes-acp-guard.test.ts +59 -1
  81. package/src/__tests__/service-account-token.test.ts +247 -0
  82. package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
  83. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  84. package/src/__tests__/skills.test.ts +9 -4
  85. package/src/__tests__/source-fingerprint.test.ts +184 -0
  86. package/src/__tests__/subagent-tool-gate-mode.test.ts +70 -3
  87. package/src/__tests__/subagent-tools.test.ts +3 -2
  88. package/src/__tests__/telephony-tts-guard.test.ts +38 -3
  89. package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
  90. package/src/__tests__/ui-channel-variants.test.ts +1 -56
  91. package/src/__tests__/unicode.test.ts +13 -0
  92. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +4 -0
  93. package/src/__tests__/watch-retro-tool-availability.test.ts +8 -17
  94. package/src/__tests__/workspace-git-service.test.ts +26 -0
  95. package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +336 -0
  96. package/src/acp/__tests__/acp-claude-oauth.test.ts +257 -6
  97. package/src/acp/__tests__/acp-credentials.test.ts +13 -0
  98. package/src/acp/__tests__/agent-process.test.ts +258 -4
  99. package/src/acp/__tests__/auth-required.test.ts +96 -0
  100. package/src/acp/__tests__/claude-token-refresh.test.ts +257 -0
  101. package/src/acp/__tests__/client-handler.test.ts +90 -2
  102. package/src/acp/__tests__/helpers/acp-model-option.ts +51 -0
  103. package/src/acp/__tests__/prepare-agent-env.test.ts +60 -1
  104. package/src/acp/__tests__/session-manager-persistence.test.ts +5 -1
  105. package/src/acp/__tests__/session-manager-resume.test.ts +270 -6
  106. package/src/acp/__tests__/session-manager-usage.test.ts +4 -2
  107. package/src/acp/__tests__/session-manager.test.ts +895 -4
  108. package/src/acp/acp-claude-oauth.ts +328 -14
  109. package/src/acp/acp-credentials.ts +19 -0
  110. package/src/acp/agent-process.test.ts +4 -1
  111. package/src/acp/agent-process.ts +96 -10
  112. package/src/acp/auth-required.ts +50 -1
  113. package/src/acp/auto-install.test.ts +9 -7
  114. package/src/acp/claude-token-refresh.ts +150 -0
  115. package/src/acp/client-handler.ts +16 -3
  116. package/src/acp/model-config.test.ts +166 -0
  117. package/src/acp/model-config.ts +138 -0
  118. package/src/acp/prepare-agent-env.ts +21 -6
  119. package/src/acp/resolve-agent.test.ts +220 -6
  120. package/src/acp/resolve-agent.ts +88 -23
  121. package/src/acp/session-manager.test.ts +196 -3
  122. package/src/acp/session-manager.ts +441 -18
  123. package/src/acp/types.ts +50 -5
  124. package/src/agent/loop.ts +1 -0
  125. package/src/api/events/acp-session-model-update.test.ts +88 -0
  126. package/src/api/events/acp-session-model-update.ts +46 -0
  127. package/src/api/events/app-files-changed.ts +4 -3
  128. package/src/api/events/notification-intent.ts +2 -0
  129. package/src/api/events/user-message-echo.ts +7 -1
  130. package/src/api/index.ts +6 -0
  131. package/src/api/responses/conversation-message.ts +5 -0
  132. package/src/apps/app-store.ts +1 -1
  133. package/src/apps/resolve-app.ts +81 -0
  134. package/src/apps/source-fingerprint.ts +266 -0
  135. package/src/avatar/identity-avatar.ts +7 -2
  136. package/src/bundler/app-compiler.ts +15 -0
  137. package/src/calls/__tests__/barge-in-guard.test.ts +81 -0
  138. package/src/{__tests__ → calls/__tests__}/call-controller.test.ts +503 -82
  139. package/src/{__tests__ → calls/__tests__}/media-stream-audio-transcode.test.ts +1 -1
  140. package/src/{__tests__ → calls/__tests__}/media-stream-output.test.ts +37 -8
  141. package/src/{__tests__ → calls/__tests__}/media-stream-parser.test.ts +1 -1
  142. package/src/{__tests__ → calls/__tests__}/media-stream-server-integration.test.ts +150 -57
  143. package/src/{__tests__ → calls/__tests__}/media-stream-stt-session.test.ts +5 -5
  144. package/src/{__tests__ → calls/__tests__}/media-turn-detector.test.ts +1 -1
  145. package/src/{live-voice → calls}/__tests__/progress-narration.test.ts +4 -4
  146. package/src/{live-voice → calls}/__tests__/progress-phrases.test.ts +1 -1
  147. package/src/calls/__tests__/voice-control-protocol.test.ts +120 -25
  148. package/src/calls/__tests__/voice-leg-coordinator.test.ts +229 -0
  149. package/src/calls/__tests__/voice-progress-cadence.test.ts +274 -0
  150. package/src/{__tests__/voice-session-bridge.test.ts → calls/__tests__/voice-session-bridge-turn-flow.test.ts} +20 -12
  151. package/src/calls/__tests__/voice-session-bridge.test.ts +56 -1
  152. package/src/calls/__tests__/voice-triage-escalate.test.ts +105 -0
  153. package/src/calls/barge-in-guard.ts +118 -0
  154. package/src/calls/call-constants.ts +24 -4
  155. package/src/calls/call-controller.ts +499 -199
  156. package/src/calls/call-transport.ts +16 -0
  157. package/src/calls/media-stream-output.ts +32 -0
  158. package/src/calls/media-stream-server.ts +82 -30
  159. package/src/calls/media-stream-stt-session.ts +15 -0
  160. package/src/{live-voice → calls}/progress-narration.ts +2 -2
  161. package/src/calls/resolve-call-tts-provider.ts +33 -2
  162. package/src/calls/tts-call-strategy.ts +9 -5
  163. package/src/calls/types.ts +2 -0
  164. package/src/calls/voice-control-protocol.ts +155 -21
  165. package/src/calls/voice-leg-coordinator.ts +189 -0
  166. package/src/calls/voice-progress-cadence.ts +423 -0
  167. package/src/calls/voice-session-bridge.ts +92 -72
  168. package/src/calls/voice-triage-escalate.ts +178 -59
  169. package/src/cli/commands/__tests__/apps.test.ts +245 -0
  170. package/src/cli/commands/__tests__/bash.test.ts +53 -0
  171. package/src/cli/commands/__tests__/roadmap.help.test.ts +19 -0
  172. package/src/cli/commands/apps.help.ts +67 -13
  173. package/src/cli/commands/apps.ts +183 -0
  174. package/src/cli/commands/bash.help.ts +8 -13
  175. package/src/cli/commands/bash.ts +7 -25
  176. package/src/cli/commands/channels/__tests__/send.test.ts +218 -0
  177. package/src/cli/commands/channels/index.help.ts +70 -2
  178. package/src/cli/commands/channels/index.ts +16 -3
  179. package/src/cli/commands/channels/send.ts +128 -0
  180. package/src/cli/commands/credentials.help.ts +6 -2
  181. package/src/cli/commands/credentials.ts +2 -2
  182. package/src/cli/commands/mcp.help.ts +8 -8
  183. package/src/cli/commands/mcp.ts +1 -1
  184. package/src/cli/commands/oauth/status.ts +5 -0
  185. package/src/cli/commands/roadmap.help.ts +5 -1
  186. package/src/cli/commands/roadmap.ts +1 -1
  187. package/src/cli/lib/bundled-marketplace.json +103 -90
  188. package/src/config/__tests__/default-profile-catalog.test.ts +22 -0
  189. package/src/config/__tests__/memory-retrospective-schema.test.ts +15 -1
  190. package/src/config/__tests__/platform-identity.test.ts +255 -0
  191. package/src/config/__tests__/platform-rehydration.test.ts +99 -9
  192. package/src/config/acp-agent-write.ts +43 -0
  193. package/src/config/acp-defaults.test.ts +13 -3
  194. package/src/config/acp-defaults.ts +23 -6
  195. package/src/config/acp-schema.test.ts +81 -0
  196. package/src/config/acp-schema.ts +30 -2
  197. package/src/config/bundled-skills/acp/SKILL.md +22 -14
  198. package/src/config/bundled-skills/acp/TOOLS.json +5 -1
  199. package/src/config/bundled-skills/app-builder/SKILL.md +12 -9
  200. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
  201. package/src/config/bundled-skills/computer-use/SKILL.md +29 -2
  202. package/src/config/bundled-skills/computer-use/TOOLS.json +99 -0
  203. package/src/config/bundled-skills/computer-use/tools/computer-use-sequence.ts +12 -0
  204. package/src/config/bundled-skills/media-processing/services/preprocess.ts +2 -1
  205. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +2 -1
  206. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +2 -1
  207. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  208. package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
  209. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +67 -139
  210. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
  211. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -1
  212. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -1
  213. package/src/config/bundled-skills/schedule/SKILL.md +3 -4
  214. package/src/config/bundled-skills/screen-annotation/SKILL.md +8 -1
  215. package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +4 -1
  216. package/src/config/bundled-skills/skill-management/TOOLS.json +6 -2
  217. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +5 -2
  218. package/src/config/default-profile-catalog.ts +1 -1
  219. package/src/config/env.ts +2 -2
  220. package/src/config/feature-flag-registry.json +33 -13
  221. package/src/config/loader.ts +2 -0
  222. package/src/config/memory-v3-gate.test.ts +31 -0
  223. package/src/config/memory-v3-gate.ts +20 -16
  224. package/src/config/platform-identity.ts +201 -0
  225. package/src/config/platform-rehydration.ts +11 -27
  226. package/src/config/schema.ts +2 -2
  227. package/src/config/schemas/__tests__/live-voice.test.ts +8 -158
  228. package/src/config/schemas/__tests__/voice.test.ts +148 -0
  229. package/src/config/schemas/calls.ts +1 -28
  230. package/src/config/schemas/live-voice.ts +1 -173
  231. package/src/config/schemas/mcp.ts +1 -1
  232. package/src/config/schemas/memory-retrospective.ts +10 -0
  233. package/src/config/schemas/services.ts +18 -0
  234. package/src/config/schemas/skills.ts +0 -14
  235. package/src/config/schemas/voice.ts +150 -0
  236. package/src/credential-execution/process-manager.test.ts +18 -19
  237. package/src/credential-health/credential-health-service.ts +11 -17
  238. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -121
  239. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +4 -1
  240. package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +133 -0
  241. package/src/daemon/app-change-notify.ts +45 -0
  242. package/src/daemon/config-watcher.ts +13 -194
  243. package/src/daemon/conversation-error.ts +3 -2
  244. package/src/daemon/conversation-media-retry.ts +2 -1
  245. package/src/daemon/conversation-process.ts +35 -64
  246. package/src/daemon/conversation-runtime-assembly.ts +20 -7
  247. package/src/daemon/conversation-skill-tools.ts +13 -31
  248. package/src/daemon/conversation-store.ts +1 -1
  249. package/src/daemon/conversation-surfaces.ts +21 -10
  250. package/src/daemon/conversation-tool-setup.ts +45 -59
  251. package/src/daemon/conversation.ts +0 -3
  252. package/src/daemon/dictation-profile-store.ts +2 -1
  253. package/src/daemon/handlers/shared.ts +2 -1
  254. package/src/daemon/handlers/skills.ts +6 -3
  255. package/src/daemon/host-bash-proxy.ts +8 -12
  256. package/src/daemon/host-browser-proxy.ts +15 -1
  257. package/src/daemon/host-cu-proxy.ts +224 -19
  258. package/src/daemon/host-file-proxy.ts +8 -10
  259. package/src/daemon/host-proxy-preactivation.ts +2 -1
  260. package/src/daemon/host-transfer-proxy.ts +14 -18
  261. package/src/daemon/lifecycle.ts +0 -5
  262. package/src/daemon/mcp-reload-service.ts +5 -5
  263. package/src/daemon/message-types/sync.ts +1 -0
  264. package/src/daemon/orphan-reaper.ts +1 -1
  265. package/src/daemon/providers-setup.ts +1 -1
  266. package/src/daemon/reaction-history-render.ts +2 -1
  267. package/src/daemon/shutdown-handlers.ts +1 -9
  268. package/src/daemon/skill-memory-refresh.ts +64 -0
  269. package/src/daemon/tool-side-effects.ts +7 -18
  270. package/src/desktop/__tests__/fake-desktop.ts +122 -0
  271. package/src/desktop/desktop-chrome-policy.test.ts +79 -0
  272. package/src/desktop/desktop-chrome-policy.ts +47 -0
  273. package/src/desktop/desktop-chrome-session.ts +37 -0
  274. package/src/desktop/desktop-dependencies.test.ts +102 -0
  275. package/src/desktop/desktop-dependencies.ts +308 -0
  276. package/src/desktop/desktop-feature.test.ts +18 -0
  277. package/src/desktop/desktop-feature.ts +19 -0
  278. package/src/desktop/desktop-panel-config.test.ts +135 -0
  279. package/src/desktop/desktop-panel-config.ts +149 -0
  280. package/src/desktop/desktop-session-manager.test.ts +730 -0
  281. package/src/desktop/desktop-session-manager.ts +850 -0
  282. package/src/desktop/desktop-stream-bridge.test.ts +395 -0
  283. package/src/desktop/desktop-stream-bridge.ts +214 -0
  284. package/src/desktop/desktop-wallpaper.test.ts +191 -0
  285. package/src/desktop/desktop-wallpaper.ts +113 -0
  286. package/src/desktop/desktop-window-drag.ts +177 -0
  287. package/src/desktop/desktop-window-manager-config.test.ts +230 -0
  288. package/src/desktop/desktop-window-manager-config.ts +238 -0
  289. package/src/desktop/desktop-window-theme.ts +170 -0
  290. package/src/heartbeat/heartbeat-service.ts +21 -3
  291. package/src/home/__tests__/feed-writer.test.ts +22 -0
  292. package/src/home/feed-writer.ts +11 -2
  293. package/src/hooks/types.ts +33 -0
  294. package/src/i18n/AGENTS.md +22 -0
  295. package/src/i18n/__tests__/i18n.test.ts +108 -0
  296. package/src/i18n/index.ts +16 -0
  297. package/src/i18n/locales.ts +93 -0
  298. package/src/i18n/messages.ts +61 -0
  299. package/src/i18n/title.ts +25 -0
  300. package/src/inbound/platform-callback-registration.ts +4 -5
  301. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +121 -0
  302. package/src/live-voice/__tests__/live-voice-events.test.ts +8 -7
  303. package/src/live-voice/__tests__/live-voice-metrics.test.ts +0 -3
  304. package/src/live-voice/__tests__/live-voice-progress.test.ts +94 -15
  305. package/src/live-voice/__tests__/live-voice-sight-frame-echo.test.ts +238 -0
  306. package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
  307. package/src/live-voice/__tests__/protocol.test.ts +39 -0
  308. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +13 -69
  309. package/src/live-voice/__tests__/session-controls.test.ts +79 -0
  310. package/src/live-voice/live-voice-photo.ts +25 -6
  311. package/src/live-voice/live-voice-session.ts +297 -699
  312. package/src/live-voice/protocol.ts +60 -2
  313. package/src/live-voice/session-controls.ts +111 -0
  314. package/src/mcp/__tests__/effective-config.test.ts +2 -2
  315. package/src/mcp/__tests__/workspace-mcp-config.test.ts +135 -0
  316. package/src/mcp/effective-config.ts +3 -3
  317. package/src/mcp/mcp-header-store.ts +29 -45
  318. package/src/mcp/spec-schema.ts +111 -0
  319. package/src/mcp/startup.ts +8 -4
  320. package/src/mcp/workspace-mcp-config.ts +188 -0
  321. package/src/messaging/provider.ts +10 -3
  322. package/src/messaging/providers/__tests__/proactive-address.test.ts +95 -0
  323. package/src/messaging/providers/callback-routing.ts +18 -0
  324. package/src/messaging/providers/channel-transport.ts +56 -0
  325. package/src/messaging/providers/discord/transport.ts +17 -0
  326. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +6 -17
  327. package/src/messaging/providers/slack/adapter.ts +7 -44
  328. package/src/messaging/providers/slack/transport.ts +16 -0
  329. package/src/messaging/providers/telegram-bot/adapter.ts +1 -48
  330. package/src/messaging/providers/telegram-bot/client.ts +0 -26
  331. package/src/messaging/providers/telegram-bot/transport.ts +20 -0
  332. package/src/messaging/providers/whatsapp/adapter.ts +1 -48
  333. package/src/messaging/providers/whatsapp/transport.ts +18 -0
  334. package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +8 -0
  335. package/src/notifications/__tests__/assistant-reply-producer.test.ts +55 -0
  336. package/src/notifications/__tests__/guardian-feed-projection.test.ts +158 -2
  337. package/src/notifications/__tests__/home-feed-side-effect.test.ts +153 -3
  338. package/src/notifications/__tests__/schedule-result-producer.test.ts +99 -3
  339. package/src/notifications/adapters/macos.ts +3 -0
  340. package/src/notifications/assistant-reply-producer.ts +8 -3
  341. package/src/notifications/conversation-pairing.ts +36 -16
  342. package/src/notifications/delivered-post-record.ts +11 -1
  343. package/src/notifications/guardian-feed-projection.ts +125 -5
  344. package/src/notifications/home-feed-side-effect.ts +55 -7
  345. package/src/notifications/schedule-result-producer.ts +66 -22
  346. package/src/oauth/AGENTS.md +2 -0
  347. package/src/oauth/__tests__/seed-providers-managed.test.ts +149 -0
  348. package/src/oauth/byo-connection.test.ts +105 -0
  349. package/src/oauth/byo-connection.ts +36 -6
  350. package/src/oauth/connection-resolver.test.ts +100 -0
  351. package/src/oauth/connection-resolver.ts +120 -3
  352. package/src/oauth/provider-serializer.ts +9 -0
  353. package/src/oauth/scope-utils.ts +24 -0
  354. package/src/oauth/seed-providers.ts +314 -27
  355. package/src/outbound-proxy/certs.ts +3 -5
  356. package/src/permissions/checker.test.ts +11 -6
  357. package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
  358. package/src/persistence/conversation-crud.ts +71 -63
  359. package/src/persistence/conversation-lineage.ts +12 -4
  360. package/src/persistence/conversation-queries.ts +2 -2
  361. package/src/persistence/conversation-title-placeholders.ts +23 -0
  362. package/src/persistence/conversation-title-service.ts +9 -26
  363. package/src/persistence/embeddings/qdrant-manager.ts +163 -18
  364. package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
  365. package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
  366. package/src/persistence/message-cursor.ts +78 -0
  367. package/src/persistence/message-reads.ts +30 -0
  368. package/src/persistence/migrations/157-invite-contact-id.ts +18 -6
  369. package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +149 -0
  370. package/src/persistence/schema/memory-core.ts +5 -0
  371. package/src/platform/client.test.ts +4 -2
  372. package/src/platform/client.ts +2 -10
  373. package/src/plugin-api/constants.ts +2 -0
  374. package/src/plugin-api/index.ts +1 -0
  375. package/src/plugin-api/types.ts +2 -0
  376. package/src/plugins/defaults/index.ts +2 -0
  377. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +46 -0
  378. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +118 -0
  379. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +83 -0
  380. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +9 -5
  381. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +20 -10
  382. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +5 -0
  383. package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +219 -0
  384. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +31 -0
  385. package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +63 -0
  386. package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +4 -0
  387. package/src/plugins/defaults/memory/context-search/agent-runner.ts +3 -2
  388. package/src/plugins/defaults/memory/context-search/format.ts +3 -2
  389. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +6 -3
  390. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +3 -2
  391. package/src/plugins/defaults/memory/graph/capability-seed.ts +6 -7
  392. package/src/plugins/defaults/memory/hooks/init.ts +10 -0
  393. package/src/plugins/defaults/memory/hooks/message-deleted.ts +21 -0
  394. package/src/plugins/defaults/memory/hooks/shutdown.ts +2 -0
  395. package/src/plugins/defaults/memory/host-utils.ts +10 -0
  396. package/src/plugins/defaults/memory/jobs-worker.ts +13 -0
  397. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +34 -60
  398. package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +126 -0
  399. package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +79 -0
  400. package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +28 -0
  401. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +3 -5
  402. package/src/plugins/defaults/memory/memory-retrospective-job.ts +12 -3
  403. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  404. package/src/plugins/defaults/memory/memory-retrospective-state.ts +312 -71
  405. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +2 -1
  406. package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +5 -3
  407. package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +9 -0
  408. package/src/plugins/defaults/memory/startup.ts +28 -2
  409. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +7 -9
  410. package/src/plugins/defaults/memory/substrate/page-index.ts +6 -1
  411. package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
  412. package/src/plugins/defaults/memory/v1/graph/consolidation.ts +4 -2
  413. package/src/plugins/defaults/memory/v1/graph/extraction.ts +4 -2
  414. package/src/plugins/defaults/memory/v1/graph/retriever.ts +4 -1
  415. package/src/plugins/defaults/memory/v2/reranker.ts +6 -1
  416. package/src/plugins/defaults/memory/v3/__tests__/card.test.ts +24 -0
  417. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +125 -0
  418. package/src/plugins/defaults/memory/v3/__tests__/sections.test.ts +26 -0
  419. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +30 -0
  420. package/src/plugins/defaults/memory/v3/card.ts +3 -2
  421. package/src/plugins/defaults/memory/v3/pool-select.ts +5 -3
  422. package/src/plugins/defaults/memory/v3/sections.ts +9 -3
  423. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +5 -2
  424. package/src/plugins/defaults/memory/worker.ts +6 -0
  425. package/src/plugins/mcp-servers.ts +15 -75
  426. package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
  427. package/src/providers/__tests__/retry-callsite.test.ts +41 -0
  428. package/src/providers/__tests__/retry-outbound-sanitize.test.ts +176 -0
  429. package/src/providers/anthropic/client.ts +0 -47
  430. package/src/providers/gemini/client.ts +9 -23
  431. package/src/providers/gemini-thought-signature.test.ts +63 -0
  432. package/src/providers/gemini-thought-signature.ts +44 -0
  433. package/src/providers/inference/__tests__/endpoint-probe.test.ts +42 -3
  434. package/src/providers/inference/auth.ts +2 -3
  435. package/src/providers/inference/connection-availability.ts +27 -8
  436. package/src/providers/inference/endpoint-probe.ts +10 -1
  437. package/src/providers/inference/resolve-auth.ts +22 -4
  438. package/src/providers/inference/service-account-token.ts +248 -0
  439. package/src/providers/model-catalog.ts +34 -0
  440. package/src/providers/model-intents.ts +10 -5
  441. package/src/providers/openai/__tests__/chat-completions-output-token-limit.test.ts +141 -0
  442. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +146 -18
  443. package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +613 -0
  444. package/src/providers/openai/__tests__/google-thought-signature.test.ts +235 -0
  445. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +83 -1
  446. package/src/providers/openai/chat-completions-provider.ts +212 -79
  447. package/src/providers/openai/codex-models.ts +0 -4
  448. package/src/providers/openai/google-thought-signature.ts +320 -0
  449. package/src/providers/opencode/client.test.ts +73 -4
  450. package/src/providers/opencode/client.ts +60 -3
  451. package/src/providers/openrouter/client.ts +20 -0
  452. package/src/providers/outbound-request-sanitize.ts +96 -0
  453. package/src/providers/retry.ts +30 -19
  454. package/src/providers/unparseable-tool-args.ts +5 -1
  455. package/src/runtime/AGENTS.md +3 -1
  456. package/src/runtime/__tests__/channel-send.test.ts +443 -0
  457. package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +89 -0
  458. package/src/runtime/__tests__/host-observe.test.ts +40 -1
  459. package/src/runtime/__tests__/live-voice-upgrade.test.ts +58 -0
  460. package/src/runtime/__tests__/runtime-ws-test-utils.ts +72 -0
  461. package/src/runtime/__tests__/watch-stream-upgrade.test.ts +59 -0
  462. package/src/runtime/agent-wake.ts +2 -1
  463. package/src/runtime/assistant-event-hub.ts +21 -3
  464. package/src/runtime/auth/same-actor.ts +38 -3
  465. package/src/runtime/channel-send.ts +316 -0
  466. package/src/runtime/host-observe.ts +11 -1
  467. package/src/runtime/http-server.ts +160 -151
  468. package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +16 -5
  469. package/src/runtime/routes/__tests__/acp-routes.test.ts +159 -4
  470. package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +275 -0
  471. package/src/runtime/routes/__tests__/channel-send-routes.test.ts +127 -0
  472. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +108 -1
  473. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +2 -2
  474. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +107 -0
  475. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +16 -12
  476. package/src/runtime/routes/acp-claude-auth-routes.ts +11 -3
  477. package/src/runtime/routes/acp-routes.test.ts +15 -0
  478. package/src/runtime/routes/acp-routes.ts +72 -9
  479. package/src/runtime/routes/app-management-routes.ts +90 -2
  480. package/src/runtime/routes/app-routes.ts +1 -1
  481. package/src/runtime/routes/channel-send-routes.ts +85 -0
  482. package/src/runtime/routes/conversation-list-routes.ts +14 -3
  483. package/src/runtime/routes/conversation-query-routes.ts +40 -0
  484. package/src/runtime/routes/conversation-routes.ts +16 -3
  485. package/src/runtime/routes/credential-routes.ts +20 -14
  486. package/src/runtime/routes/desktop-setup-routes.test.ts +74 -0
  487. package/src/runtime/routes/desktop-setup-routes.ts +35 -0
  488. package/src/runtime/routes/diagnostics-routes.ts +61 -4
  489. package/src/runtime/routes/events-routes.ts +4 -0
  490. package/src/runtime/routes/host-cu-routes.ts +6 -0
  491. package/src/runtime/routes/index.ts +4 -2
  492. package/src/runtime/routes/log-export-routes.ts +2 -0
  493. package/src/runtime/routes/mcp-auth-routes.ts +39 -47
  494. package/src/runtime/routes/migration-routes.ts +6 -20
  495. package/src/runtime/routes/oauth-commands-routes.ts +50 -0
  496. package/src/runtime/routes/oauth-providers.ts +7 -0
  497. package/src/runtime/routes/platform-routes.ts +18 -15
  498. package/src/runtime/routes/workspace-routes.test.ts +55 -26
  499. package/src/runtime/routes/workspace-routes.ts +46 -63
  500. package/src/runtime/routes/workspace-tree-walk.test.ts +244 -0
  501. package/src/runtime/routes/workspace-tree-walk.ts +240 -0
  502. package/src/runtime/routes/workspace-utils.ts +17 -0
  503. package/src/runtime/services/__tests__/conversation-serializer.test.ts +28 -0
  504. package/src/runtime/services/conversation-serializer.ts +16 -3
  505. package/src/runtime/sync/resource-sync-events.ts +2 -1
  506. package/src/schedule/run-script.ts +2 -1
  507. package/src/schedule/schedule-store.ts +9 -2
  508. package/src/schedule/worker.ts +1 -1
  509. package/src/security/__tests__/ces-http-record-backend.test.ts +137 -0
  510. package/src/security/ces-http-record-backend.ts +120 -0
  511. package/src/security/ces-rpc-record-backend.ts +7 -0
  512. package/src/security/untrusted-content.ts +3 -1
  513. package/src/skills/__tests__/workspace-skill-md-signature.test.ts +60 -0
  514. package/src/skills/workspace-skill-md-signature.ts +50 -0
  515. package/src/subagent/consult-context.ts +12 -25
  516. package/src/tools/__tests__/side-effect-abort-guard.test.ts +1 -0
  517. package/src/tools/acp/list-agents.test.ts +3 -1
  518. package/src/tools/acp/spawn.test.ts +146 -11
  519. package/src/tools/acp/spawn.ts +21 -3
  520. package/src/tools/acp/status.test.ts +94 -0
  521. package/src/tools/acp/status.ts +40 -3
  522. package/src/tools/app-control/skill-proxy-bridge.ts +2 -1
  523. package/src/tools/browser/browser-execution.ts +1 -1
  524. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +9 -3
  525. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +2 -1
  526. package/src/tools/browser/cdp-client/extension-cdp-client.ts +3 -2
  527. package/src/tools/browser/network-recorder.ts +2 -1
  528. package/src/tools/capability-offer.test.ts +22 -2
  529. package/src/tools/capability-offer.ts +16 -1
  530. package/src/tools/computer-use/definitions.ts +121 -2
  531. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -1
  532. package/src/tools/credentials/metadata-store.ts +100 -0
  533. package/src/tools/filesystem/search.ts +2 -1
  534. package/src/tools/host-filesystem/edit.test.ts +2 -1
  535. package/src/tools/host-filesystem/edit.ts +2 -2
  536. package/src/tools/host-filesystem/read.test.ts +2 -1
  537. package/src/tools/host-filesystem/read.ts +2 -2
  538. package/src/tools/host-filesystem/transfer.test.ts +2 -1
  539. package/src/tools/host-filesystem/transfer.ts +2 -2
  540. package/src/tools/host-filesystem/write.test.ts +2 -1
  541. package/src/tools/host-filesystem/write.ts +2 -2
  542. package/src/tools/host-terminal/host-shell.test.ts +16 -0
  543. package/src/tools/host-terminal/host-shell.ts +4 -20
  544. package/src/tools/network/web-search.ts +6 -1
  545. package/src/tools/policy-context.ts +6 -7
  546. package/src/tools/skills/scaffold-managed.ts +61 -3
  547. package/src/tools/subagent/spawn.ts +5 -8
  548. package/src/tools/terminal/__tests__/sanitized-bash.test.ts +57 -0
  549. package/src/tools/terminal/sanitized-bash.ts +105 -0
  550. package/src/tools/ui-surface/channel-variants.ts +10 -59
  551. package/src/tools/watch/watch-retro-report.ts +0 -8
  552. package/src/types/host-capabilities.ts +5 -3
  553. package/src/util/__tests__/language-subtag.test.ts +23 -1
  554. package/src/util/ca-bundle.ts +13 -0
  555. package/src/util/language-subtag.ts +16 -0
  556. package/src/util/platform.ts +8 -0
  557. package/src/util/unicode.ts +15 -1
  558. package/src/watch/watch-retro.ts +6 -8
  559. package/src/watch/watch-timeline.ts +2 -2
  560. package/src/workflows/run-manager.ts +2 -1
  561. package/src/workspace/git-porcelain.ts +66 -0
  562. package/src/workspace/git-service.ts +6 -55
  563. package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +258 -0
  564. package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +78 -0
  565. package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +214 -0
  566. package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +102 -0
  567. package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +199 -0
  568. package/src/workspace/migrations/registry.ts +6 -0
  569. package/src/__tests__/app-source-watcher.test.ts +0 -218
  570. package/src/__tests__/config-watcher-skill-reseed.test.ts +0 -359
  571. package/src/__tests__/speaker-identification.test.ts +0 -56
  572. package/src/calls/speaker-identification.ts +0 -233
  573. package/src/daemon/app-source-watcher.ts +0 -218
  574. package/src/messaging/providers/whatsapp/client.ts +0 -29
  575. package/src/runtime/routes/debug-bash-routes.ts +0 -173
  576. package/src/util/fs-watcher-error.ts +0 -36
  577. /package/src/{live-voice → calls}/progress-phrases.ts +0 -0
@@ -19,6 +19,7 @@
19
19
  * concept pages or seeded skill entries change.
20
20
  */
21
21
 
22
+ import { safeStringSlice } from "../host-utils.js";
22
23
  import { getLogger } from "../logging.js";
23
24
  import {
24
25
  CLI_COMMAND_SLUG_PREFIX,
@@ -51,7 +52,11 @@ const SUMMARY_MAX_LENGTH = 200;
51
52
  * and corrupt the format the router parses.
52
53
  */
53
54
  function normalizeSummary(raw: string): string {
54
- return raw.replace(/\s+/g, " ").trim().slice(0, SUMMARY_MAX_LENGTH);
55
+ return safeStringSlice(
56
+ raw.replace(/\s+/g, " ").trim(),
57
+ 0,
58
+ SUMMARY_MAX_LENGTH,
59
+ );
55
60
  }
56
61
 
57
62
  /**
@@ -1,4 +1,4 @@
1
- import { getConfig } from "../../../../config/loader.js";
1
+ import { loadWorkspaceMcpConfig } from "../../../../mcp/workspace-mcp-config.js";
2
2
 
3
3
  /**
4
4
  * Generic input for building capability statements: the capability fields of
@@ -92,7 +92,7 @@ export function augmentMcpSetupDescription(
92
92
  if (input.id !== "mcp-setup") {
93
93
  return input;
94
94
  }
95
- const servers = getConfig().mcp?.servers;
95
+ const servers = loadWorkspaceMcpConfig().servers;
96
96
  if (!servers) {
97
97
  return input;
98
98
  }
@@ -26,7 +26,7 @@ import {
26
26
  } from "../../graph/store.js";
27
27
  import type { MemoryNode } from "../../graph/types.js";
28
28
  import { isCapabilityNode } from "../../graph/types.js";
29
- import { BackendUnavailableError } from "../../host-utils.js";
29
+ import { BackendUnavailableError, safeStringSlice } from "../../host-utils.js";
30
30
  import { extractToolUse, userMessage } from "../../llm-helpers.js";
31
31
  import { getLogger } from "../../logging.js";
32
32
  import {
@@ -268,7 +268,9 @@ async function identifyDuplicateGroups(
268
268
  const listing = nodes
269
269
  .map((n) => {
270
270
  const preview =
271
- n.content.length > 100 ? n.content.slice(0, 100) + "…" : n.content;
271
+ n.content.length > 100
272
+ ? safeStringSlice(n.content, 0, 100) + "…"
273
+ : n.content;
272
274
  return `[${n.id}] ${preview}`;
273
275
  })
274
276
  .join("\n");
@@ -46,7 +46,7 @@ import type {
46
46
  SourceType,
47
47
  TriggerType,
48
48
  } from "../../graph/types.js";
49
- import { BackendUnavailableError } from "../../host-utils.js";
49
+ import { BackendUnavailableError, safeStringSlice } from "../../host-utils.js";
50
50
  import { extractToolUse, userMessage } from "../../llm-helpers.js";
51
51
  import { getLogger } from "../../logging.js";
52
52
  import { buildIdentityContext } from "../identity-context.js";
@@ -1617,7 +1617,9 @@ async function findCandidateNodes(
1617
1617
  await import("../../../../../persistence/embeddings/embed.js");
1618
1618
  const searchText =
1619
1619
  transcript.length > 3000
1620
- ? transcript.slice(0, 1500) + "\n...\n" + transcript.slice(-1500)
1620
+ ? safeStringSlice(transcript, 0, 1500) +
1621
+ "\n...\n" +
1622
+ safeStringSlice(transcript, transcript.length - 1500)
1621
1623
  : transcript;
1622
1624
 
1623
1625
  try {
@@ -33,6 +33,7 @@ import type {
33
33
  ScoredNode,
34
34
  } from "../../graph/types.js";
35
35
  import { isCapabilityNode } from "../../graph/types.js";
36
+ import { safeStringSlice } from "../../host-utils.js";
36
37
  import { extractToolUse, userMessage } from "../../llm-helpers.js";
37
38
  import { getLogger } from "../../logging.js";
38
39
  import { searchGraphNodes } from "./graph-search.js";
@@ -491,7 +492,9 @@ export async function loadContextMemory(
491
492
  try {
492
493
  const queryText = opts.recentSummaries.join("\n\n");
493
494
  const truncated =
494
- queryText.length > 3000 ? queryText.slice(0, 3000) : queryText;
495
+ queryText.length > 3000
496
+ ? safeStringSlice(queryText, 0, 3000)
497
+ : queryText;
495
498
  contextQueryText = truncated;
496
499
  const result = await embedWithRetry(opts.config, [truncated], {
497
500
  signal: opts.signal,
@@ -3,6 +3,7 @@
3
3
  import { createHash } from "node:crypto";
4
4
 
5
5
  import type { AssistantConfig } from "../../../../config/types.js";
6
+ import { safeStringSlice } from "../host-utils.js";
6
7
  import { getLogger } from "../logging.js";
7
8
  import { getWorkspaceDir } from "../paths.js";
8
9
  import { readPage } from "../substrate/page-store.js";
@@ -57,7 +58,11 @@ function buildPassage(slug: string, body: string): string {
57
58
  const blank = trimmed.search(/\n\s*\n/);
58
59
  const para = blank === -1 ? trimmed : trimmed.slice(0, blank);
59
60
  const stripped = para.replace(/^#+\s.*\n/, "").trim();
60
- const compact = stripped.replace(/\s+/g, " ").slice(0, PASSAGE_CHAR_CAP);
61
+ const compact = safeStringSlice(
62
+ stripped.replace(/\s+/g, " "),
63
+ 0,
64
+ PASSAGE_CHAR_CAP,
65
+ );
61
66
  return `${slug}\n${compact}`;
62
67
  }
63
68
 
@@ -6,6 +6,8 @@
6
6
 
7
7
  import { describe, expect, test } from "bun:test";
8
8
 
9
+ import { stripOrphanedSurrogates } from "../../../../../util/unicode.js";
10
+ import { LINK_SEPARATOR } from "../../substrate/page-links.js";
9
11
  import { renderCard } from "../card.js";
10
12
 
11
13
  const PAGE = `---
@@ -73,6 +75,28 @@ Lead paragraph for page a.
73
75
  expect(line.length).toBeLessThan(300);
74
76
  });
75
77
 
78
+ test("capping a `current:` value never splits an emoji at the cap", () => {
79
+ // 279 BMP characters put the 280-character cap inside the emoji's pair.
80
+ const current = `${"x".repeat(279)}🎉 trailing`;
81
+ const card = renderCard(
82
+ "page-a",
83
+ `---\ncurrent: "${current}"\n---\n\nLead.\n`,
84
+ );
85
+ const line = card.split("\n")[1]!;
86
+ expect(stripOrphanedSurrogates(line)).toBe(line);
87
+ expect(line).toBe(`[current: ${"x".repeat(279)}…]`);
88
+ });
89
+
90
+ test("capping a link note never splits an emoji at the cap", () => {
91
+ const note = `${"n".repeat(79)}🎉 trailing`;
92
+ const card = renderCard(
93
+ "page-a",
94
+ `---\nkind: index\nlinks:\n - "other${LINK_SEPARATOR}${note}"\n---\n\nLead.\n`,
95
+ );
96
+ expect(stripOrphanedSurrogates(card)).toBe(card);
97
+ expect(card).toContain(`other${LINK_SEPARATOR}${"n".repeat(79)}…`);
98
+ });
99
+
76
100
  test("the `status:` draft marker does NOT render as a card line", () => {
77
101
  const card = renderCard("page-a", `---\nstatus: cc-draft\n---\n\nLead.\n`);
78
102
  expect(card).not.toContain("cc-draft");
@@ -39,7 +39,9 @@ import type {
39
39
  SendMessageOptions,
40
40
  } from "@vellumai/plugin-api";
41
41
 
42
+ import { OpenRouterProvider } from "../../../../../providers/openrouter/client.js";
42
43
  import { ProviderError } from "../../../../../util/errors.js";
44
+ import { stripOrphanedSurrogates } from "../../../../../util/unicode.js";
43
45
  import { sectionHeadLine } from "../sections.js";
44
46
  import type { MemoryRoutingTurn, Section } from "../types.js";
45
47
 
@@ -842,4 +844,127 @@ describe("selectPool: sections and keyword-in-context snippets", () => {
842
844
  true,
843
845
  );
844
846
  });
847
+ test("a window whose edges land inside an emoji keeps both pairs whole and the request well-formed", async () => {
848
+ providerStub = makeProvider(toolUseResponse({ ids: [] }));
849
+ // 200 emoji (400 code units) on each side of the term, offset so that a
850
+ // code-unit window centered on the term starts and ends mid-pair. A
851
+ // window cut there carries half a pair on each edge, which strict
852
+ // provider parsers reject once JSON.stringify escapes each orphan.
853
+ const emoji = "😀".repeat(200);
854
+ const body = `${emoji}y turnip ${emoji}`;
855
+ const term = body.indexOf("turnip");
856
+ const naiveStart = term + 3 - 150;
857
+ const naive = body.slice(naiveStart, naiveStart + 300);
858
+ expect(stripOrphanedSurrogates(naive)).not.toBe(naive);
859
+ const section = sectionOf("page-a", "Rollout", body);
860
+ await selectPool(
861
+ finderOnly({
862
+ slug: "page-a",
863
+ descriptor: section.text,
864
+ section,
865
+ terms: ["turnip"],
866
+ }),
867
+ makeTurn("turnip?"),
868
+ );
869
+ const [block] = sentBlocks();
870
+ expect(stripOrphanedSurrogates(block.text)).toBe(block.text);
871
+ const line = block.text
872
+ .split("\n")
873
+ .find((l) => l.startsWith("[1] page-a "))!;
874
+ expect(line).toContain("§Rollout: … ");
875
+ expect(line).toContain("turnip");
876
+ expect(line.endsWith(" …")).toBe(true);
877
+ // The window stays bounded: at most one code unit shorter on each edge.
878
+ const windowStart = line.indexOf("… ") + "… ".length;
879
+ expect(line.length - windowStart).toBeLessThanOrEqual(300 + " …".length);
880
+ expect(line.length - windowStart).toBeGreaterThanOrEqual(298 + " …".length);
881
+ });
882
+ });
883
+
884
+ // ---------------------------------------------------------------------------
885
+ // selectPool: cataloged thinking and forced-tool compatibility.
886
+ // ---------------------------------------------------------------------------
887
+
888
+ describe("selectPool: cataloged thinking and forced-tool compatibility", () => {
889
+ test("OpenRouter Kimi K2.6 omits the forced choice and yields a structured selection", async () => {
890
+ // A real OpenAI chat-completions provider stands in for the cataloged
891
+ // OpenRouter Kimi profile. The request succeeds without a reactive retry
892
+ // because the forced select_pages choice is omitted before dispatch.
893
+ const wireRequests: unknown[] = [];
894
+ const kimi = new OpenRouterProvider(
895
+ "test-key",
896
+ "moonshotai/kimi-k2.6-20260420",
897
+ );
898
+ (kimi as unknown as { client: unknown }).client = {
899
+ chat: {
900
+ completions: {
901
+ create: async (params: unknown) => {
902
+ wireRequests.push(JSON.parse(JSON.stringify(params)));
903
+ return {
904
+ async *[Symbol.asyncIterator]() {
905
+ yield {
906
+ choices: [
907
+ {
908
+ delta: {
909
+ tool_calls: [
910
+ {
911
+ index: 0,
912
+ id: "call-1",
913
+ function: {
914
+ name: "select_pages",
915
+ arguments: '{"ids":[3]}',
916
+ },
917
+ },
918
+ ],
919
+ },
920
+ finish_reason: "tool_calls",
921
+ },
922
+ ],
923
+ usage: { prompt_tokens: 10, completion_tokens: 2 },
924
+ };
925
+ },
926
+ };
927
+ },
928
+ },
929
+ },
930
+ };
931
+
932
+ // The profile layer injects the thinking effort onto the call config in
933
+ // production; emulate that here so the forced tool_choice rides alongside
934
+ // reasoning on the wire.
935
+ providerStub = {
936
+ name: "kimi-openai-compat",
937
+ sendMessage: (messages: Message[], options?: SendMessageOptions) =>
938
+ kimi.sendMessage(messages, {
939
+ ...options,
940
+ config: {
941
+ ...options?.config,
942
+ effort: "high",
943
+ thinking: { enabled: true },
944
+ },
945
+ }),
946
+ };
947
+
948
+ const selection = await selectPool(makePool(), makeTurn("rollout?"));
949
+
950
+ // The preflight path avoids an incompatible first request, and the
951
+ // pool-level re-prompt loop never engages.
952
+ expect(wireRequests).toHaveLength(1);
953
+ const first = wireRequests[0] as {
954
+ tool_choice?: unknown;
955
+ reasoning?: { effort?: string; summary?: string };
956
+ };
957
+ expect(first.tool_choice).toBeUndefined();
958
+ expect(first.reasoning).toMatchObject({
959
+ effort: "high",
960
+ summary: "detailed",
961
+ });
962
+ expect(warnPayloads().filter((p) => p.reason === "provider_error")).toEqual(
963
+ [],
964
+ );
965
+
966
+ // Structured selection survived: ids [3] is the topic-x finder line.
967
+ expect(selection.keptAll).toBe(false);
968
+ expect(selection.pages).toEqual([{ slug: "topic-x", sections: [] }]);
969
+ });
845
970
  });
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
+ import { stripOrphanedSurrogates } from "../../../../../util/unicode.js";
3
4
  import {
4
5
  buildSectionIndex,
5
6
  leadSectionOfBody,
@@ -108,6 +109,31 @@ describe("buildSectionIndex", () => {
108
109
  }
109
110
  });
110
111
 
112
+ test("a hard split never lands inside a surrogate pair and loses no text", async () => {
113
+ // One unbroken line of emoji longer than the window, offset by one BMP
114
+ // character so the window boundary falls between a pair's halves.
115
+ const unbroken = `y${"😀".repeat(SECTION_CHUNK_CHARS)}`;
116
+ const index = await buildSectionIndex(
117
+ ["page-a"],
118
+ reader({ "page-a": `## Big\n${unbroken}` }),
119
+ );
120
+
121
+ const big = index.sections.filter((s) => s.title === "Big");
122
+ expect(big.length).toBeGreaterThan(1);
123
+ for (const chunk of big) {
124
+ expect(chunk.text.length).toBeLessThanOrEqual(SECTION_CHUNK_CHARS);
125
+ expect(stripOrphanedSurrogates(chunk.text)).toBe(chunk.text);
126
+ }
127
+ expect(big.map(sectionBody).join("")).toBe(unbroken);
128
+ });
129
+
130
+ test("a capped head-line title never ends in half an emoji", () => {
131
+ const title = `${"t".repeat(199)}🎉 more`;
132
+ const head = sectionHeadLine("page-a", title);
133
+ expect(stripOrphanedSurrogates(head)).toBe(head);
134
+ expect(head).toBe(`page-a - ${"t".repeat(199)}`);
135
+ });
136
+
111
137
  test("a heading longer than the window still chunks by near-window bodies; the key keeps the full title", async () => {
112
138
  const title = "H".repeat(SECTION_CHUNK_CHARS + 100);
113
139
  const body = "b".repeat(SECTION_CHUNK_CHARS * 2);
@@ -38,6 +38,7 @@ import { ESCALATION_CONTINUATION_CONTENT } from "../../../../../calls/voice-tria
38
38
  import { MemoryV3GateSchema } from "../../../../../config/schemas/memory-v3.js";
39
39
  import { ensureMemoryV3SelectionsSchema } from "../../../../../persistence/migrations/338-move-memory-v3-selections-to-memory-db.js";
40
40
  import * as schema from "../../../../../persistence/schema/index.js";
41
+ import { stripOrphanedSurrogates } from "../../../../../util/unicode.js";
41
42
  import type { HotSetEntry, HotSetOptions } from "../hot-set.js";
42
43
  import type { OrchestrateResult } from "../orchestrate.js";
43
44
  import {
@@ -1083,6 +1084,35 @@ describe("memory-v3 engine", () => {
1083
1084
  expect(turn.previousAssistantMessage).toBeUndefined();
1084
1085
  });
1085
1086
 
1087
+ test("the reply tail never starts inside a surrogate pair", async () => {
1088
+ // 1300 emoji then one BMP character: the 2500-code-unit tail would start
1089
+ // on a low surrogate, an orphan the embedding request's parser rejects.
1090
+ const reply = `${"😀".repeat(1300)}a`;
1091
+ messages = [
1092
+ {
1093
+ role: "user",
1094
+ content: JSON.stringify([{ type: "text", text: "first question" }]),
1095
+ },
1096
+ {
1097
+ role: "assistant",
1098
+ content: JSON.stringify([{ type: "text", text: reply }]),
1099
+ },
1100
+ {
1101
+ role: "user",
1102
+ content: JSON.stringify([{ type: "text", text: "and now this" }]),
1103
+ },
1104
+ ];
1105
+ await observeTurn("conv-1", 1);
1106
+
1107
+ const turn = (
1108
+ orchestrateSpy.mock.calls as unknown as unknown[][]
1109
+ )[0]![0] as MemoryRoutingTurn;
1110
+ const tail = turn.previousAssistantMessage!;
1111
+ expect(stripOrphanedSurrogates(tail)).toBe(tail);
1112
+ expect(tail.length).toBeLessThanOrEqual(2500);
1113
+ expect(reply.endsWith(tail)).toBe(true);
1114
+ });
1115
+
1086
1116
  test("a page with lines from two lanes is attributed the lane of the line whose section was selected", () => {
1087
1117
  const bulk: Section = {
1088
1118
  article: "page-two",
@@ -1,4 +1,5 @@
1
1
  import { FRONTMATTER_REGEX, parseFrontmatterFields } from "../frontmatter.js";
2
+ import { safeStringSlice } from "../host-utils.js";
2
3
  import { injectedConceptHeader } from "../substrate/injected-block-slugs.js";
3
4
  import { LINK_SEPARATOR, parseLinkEntry } from "../substrate/page-links.js";
4
5
  import type { Slug } from "./types.js";
@@ -38,7 +39,7 @@ function renderLinkEntry(entry: string): string {
38
39
  }
39
40
  const note =
40
41
  description.length > LINK_NOTE_MAX_CHARS
41
- ? `${description.slice(0, LINK_NOTE_MAX_CHARS).trimEnd()}…`
42
+ ? `${safeStringSlice(description, 0, LINK_NOTE_MAX_CHARS).trimEnd()}…`
42
43
  : description;
43
44
  return `${target}${LINK_SEPARATOR}${note}`;
44
45
  }
@@ -103,7 +104,7 @@ export function renderCurrentLine(
103
104
  }
104
105
  const capped =
105
106
  collapsed.length > CURRENT_MAX_CHARS
106
- ? `${collapsed.slice(0, CURRENT_MAX_CHARS).trimEnd()}…`
107
+ ? `${safeStringSlice(collapsed, 0, CURRENT_MAX_CHARS).trimEnd()}…`
107
108
  : collapsed;
108
109
  return `[current: ${capped}]`;
109
110
  }
@@ -57,7 +57,7 @@ import { z } from "zod";
57
57
 
58
58
  import { classifyConversationError } from "../../../../daemon/conversation-error.js";
59
59
  import type { PendingConversationNotice } from "../../../../daemon/conversation-notices.js";
60
- import { redactLogString, truncate } from "../host-utils.js";
60
+ import { redactLogString, safeStringSlice, truncate } from "../host-utils.js";
61
61
  import {
62
62
  cachedTextBlock,
63
63
  extractToolUse,
@@ -276,7 +276,9 @@ function oneLine(text: string): string {
276
276
  * `§<title>: … <window> …`, the window being up to {@link SNIPPET_MAX_CHARS}
277
277
  * of the one-line section body centered on the first occurrence of the
278
278
  * best-contributing query term that occurs in it (the lead, titled `""`,
279
- * renders the window alone). `undefined` when no term occurs in the body.
279
+ * renders the window alone). The window edges never split a surrogate pair:
280
+ * an orphaned half is invalid UTF-16 and strict provider parsers reject the
281
+ * whole request. `undefined` when no term occurs in the body.
280
282
  */
281
283
  function renderKeywordInContext(
282
284
  section: Section,
@@ -299,7 +301,7 @@ function renderKeywordInContext(
299
301
  const end = Math.min(body.length, start + SNIPPET_MAX_CHARS);
300
302
  const window = [
301
303
  start > 0 ? "… " : "",
302
- body.slice(start, end).trim(),
304
+ safeStringSlice(body, start, end).trim(),
303
305
  end < body.length ? " …" : "",
304
306
  ].join("");
305
307
  const title = section.title.trim();
@@ -1,3 +1,4 @@
1
+ import { safeStringSlice } from "../host-utils.js";
1
2
  import type { Section, SectionIndex, Slug } from "./types.js";
2
3
 
3
4
  /**
@@ -44,7 +45,7 @@ function lastSlugSegment(slug: Slug): string {
44
45
  * {@link sectionBody}.
45
46
  */
46
47
  export function sectionHeadLine(article: Slug, title: string): string {
47
- return `${lastSlugSegment(article)} - ${title.slice(0, SECTION_HEAD_TITLE_CHARS)}`;
48
+ return `${lastSlugSegment(article)} - ${safeStringSlice(title, 0, SECTION_HEAD_TITLE_CHARS)}`;
48
49
  }
49
50
 
50
51
  /**
@@ -95,7 +96,9 @@ export function splitIntoRawSections(body: string): RawSection[] {
95
96
  /**
96
97
  * Split `text` into chunks no longer than `limit`, preferring to break on
97
98
  * newlines so chunks stay readable. A single line longer than the limit is
98
- * hard-split at the limit. Order is preserved.
99
+ * hard-split at the limit, backed off one code unit when the limit would cut
100
+ * a surrogate pair (an orphaned half is invalid UTF-16 that strict provider
101
+ * parsers reject). Order is preserved.
99
102
  */
100
103
  function chunkText(text: string, limit: number): string[] {
101
104
  if (text.length <= limit) {
@@ -109,7 +112,10 @@ function chunkText(text: string, limit: number): string[] {
109
112
  const newlineBreak = window.lastIndexOf("\n");
110
113
  // Only break on a newline if it leaves a non-trivial chunk; otherwise hard
111
114
  // split at the window boundary to guarantee forward progress.
112
- const breakAt = newlineBreak > 0 ? newlineBreak : limit;
115
+ const breakAt =
116
+ newlineBreak > 0
117
+ ? newlineBreak
118
+ : Math.max(1, safeStringSlice(remaining, 0, limit).length);
113
119
  chunks.push(remaining.slice(0, breakAt));
114
120
  remaining = remaining.slice(breakAt).replace(/^\n/, "");
115
121
  }
@@ -44,7 +44,7 @@ import {
44
44
  timeLatencySubSpan,
45
45
  } from "../../../../daemon/turn-latency-sub-spans.js";
46
46
  import { enqueueMemoryJob } from "../../../../persistence/jobs-store.js";
47
- import { stripCommentLines } from "../host-utils.js";
47
+ import { safeStringSlice, stripCommentLines } from "../host-utils.js";
48
48
  import { getLogger } from "../logging.js";
49
49
  import { type MemorySqlite, memorySqliteOrNull } from "../memory-db.js";
50
50
  import { getWorkspaceDir, getWorkspacePromptPath } from "../paths.js";
@@ -578,7 +578,10 @@ async function buildShadowTurn(
578
578
  }
579
579
  const text = stringifyMessageContent(rows[i]!.content);
580
580
  if (text.length > 0) {
581
- previousAssistantMessage = text.slice(-REPLY_QUERY_TAIL_CHARS);
581
+ previousAssistantMessage = safeStringSlice(
582
+ text,
583
+ Math.max(0, text.length - REPLY_QUERY_TAIL_CHARS),
584
+ );
582
585
  break;
583
586
  }
584
587
  }
@@ -16,6 +16,10 @@ import { getConfig } from "../../../config/loader.js";
16
16
  import { isMemoryEnabled } from "../../../config/memory-v3-gate.js";
17
17
  import { rehydratePlatformCredentials } from "../../../config/platform-rehydration.js";
18
18
  import { startConversationEvictor } from "../../../daemon/conversation-evictor.js";
19
+ import {
20
+ startWorkspaceSkillMdMtimePoll,
21
+ stopWorkspaceSkillMdMtimePoll,
22
+ } from "../../../daemon/skill-memory-refresh.js";
19
23
  import { resetDb } from "../../../persistence/db-connection.js";
20
24
  import {
21
25
  EMBEDDING_SHUTDOWN_BUDGET_MS,
@@ -108,6 +112,7 @@ async function main(): Promise<void> {
108
112
  shuttingDown = true;
109
113
  log.info({ signal }, "Memory worker process shutting down");
110
114
  worker?.stop();
115
+ stopWorkspaceSkillMdMtimePoll();
111
116
  if (keepAlive != null) {
112
117
  clearInterval(keepAlive);
113
118
  }
@@ -161,6 +166,7 @@ async function main(): Promise<void> {
161
166
  }
162
167
 
163
168
  worker = startMemoryJobsWorkerLoop();
169
+ startWorkspaceSkillMdMtimePoll();
164
170
 
165
171
  // Keep-alive: the worker's setTimeout timers are unref'd, so without
166
172
  // this interval the process would exit immediately.
@@ -14,7 +14,7 @@
14
14
  * This module reads those files and projects each entry onto the
15
15
  * assistant's own `McpServerConfig` shape, so a plugin-declared server can
16
16
  * flow through the same surfaces as one configured in the workspace
17
- * `config.json`.
17
+ * `mcp.json`.
18
18
  *
19
19
  * Failure isolation follows the spec: an invalid top-level `mcp.json`
20
20
  * disables MCP for that plugin only, and an invalid individual server
@@ -39,17 +39,17 @@
39
39
  import { existsSync, readFileSync } from "node:fs";
40
40
  import { join } from "node:path";
41
41
 
42
- import { z } from "zod";
43
-
44
42
  import {
45
43
  type AllPluginInfo,
46
44
  listAllPlugins,
47
45
  type ListInstalledPluginsOptions,
48
46
  } from "../cli/lib/list-installed-plugins.js";
49
- import type {
50
- McpTransport,
51
- ResolvedMcpServerConfig,
52
- } from "../config/schemas/mcp.js";
47
+ import type { ResolvedMcpServerConfig } from "../config/schemas/mcp.js";
48
+ import {
49
+ projectSpecServerToTransport,
50
+ SpecMcpDocumentSchema,
51
+ SpecMcpServerSchema,
52
+ } from "../mcp/spec-schema.js";
53
53
  import { getLogger } from "../util/logger.js";
54
54
 
55
55
  const log = getLogger("plugin-mcp-servers");
@@ -77,45 +77,6 @@ export const PLUGIN_MCP_MANIFEST = "mcp.json";
77
77
  * origin risk.
78
78
  */
79
79
 
80
- // ---------------------------------------------------------------------------
81
- // Wire schema (Agent Plugins 1.0.0)
82
- // ---------------------------------------------------------------------------
83
-
84
- const PluginStdioServerSchema = z.object({
85
- type: z.literal("stdio"),
86
- command: z.string().min(1),
87
- args: z.array(z.string()).optional(),
88
- env: z.record(z.string(), z.string()).optional(),
89
- cwd: z.string().optional(),
90
- });
91
-
92
- /**
93
- * Remote MCP entry. Agent Plugins 1.0.0 requires `type` and names no
94
- * default. MCP's current remote transport is Streamable HTTP, so this
95
- * host fills omitted `type` and the Claude-style `http` alias as
96
- * `streamable-http`. `sse` stays explicit.
97
- */
98
- const PluginHttpServerSchema = z.object({
99
- type: z
100
- .union([
101
- z.literal("streamable-http"),
102
- z.literal("sse"),
103
- z.literal("http").transform(() => "streamable-http" as const),
104
- ])
105
- .default("streamable-http"),
106
- url: z.string().min(1),
107
- headers: z.record(z.string(), z.string()).optional(),
108
- });
109
-
110
- const PluginMcpServerSchema = z.union([
111
- PluginStdioServerSchema,
112
- PluginHttpServerSchema,
113
- ]);
114
-
115
- const PluginMcpManifestSchema = z.object({
116
- mcpServers: z.record(z.string(), z.unknown()),
117
- });
118
-
119
80
  // ---------------------------------------------------------------------------
120
81
  // Public shape
121
82
  // ---------------------------------------------------------------------------
@@ -251,7 +212,7 @@ export function readPluginMcpServers(
251
212
  continue;
252
213
  }
253
214
 
254
- const entry = PluginMcpServerSchema.safeParse(raw);
215
+ const entry = SpecMcpServerSchema.safeParse(raw);
255
216
  if (!entry.success) {
256
217
  issues.push({
257
218
  pluginName: plugin.name,
@@ -325,7 +286,7 @@ function parseManifest(
325
286
  };
326
287
  }
327
288
 
328
- const manifest = PluginMcpManifestSchema.safeParse(json);
289
+ const manifest = SpecMcpDocumentSchema.safeParse(json);
329
290
  if (!manifest.success) {
330
291
  return {
331
292
  error: `${PLUGIN_MCP_MANIFEST} is missing a valid "mcpServers" object`,
@@ -334,33 +295,12 @@ function parseManifest(
334
295
  return { mcpServers: manifest.data.mcpServers };
335
296
  }
336
297
 
337
- /**
338
- * Project one spec-shaped entry onto the assistant's transport union. The
339
- * two vocabularies already agree on type names and required fields, so this
340
- * is a field copy plus path interpolation for the stdio case.
341
- */
342
298
  function projectTransport(
343
- entry: z.infer<typeof PluginMcpServerSchema>,
299
+ entry: Parameters<typeof projectSpecServerToTransport>[0],
344
300
  pluginRoot: string,
345
- ): McpTransport {
346
- if (entry.type === "stdio") {
347
- const pluginData = join(pluginRoot, "data");
348
- const expand = (v: string): string =>
349
- interpolatePluginPaths(v, pluginRoot, pluginData);
350
- return {
351
- type: "stdio",
352
- command: entry.command,
353
- args: (entry.args ?? []).map(expand),
354
- ...(entry.env && {
355
- env: Object.fromEntries(
356
- Object.entries(entry.env).map(([k, v]) => [k, expand(v)]),
357
- ),
358
- }),
359
- };
360
- }
361
- return {
362
- type: entry.type,
363
- url: entry.url,
364
- ...(entry.headers && { headers: entry.headers }),
365
- };
301
+ ) {
302
+ const pluginData = join(pluginRoot, "data");
303
+ return projectSpecServerToTransport(entry, (value) =>
304
+ interpolatePluginPaths(value, pluginRoot, pluginData),
305
+ );
366
306
  }