@vellumai/assistant 0.8.1 → 0.8.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 (506) hide show
  1. package/ARCHITECTURE.md +2 -7
  2. package/Dockerfile +75 -1
  3. package/bun.lock +11 -1
  4. package/docker-entrypoint.sh +5 -0
  5. package/docker-init-apt-root.sh +94 -0
  6. package/docker-kata-apt-env.sh +39 -0
  7. package/docs/plugins.md +88 -47
  8. package/docs/skills.md +9 -7
  9. package/examples/plugins/echo/README.md +27 -27
  10. package/examples/plugins/echo/package.json +3 -0
  11. package/examples/plugins/echo/register.ts +31 -31
  12. package/node_modules/@vellumai/slack-text/src/index.test.ts +114 -14
  13. package/node_modules/@vellumai/slack-text/src/index.ts +82 -18
  14. package/openapi.yaml +325 -3
  15. package/package.json +3 -1
  16. package/scripts/generate-openapi.ts +83 -10
  17. package/scripts/sync-llm-catalog.ts +2 -2
  18. package/scripts/sync-web-search-catalog.ts +47 -25
  19. package/src/__tests__/agent-image-optimize.test.ts +11 -3
  20. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +131 -0
  21. package/src/__tests__/anthropic-provider.test.ts +45 -0
  22. package/src/__tests__/app-builder-tool-scripts.test.ts +9 -3
  23. package/src/__tests__/app-executors.test.ts +220 -4
  24. package/src/__tests__/auto-analysis-end-to-end.test.ts +35 -0
  25. package/src/__tests__/bundled-asset.test.ts +6 -6
  26. package/src/__tests__/channel-availability-routes.test.ts +206 -0
  27. package/src/__tests__/channel-delivery-store.test.ts +289 -1
  28. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -1
  29. package/src/__tests__/clawhub.test.ts +75 -16
  30. package/src/__tests__/compactor-tail-resolution.test.ts +41 -0
  31. package/src/__tests__/config-schema.test.ts +21 -0
  32. package/src/__tests__/config-set-route.test.ts +80 -0
  33. package/src/__tests__/config-sounds-sync.test.ts +97 -0
  34. package/src/__tests__/config-watcher-skill-reseed.test.ts +453 -0
  35. package/src/__tests__/context-search-conversations-source.test.ts +117 -2
  36. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  37. package/src/__tests__/context-search-workspace-source.test.ts +7 -0
  38. package/src/__tests__/context-token-estimator.test.ts +1 -0
  39. package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
  40. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
  41. package/src/__tests__/conversation-agent-loop-overflow.test.ts +92 -92
  42. package/src/__tests__/conversation-agent-loop.test.ts +2 -0
  43. package/src/__tests__/conversation-error.test.ts +42 -3
  44. package/src/__tests__/conversation-fork-crud.test.ts +82 -0
  45. package/src/__tests__/conversation-inference-profile-route.test.ts +40 -4
  46. package/src/__tests__/conversation-lifecycle.test.ts +173 -0
  47. package/src/__tests__/conversation-message-sync-tags.test.ts +97 -0
  48. package/src/__tests__/conversation-pairing.test.ts +54 -0
  49. package/src/__tests__/conversation-process-callsite.test.ts +4 -1
  50. package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -1
  51. package/src/__tests__/conversation-queue.test.ts +4 -1
  52. package/src/__tests__/conversation-runtime-assembly.test.ts +76 -9
  53. package/src/__tests__/conversation-slash-queue.test.ts +59 -1
  54. package/src/__tests__/conversation-slash-unknown.test.ts +4 -1
  55. package/src/__tests__/conversation-surfaces-table-action.test.ts +360 -0
  56. package/src/__tests__/conversation-sync-tags.test.ts +235 -0
  57. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  58. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  59. package/src/__tests__/credential-security-invariants.test.ts +3 -2
  60. package/src/__tests__/db-slack-external-content-normalization.test.ts +301 -0
  61. package/src/__tests__/delete-managed-skill-tool.test.ts +55 -13
  62. package/src/__tests__/disk-pressure-tools.test.ts +1 -0
  63. package/src/__tests__/dm-backfill.test.ts +121 -10
  64. package/src/__tests__/document-tool-security.test.ts +258 -0
  65. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  66. package/src/__tests__/edit-propagation.test.ts +33 -0
  67. package/src/__tests__/empty-response-pipeline.test.ts +0 -4
  68. package/src/__tests__/external-plugin-loader.test.ts +60 -36
  69. package/src/__tests__/filing-service.test.ts +140 -0
  70. package/src/__tests__/get-skill-detail-audit.test.ts +0 -4
  71. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +43 -62
  72. package/src/__tests__/helpers/tar-fixtures.ts +39 -0
  73. package/src/__tests__/helpers/wait-for.ts +21 -0
  74. package/src/__tests__/history-repair-pipeline.test.ts +0 -3
  75. package/src/__tests__/history-repair.test.ts +73 -0
  76. package/src/__tests__/host-app-control-proxy.test.ts +266 -10
  77. package/src/__tests__/image-credentials.test.ts +1 -1
  78. package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
  79. package/src/__tests__/inference-no-mode-boot-e2e.test.ts +1 -1
  80. package/src/__tests__/inference-profile-reaper.test.ts +4 -2
  81. package/src/__tests__/inference-profile-session-handler.test.ts +18 -6
  82. package/src/__tests__/inference-profile-session-ipc.test.ts +17 -5
  83. package/src/__tests__/injector-chain.test.ts +10 -8
  84. package/src/__tests__/install-skill-routing.test.ts +155 -37
  85. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +92 -3
  86. package/src/__tests__/list-messages-page-latest.test.ts +55 -0
  87. package/src/__tests__/llm-call-pipeline.test.ts +0 -3
  88. package/src/__tests__/llm-catalog-parity.test.ts +55 -13
  89. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +34 -0
  90. package/src/__tests__/llm-request-log-source-factory.test.ts +29 -53
  91. package/src/__tests__/llm-usage-store.test.ts +114 -0
  92. package/src/__tests__/managed-profile-guard.test.ts +31 -29
  93. package/src/__tests__/managed-skill-lifecycle.test.ts +109 -18
  94. package/src/__tests__/managed-store.test.ts +84 -192
  95. package/src/__tests__/media-generate-image.test.ts +1 -1
  96. package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -2
  97. package/src/__tests__/messages-after-tiebreaker.test.ts +122 -0
  98. package/src/__tests__/oauth-commands-routes.test.ts +168 -16
  99. package/src/__tests__/oauth-provider-profiles.test.ts +9 -0
  100. package/src/__tests__/openai-provider.test.ts +24 -0
  101. package/src/__tests__/openai-responses-cutover-guard.test.ts +17 -9
  102. package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -2
  103. package/src/__tests__/persistence-pipeline.test.ts +0 -2
  104. package/src/__tests__/{managed-proxy-context.test.ts → platform-proxy-context.test.ts} +1 -1
  105. package/src/__tests__/platform.test.ts +2 -0
  106. package/src/__tests__/plugin-api-shim.test.ts +125 -0
  107. package/src/__tests__/plugin-bootstrap.test.ts +10 -36
  108. package/src/__tests__/plugin-external-api.test.ts +68 -0
  109. package/src/__tests__/plugin-registry.test.ts +0 -77
  110. package/src/__tests__/plugin-route-contribution.test.ts +0 -1
  111. package/src/__tests__/plugin-skill-contribution.test.ts +0 -2
  112. package/src/__tests__/plugin-tool-contribution.test.ts +16 -15
  113. package/src/__tests__/plugin-types.test.ts +3 -13
  114. package/src/__tests__/process-message-background-slack.test.ts +8 -1
  115. package/src/__tests__/process-message-display-content.test.ts +421 -0
  116. package/src/__tests__/provider-catalog-visibility.test.ts +142 -0
  117. package/src/__tests__/provider-error-scenarios.test.ts +111 -0
  118. package/src/__tests__/{provider-managed-proxy-integration.test.ts → provider-platform-proxy-integration.test.ts} +8 -8
  119. package/src/__tests__/scaffold-managed-skill-tool.test.ts +65 -13
  120. package/src/__tests__/schedule-routes.test.ts +50 -3
  121. package/src/__tests__/schedule-store.test.ts +94 -0
  122. package/src/__tests__/scheduler-reuse-conversation.test.ts +54 -7
  123. package/src/__tests__/schema-transforms.test.ts +20 -0
  124. package/src/__tests__/search-skills-unified.test.ts +0 -5
  125. package/src/__tests__/server-history-render.test.ts +43 -0
  126. package/src/__tests__/skill-load-feature-flag.test.ts +0 -12
  127. package/src/__tests__/skill-load-tool.test.ts +27 -89
  128. package/src/__tests__/skill-memory.test.ts +23 -3
  129. package/src/__tests__/skills-file-content-endpoint.test.ts +9 -38
  130. package/src/__tests__/skills-files-catalog-fallback.test.ts +0 -3
  131. package/src/__tests__/skills-install-extract.test.ts +49 -38
  132. package/src/__tests__/skills-install-staging.test.ts +159 -0
  133. package/src/__tests__/skills-uninstall.test.ts +9 -41
  134. package/src/__tests__/skills.test.ts +51 -58
  135. package/src/__tests__/slack-channel-config.test.ts +9 -0
  136. package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
  137. package/src/__tests__/system-prompt.test.ts +737 -63
  138. package/src/__tests__/terminal-tools.test.ts +28 -1
  139. package/src/__tests__/thread-backfill.test.ts +557 -27
  140. package/src/__tests__/title-generate-pipeline.test.ts +0 -13
  141. package/src/__tests__/token-estimate-pipeline.test.ts +0 -3
  142. package/src/__tests__/tool-error-pipeline.test.ts +0 -3
  143. package/src/__tests__/tool-execute-pipeline.test.ts +0 -5
  144. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -1
  145. package/src/__tests__/tool-executor.test.ts +16 -4
  146. package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -12
  147. package/src/__tests__/turn-events-store.test.ts +256 -0
  148. package/src/__tests__/twilio-routes.test.ts +4 -0
  149. package/src/__tests__/user-plugin-loader.test.ts +0 -7
  150. package/src/__tests__/voice-session-bridge.test.ts +198 -0
  151. package/src/__tests__/web-search-catalog-parity.test.ts +32 -10
  152. package/src/__tests__/workspace-migration-057-repair-stale-gemini-model-ids.test.ts +115 -3
  153. package/src/__tests__/workspace-migration-072-seed-reply-suggestion-callsite.test.ts +50 -0
  154. package/src/__tests__/workspace-migration-073-repair-recall-callsite-empty-profile.test.ts +153 -0
  155. package/src/__tests__/workspace-migration-085-memory-v2-bm25-b-reembed-disabled-v2-pages.test.ts +220 -0
  156. package/src/__tests__/workspace-migration-086-revert-stale-gemini-mis-rewrites.test.ts +269 -0
  157. package/src/__tests__/workspace-migration-remove-legacy-skills-index.test.ts +309 -0
  158. package/src/__tests__/workspace-migrations-runner.test.ts +111 -3
  159. package/src/acp/resolve-agent.ts +1 -1
  160. package/src/agent/image-optimize.ts +13 -5
  161. package/src/calls/voice-session-bridge.ts +61 -42
  162. package/src/channels/types.ts +108 -0
  163. package/src/cli/__tests__/unknown-command.test.ts +24 -0
  164. package/src/cli/commands/__tests__/changelog.test.ts +304 -319
  165. package/src/cli/commands/__tests__/schedules.test.ts +491 -0
  166. package/src/cli/commands/changelog.ts +106 -42
  167. package/src/cli/commands/conversations.ts +102 -17
  168. package/src/cli/commands/default-action.ts +10 -53
  169. package/src/cli/commands/notifications.ts +329 -317
  170. package/src/cli/commands/plugins.ts +185 -0
  171. package/src/cli/commands/schedules.ts +391 -0
  172. package/src/cli/commands/telemetry.ts +40 -0
  173. package/src/cli/lib/__tests__/cli-colors.test.ts +48 -0
  174. package/src/cli/lib/__tests__/confirm-prompt.test.ts +159 -0
  175. package/src/cli/lib/__tests__/install-from-github.test.ts +355 -0
  176. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +154 -0
  177. package/src/cli/lib/__tests__/uninstall-plugin.test.ts +124 -0
  178. package/src/cli/lib/__tests__/unknown-command.test.ts +106 -0
  179. package/src/cli/lib/cli-colors.ts +12 -0
  180. package/src/cli/lib/confirm-prompt.ts +79 -0
  181. package/src/cli/lib/install-from-github.ts +304 -0
  182. package/src/cli/lib/list-installed-plugins.ts +137 -0
  183. package/src/cli/lib/uninstall-plugin.ts +82 -0
  184. package/src/cli/lib/unknown-command.ts +111 -0
  185. package/src/cli/program.ts +38 -2
  186. package/src/config/bundled-skills/app-builder/SKILL.md +23 -21
  187. package/src/config/bundled-skills/app-builder/TOOLS.json +7 -0
  188. package/src/config/bundled-skills/computer-use/TOOLS.json +15 -52
  189. package/src/config/bundled-skills/document/SKILL.md +23 -3
  190. package/src/config/bundled-skills/document/TOOLS.json +53 -0
  191. package/src/config/bundled-skills/document/tools/document-delete.ts +12 -0
  192. package/src/config/bundled-skills/document/tools/document-list.ts +12 -0
  193. package/src/config/bundled-skills/document/tools/document-read.ts +12 -0
  194. package/src/config/bundled-skills/skill-management/SKILL.md +2 -2
  195. package/src/config/bundled-skills/skill-management/TOOLS.json +7 -7
  196. package/src/config/bundled-tool-registry.ts +6 -0
  197. package/src/config/feature-flag-registry.json +41 -1
  198. package/src/config/loader.ts +64 -38
  199. package/src/config/schema.ts +7 -10
  200. package/src/config/schemas/__tests__/llm-request-logs.test.ts +36 -0
  201. package/src/config/schemas/channels.ts +8 -0
  202. package/src/config/schemas/compaction.ts +28 -0
  203. package/src/config/schemas/heartbeat.ts +9 -0
  204. package/src/config/schemas/llm-request-logs.ts +31 -7
  205. package/src/config/schemas/llm.ts +3 -0
  206. package/src/config/schemas/memory-retrieval.ts +18 -0
  207. package/src/config/schemas/tools.ts +14 -0
  208. package/src/config/skills.ts +3 -96
  209. package/src/context/compactor.ts +1047 -0
  210. package/src/context/token-estimator.ts +2 -2
  211. package/src/context/window-manager.ts +197 -1520
  212. package/src/credential-execution/managed-catalog.ts +37 -0
  213. package/src/credential-health/credential-health-service.ts +280 -19
  214. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +34 -0
  215. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +138 -0
  216. package/src/daemon/__tests__/conversation-tool-setup.test.ts +74 -0
  217. package/src/daemon/approval-generators.ts +8 -6
  218. package/src/daemon/config-watcher.ts +94 -31
  219. package/src/daemon/conversation-agent-loop.ts +169 -9
  220. package/src/daemon/conversation-error.ts +171 -37
  221. package/src/daemon/conversation-lifecycle.ts +53 -40
  222. package/src/daemon/conversation-messaging.ts +25 -6
  223. package/src/daemon/conversation-process.ts +49 -12
  224. package/src/daemon/conversation-runtime-assembly.ts +16 -1
  225. package/src/daemon/conversation-slash.ts +12 -5
  226. package/src/daemon/conversation-store.ts +11 -4
  227. package/src/daemon/conversation-tool-setup.ts +39 -7
  228. package/src/daemon/conversation.ts +33 -1
  229. package/src/daemon/external-plugins-bootstrap.ts +217 -181
  230. package/src/daemon/first-greeting.ts +22 -2
  231. package/src/daemon/handlers/config-model.ts +6 -5
  232. package/src/daemon/handlers/config-slack-channel.ts +15 -3
  233. package/src/daemon/handlers/shared.ts +14 -5
  234. package/src/daemon/handlers/skills.ts +111 -108
  235. package/src/daemon/history-repair.ts +28 -1
  236. package/src/daemon/host-app-control-proxy.ts +98 -23
  237. package/src/daemon/lifecycle.ts +45 -35
  238. package/src/daemon/meet-host-supervisor.ts +5 -4
  239. package/src/daemon/memory-v2-startup.ts +49 -0
  240. package/src/daemon/message-protocol.ts +1 -0
  241. package/src/daemon/message-types/conversations.ts +25 -0
  242. package/src/daemon/message-types/messages.ts +61 -0
  243. package/src/daemon/message-types/subagents.ts +1 -0
  244. package/src/daemon/message-types/sync.ts +1 -0
  245. package/src/daemon/pkb-reminder-builder.test.ts +1 -1
  246. package/src/daemon/pkb-reminder-builder.ts +1 -1
  247. package/src/daemon/plugin-source-watcher.ts +146 -0
  248. package/src/daemon/process-message.ts +21 -3
  249. package/src/daemon/server.ts +11 -2
  250. package/src/daemon/skill-memory-refresh.ts +29 -0
  251. package/src/documents/document-store.ts +221 -3
  252. package/src/embedded/plugin-api.ts +40 -0
  253. package/src/filing/filing-service.ts +39 -0
  254. package/src/heartbeat/__tests__/heartbeat-service.test.ts +91 -6
  255. package/src/heartbeat/heartbeat-run-store.ts +2 -1
  256. package/src/heartbeat/heartbeat-service.ts +41 -0
  257. package/src/home/__tests__/feed-types.test.ts +40 -0
  258. package/src/home/feed-types.ts +22 -0
  259. package/src/home/post-connect-feed.ts +1 -0
  260. package/src/index.ts +18 -1
  261. package/src/live-voice/__tests__/live-voice-stt.test.ts +57 -0
  262. package/src/mcp/client.ts +20 -4
  263. package/src/media/image-credentials.ts +3 -3
  264. package/src/memory/__tests__/bookmark-crud.test.ts +33 -27
  265. package/src/memory/__tests__/conversation-queries.test.ts +263 -0
  266. package/src/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +113 -0
  267. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +119 -14
  268. package/src/memory/__tests__/message-content.test.ts +35 -0
  269. package/src/memory/bookmark-crud.ts +42 -10
  270. package/src/memory/context-search/sources/conversations.ts +62 -2
  271. package/src/memory/context-search/sources/workspace.ts +4 -0
  272. package/src/memory/conversation-crud.ts +63 -19
  273. package/src/memory/conversation-queries.ts +110 -10
  274. package/src/memory/db-init.ts +6 -0
  275. package/src/memory/delivery-crud.ts +152 -5
  276. package/src/memory/embedding-backend.ts +4 -4
  277. package/src/memory/external-conversation-store.ts +66 -5
  278. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +66 -9
  279. package/src/memory/graph/conversation-graph-memory.ts +31 -15
  280. package/src/memory/graph/tools.ts +3 -3
  281. package/src/memory/indexer.ts +34 -29
  282. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +73 -0
  283. package/src/memory/jobs/embed-concept-page.ts +20 -11
  284. package/src/memory/jobs-worker.ts +6 -1
  285. package/src/memory/llm-request-log-source-clickhouse.ts +17 -10
  286. package/src/memory/llm-request-log-source.ts +19 -52
  287. package/src/memory/llm-usage-store.ts +125 -5
  288. package/src/memory/memory-retrospective-startup-cleanup.ts +72 -5
  289. package/src/memory/message-content.ts +1 -1
  290. package/src/memory/migrations/109-external-conversation-bindings.ts +15 -4
  291. package/src/memory/migrations/229-delete-private-conversations.test.ts +38 -1
  292. package/src/memory/migrations/229-delete-private-conversations.ts +7 -0
  293. package/src/memory/migrations/247-external-conversation-binding-thread-id.ts +78 -0
  294. package/src/memory/migrations/248-create-onboarding-events.ts +21 -0
  295. package/src/memory/migrations/249-normalize-slack-external-content.ts +240 -0
  296. package/src/memory/migrations/index.ts +6 -0
  297. package/src/memory/migrations/registry.ts +8 -0
  298. package/src/memory/onboarding-events-store.ts +106 -0
  299. package/src/memory/schema/bookmarks.ts +0 -2
  300. package/src/memory/schema/calls.ts +1 -0
  301. package/src/memory/schema/inference.ts +1 -3
  302. package/src/memory/schema/infrastructure.ts +12 -0
  303. package/src/memory/turn-events-store.ts +127 -2
  304. package/src/memory/v2/__tests__/activation.test.ts +0 -8
  305. package/src/memory/v2/__tests__/injection.test.ts +98 -8
  306. package/src/memory/v2/__tests__/migration.test.ts +87 -0
  307. package/src/memory/v2/__tests__/page-index.test.ts +83 -0
  308. package/src/memory/v2/__tests__/prompts-router.test.ts +58 -6
  309. package/src/memory/v2/__tests__/qdrant.test.ts +66 -3
  310. package/src/memory/v2/__tests__/router.test.ts +15 -0
  311. package/src/memory/v2/__tests__/skill-store.test.ts +387 -8
  312. package/src/memory/v2/injection.ts +32 -6
  313. package/src/memory/v2/migration.ts +49 -19
  314. package/src/memory/v2/page-index.ts +35 -5
  315. package/src/memory/v2/prompts/router.ts +11 -8
  316. package/src/memory/v2/prompts/sweep.ts +2 -2
  317. package/src/memory/v2/qdrant.ts +135 -7
  318. package/src/memory/v2/router.ts +9 -8
  319. package/src/memory/v2/skill-store.ts +120 -35
  320. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +45 -5
  321. package/src/messaging/providers/slack/__tests__/download.test.ts +231 -0
  322. package/src/messaging/providers/slack/adapter.ts +43 -5
  323. package/src/messaging/providers/slack/client.ts +27 -0
  324. package/src/messaging/providers/slack/deep-link.ts +65 -0
  325. package/src/messaging/providers/slack/download.ts +104 -0
  326. package/src/messaging/providers/slack/message-metadata.test.ts +32 -0
  327. package/src/messaging/providers/slack/message-metadata.ts +27 -0
  328. package/src/messaging/providers/slack/render-transcript.test.ts +134 -0
  329. package/src/messaging/providers/slack/render-transcript.ts +69 -5
  330. package/src/messaging/providers/slack/types.ts +20 -1
  331. package/src/notifications/conversation-pairing.ts +2 -1
  332. package/src/notifications/decision-engine.ts +2 -1
  333. package/src/notifications/emit-signal.ts +20 -1
  334. package/src/notifications/home-feed-side-effect.ts +54 -0
  335. package/src/notifications/signal.ts +3 -1
  336. package/src/oauth/connection-resolver.ts +8 -4
  337. package/src/oauth/platform-connection.ts +6 -2
  338. package/src/oauth/seed-providers.ts +10 -1
  339. package/src/permissions/checker.ts +2 -0
  340. package/src/permissions/ipc-risk-types.ts +1 -0
  341. package/src/permissions/question-prompter.test.ts +416 -0
  342. package/src/permissions/question-prompter.ts +294 -0
  343. package/src/platform/client.test.ts +1 -1
  344. package/src/platform/client.ts +1 -1
  345. package/src/plugin-api/constants.ts +26 -0
  346. package/src/plugin-api/index.ts +34 -1
  347. package/src/plugin-api/types.ts +104 -22
  348. package/src/plugins/defaults/circuit-breaker.ts +0 -5
  349. package/src/plugins/defaults/compaction.ts +0 -4
  350. package/src/plugins/defaults/empty-response.ts +0 -2
  351. package/src/plugins/defaults/history-repair.ts +0 -2
  352. package/src/plugins/defaults/injectors.ts +36 -3
  353. package/src/plugins/defaults/llm-call.ts +0 -2
  354. package/src/plugins/defaults/memory-retrieval.ts +0 -1
  355. package/src/plugins/defaults/overflow-reduce.ts +0 -1
  356. package/src/plugins/defaults/persistence.ts +0 -2
  357. package/src/plugins/defaults/title-generate.ts +0 -5
  358. package/src/plugins/defaults/token-estimate.ts +0 -2
  359. package/src/plugins/defaults/tool-error.ts +0 -7
  360. package/src/plugins/defaults/tool-execute.ts +0 -2
  361. package/src/plugins/defaults/tool-result-truncate.ts +0 -4
  362. package/src/plugins/ensure-plugin-api-shim.ts +96 -0
  363. package/src/plugins/external-api.ts +104 -0
  364. package/src/plugins/external-plugin-loader.ts +105 -32
  365. package/src/plugins/feature-gate.ts +22 -0
  366. package/src/plugins/pipeline.ts +37 -0
  367. package/src/plugins/registry.ts +48 -80
  368. package/src/plugins/types.ts +31 -26
  369. package/src/plugins/user-loader.ts +21 -2
  370. package/src/proactive-artifact/aux-message-injector.ts +11 -0
  371. package/src/proactive-artifact/job.test.ts +37 -5
  372. package/src/prompts/__tests__/system-prompt.test.ts +12 -0
  373. package/src/prompts/__tests__/task-progress-hint-section.test.ts +99 -0
  374. package/src/prompts/normalize-onboarding.ts +27 -0
  375. package/src/prompts/sections.ts +302 -0
  376. package/src/prompts/system-prompt.ts +63 -166
  377. package/src/prompts/templates/BOOTSTRAP.md +17 -1
  378. package/src/prompts/templates/system-sections.ts +173 -0
  379. package/src/providers/__tests__/inference.test.ts +22 -7
  380. package/src/providers/anthropic/client.ts +28 -28
  381. package/src/providers/connection-resolution.ts +7 -0
  382. package/src/providers/inference/adapter-factory.ts +41 -4
  383. package/src/providers/inference/connections.ts +74 -29
  384. package/src/providers/inference/resolve-auth.ts +12 -4
  385. package/src/providers/model-catalog.ts +294 -12
  386. package/src/providers/openai/chat-completions-provider.ts +10 -2
  387. package/src/providers/openrouter/client.ts +7 -0
  388. package/src/providers/{managed-proxy → platform-proxy}/constants.ts +4 -1
  389. package/src/providers/{managed-proxy → platform-proxy}/context.ts +3 -3
  390. package/src/providers/provider-availability.ts +17 -2
  391. package/src/providers/provider-catalog-visibility.ts +36 -0
  392. package/src/providers/registry.ts +22 -14
  393. package/src/providers/retry.ts +47 -1
  394. package/src/runtime/__tests__/agent-wake.test.ts +152 -0
  395. package/src/runtime/agent-wake.ts +42 -14
  396. package/src/runtime/auth/route-policy.ts +8 -1
  397. package/src/runtime/btw-sidechain.ts +2 -0
  398. package/src/runtime/http-types.ts +19 -0
  399. package/src/runtime/migrations/origin-mode.ts +1 -1
  400. package/src/runtime/pending-interactions.ts +1 -0
  401. package/src/runtime/routes/__tests__/bookmark-routes.test.ts +17 -0
  402. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +5 -1
  403. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +107 -20
  404. package/src/runtime/routes/__tests__/question-routes.test.ts +395 -0
  405. package/src/runtime/routes/__tests__/tts-routes.test.ts +64 -1
  406. package/src/runtime/routes/acp-routes-list.test.ts +143 -0
  407. package/src/runtime/routes/acp-routes.ts +5 -3
  408. package/src/runtime/routes/auth-routes.ts +1 -1
  409. package/src/runtime/routes/bookmark-routes.ts +5 -3
  410. package/src/runtime/routes/btw-routes.ts +5 -1
  411. package/src/runtime/routes/channel-availability-routes.ts +121 -0
  412. package/src/runtime/routes/conversation-cli-routes.ts +44 -3
  413. package/src/runtime/routes/conversation-list-routes.ts +3 -20
  414. package/src/runtime/routes/conversation-management-routes.ts +17 -42
  415. package/src/runtime/routes/conversation-query-routes.ts +40 -35
  416. package/src/runtime/routes/conversation-routes.ts +90 -11
  417. package/src/runtime/routes/documents-routes.ts +25 -86
  418. package/src/runtime/routes/group-routes.ts +5 -0
  419. package/src/runtime/routes/inbound-conversation.ts +28 -8
  420. package/src/runtime/routes/inbound-message-handler.ts +236 -41
  421. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +111 -0
  422. package/src/runtime/routes/inbound-stages/background-dispatch.ts +32 -1
  423. package/src/runtime/routes/inbound-stages/edit-intercept.ts +17 -4
  424. package/src/runtime/routes/index.ts +6 -0
  425. package/src/runtime/routes/inference-profile-session-handler.ts +17 -44
  426. package/src/runtime/routes/inference-profile-session-reaper.ts +7 -21
  427. package/src/runtime/routes/inference-provider-connection-routes.ts +65 -21
  428. package/src/runtime/routes/integrations/slack/share.ts +4 -52
  429. package/src/runtime/routes/integrations/slack/token.ts +43 -0
  430. package/src/runtime/routes/integrations/twilio.ts +6 -13
  431. package/src/runtime/routes/notification-routes.ts +1 -1
  432. package/src/runtime/routes/oauth-commands-routes.ts +105 -15
  433. package/src/runtime/routes/oauth-lifecycle-routes.ts +43 -0
  434. package/src/runtime/routes/question-routes.ts +259 -0
  435. package/src/runtime/routes/rename-conversation-routes.ts +2 -33
  436. package/src/runtime/routes/schedule-routes.ts +4 -7
  437. package/src/runtime/routes/subagents-routes.ts +57 -18
  438. package/src/runtime/routes/telemetry-routes.ts +27 -0
  439. package/src/runtime/routes/tts-routes.ts +27 -2
  440. package/src/runtime/routes/workspace-routes.test.ts +43 -0
  441. package/src/runtime/routes/workspace-routes.ts +28 -0
  442. package/src/runtime/services/conversation-serializer.ts +39 -7
  443. package/src/runtime/sync/resource-sync-events.ts +93 -1
  444. package/src/schedule/schedule-store.ts +27 -2
  445. package/src/schedule/scheduler.ts +9 -1
  446. package/src/security/__tests__/untrusted-content.test.ts +86 -0
  447. package/src/security/untrusted-content.ts +93 -8
  448. package/src/skills/catalog-files.ts +1 -1
  449. package/src/skills/catalog-install.ts +233 -116
  450. package/src/skills/clawhub.ts +70 -13
  451. package/src/skills/managed-store.ts +4 -119
  452. package/src/skills/skillssh-registry.ts +27 -48
  453. package/src/subagent/manager.ts +15 -7
  454. package/src/telemetry/types.ts +113 -1
  455. package/src/telemetry/usage-telemetry-reporter.test.ts +312 -5
  456. package/src/telemetry/usage-telemetry-reporter.ts +113 -7
  457. package/src/tools/apps/executors.ts +58 -7
  458. package/src/tools/ask-question/ask-question-tool.test.ts +509 -0
  459. package/src/tools/ask-question/ask-question-tool.ts +304 -0
  460. package/src/tools/browser/browser-execution.ts +15 -11
  461. package/src/tools/computer-use/definitions.ts +3 -3
  462. package/src/tools/credentials/vault.ts +1 -1
  463. package/src/tools/document/document-tool.ts +124 -1
  464. package/src/tools/filesystem/edit.ts +1 -1
  465. package/src/tools/filesystem/list.ts +1 -1
  466. package/src/tools/filesystem/read.ts +1 -1
  467. package/src/tools/filesystem/write.ts +5 -2
  468. package/src/tools/host-filesystem/transfer.ts +1 -1
  469. package/src/tools/host-terminal/host-shell.ts +1 -1
  470. package/src/tools/permission-checker.ts +1 -1
  471. package/src/tools/registry.ts +17 -7
  472. package/src/tools/schedule/create.ts +2 -2
  473. package/src/tools/schema-transforms.ts +7 -2
  474. package/src/tools/side-effects.ts +1 -0
  475. package/src/tools/skills/delete-managed.ts +4 -4
  476. package/src/tools/skills/execute.ts +1 -1
  477. package/src/tools/skills/scaffold-managed.ts +3 -2
  478. package/src/tools/subagent/notify-parent.ts +1 -1
  479. package/src/tools/system/request-permission.ts +2 -2
  480. package/src/tools/terminal/safe-env.ts +60 -1
  481. package/src/tools/tool-manifest.ts +2 -0
  482. package/src/tools/types.ts +72 -21
  483. package/src/tools/ui-surface/definitions.ts +6 -5
  484. package/src/tts/__tests__/provider-adapters.test.ts +76 -2
  485. package/src/tts/providers/elevenlabs-provider.ts +75 -1
  486. package/src/types/onboarding-context.ts +2 -0
  487. package/src/util/errors.ts +17 -0
  488. package/src/util/platform.ts +10 -0
  489. package/src/watcher/__tests__/engine.test.ts +22 -0
  490. package/src/watcher/engine.ts +6 -2
  491. package/src/workspace/migrations/057-repair-stale-gemini-model-ids.ts +80 -15
  492. package/src/workspace/migrations/072-seed-reply-suggestion-callsite.ts +35 -22
  493. package/src/workspace/migrations/073-repair-recall-callsite-empty-profile.ts +3 -1
  494. package/src/workspace/migrations/083-system-prompt-prefix-to-file.ts +191 -0
  495. package/src/workspace/migrations/084-remove-legacy-skills-index.ts +276 -0
  496. package/src/workspace/migrations/085-memory-v2-bm25-b-reembed-disabled-v2-pages.ts +137 -0
  497. package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +198 -0
  498. package/src/workspace/migrations/registry.ts +8 -0
  499. package/src/workspace/migrations/runner.ts +39 -9
  500. package/src/workspace/migrations/types.ts +4 -0
  501. package/examples/plugins/echo/bun.lock +0 -25
  502. package/src/__tests__/context-window-manager.test.ts +0 -2481
  503. package/src/context/__tests__/compact-prompt.test.ts +0 -63
  504. package/src/context/prompts/compact.md +0 -26
  505. package/src/prompts/__tests__/build-cli-reference-section.test.ts +0 -37
  506. /package/src/__tests__/{secret-routes-managed-proxy.test.ts → secret-routes-platform-proxy.test.ts} +0 -0
@@ -3,18 +3,13 @@
3
3
  * `assistant/src/daemon/handlers/skills.ts`.
4
4
  *
5
5
  * One representative call site (the `installSkill` bundled branch) is
6
- * exercised all 5 sites share the same delegation to
7
- * `maybeSeedMemoryV2Skills`, so a single suite covers behavior. Validates:
8
- * - config on helper invoked after seedSkillGraphNodes and the seed
9
- * observed (callOrder picks up "v2")
10
- * - config off → helper still invoked, but the seed short-circuits
6
+ * exercised; all handler seed sites share the same delegation to
7
+ * `refreshSkillCapabilityMemories`, so a single suite covers behavior. Validates:
8
+ * - handler invokes the centralized refresh helper with the live config.
11
9
  *
12
- * The handler delegates to `maybeSeedMemoryV2Skills` from
13
- * `daemon/memory-v2-startup.ts`. We mock that module directly so the test
14
- * does not have to drain the dynamic-import microtask chain. The helper's
15
- * gate semantics are covered by `lifecycle-memory-v2-seed.test.ts`; here
16
- * we only verify that the handler invokes the helper synchronously with
17
- * the live config.
10
+ * The helper's gate semantics (flag + config + rejection swallowing) are
11
+ * covered by `lifecycle-memory-v2-seed.test.ts`; here we only verify that the
12
+ * handler delegates to the centralized refresh path synchronously.
18
13
  */
19
14
  import { beforeEach, describe, expect, mock, test } from "bun:test";
20
15
 
@@ -22,16 +17,9 @@ import { beforeEach, describe, expect, mock, test } from "bun:test";
22
17
  // Programmable test state
23
18
  // ---------------------------------------------------------------------------
24
19
 
25
- const flagsState = { configV2Enabled: true };
20
+ const configState = { v2Enabled: true };
26
21
 
27
- const callOrder: string[] = [];
28
-
29
- const mockSeedSkillGraphNodes = mock(() => {
30
- callOrder.push("v1");
31
- });
32
- // Body installed in `beforeEach` so each test sees a fresh implementation
33
- // that closes over the up-to-date `flagsState`.
34
- const mockMaybeSeedMemoryV2Skills = mock(
22
+ const mockRefreshSkillCapabilityMemories = mock(
35
23
  (_config: { memory: { v2: { enabled: boolean } } }) => {},
36
24
  );
37
25
 
@@ -53,10 +41,6 @@ mock.module("../config/skills.js", () => ({
53
41
  ],
54
42
  }));
55
43
 
56
- mock.module("../config/assistant-feature-flags.js", () => ({
57
- isAssistantFeatureFlagEnabled: () => true,
58
- }));
59
-
60
44
  // Stub both `getConfig` and `loadConfig`. `loadConfig` is reached by code
61
45
  // paths transitively imported during teardown (e.g. dynamic imports inside
62
46
  // `oauth2.ts`); leaving it undefined here would break sibling test files
@@ -68,13 +52,13 @@ mock.module("../config/loader.js", () => ({
68
52
  deepMergeOverwrite: (a: unknown) => a,
69
53
  mergeDefaultWorkspaceConfig: () => {},
70
54
  getConfig: () => ({
71
- memory: { v2: { enabled: flagsState.configV2Enabled } },
55
+ memory: { v2: { enabled: configState.v2Enabled } },
72
56
  }),
73
57
  getConfigReadOnly: () => ({
74
- memory: { v2: { enabled: flagsState.configV2Enabled } },
58
+ memory: { v2: { enabled: configState.v2Enabled } },
75
59
  }),
76
60
  loadConfig: () => ({
77
- memory: { v2: { enabled: flagsState.configV2Enabled } },
61
+ memory: { v2: { enabled: configState.v2Enabled } },
78
62
  }),
79
63
  invalidateConfigCache: () => {},
80
64
  loadRawConfig: () => ({}),
@@ -147,9 +131,11 @@ mock.module("../skills/catalog-cache.js", () => ({
147
131
  }));
148
132
 
149
133
  mock.module("../skills/catalog-install.js", () => ({
150
- installSkillLocally: async () => {},
151
- upsertSkillsIndex: () => {},
134
+ commitStagedSkillInstall: () => {},
135
+ createSkillInstallStagingDir: () => "/tmp/test-skills/.install-staging/test",
152
136
  getRepoSkillsDir: () => undefined,
137
+ installSkillDependenciesIfPresent: () => {},
138
+ installSkillLocally: async () => {},
153
139
  }));
154
140
 
155
141
  mock.module("../skills/catalog-search.js", () => ({
@@ -159,23 +145,15 @@ mock.module("../skills/catalog-search.js", () => ({
159
145
  mock.module("../skills/managed-store.js", () => ({
160
146
  createManagedSkill: () => ({ created: true }),
161
147
  deleteManagedSkill: () => ({ deleted: true }),
162
- removeSkillsIndexEntry: () => {},
163
148
  validateManagedSkillId: () => null,
164
149
  }));
165
150
 
166
151
  mock.module("../memory/graph/capability-seed.js", () => ({
167
152
  deleteSkillCapabilityNode: () => {},
168
- seedSkillGraphNodes: mockSeedSkillGraphNodes,
169
- seedUninstalledCatalogSkillMemories: async () => {},
170
- }));
171
-
172
- mock.module("../memory/v2/skill-store.js", () => ({
173
- seedV2SkillEntries: mock(async () => {}),
174
- getSkillCapability: () => null,
175
153
  }));
176
154
 
177
- mock.module("../daemon/memory-v2-startup.js", () => ({
178
- maybeSeedMemoryV2Skills: mockMaybeSeedMemoryV2Skills,
155
+ mock.module("../daemon/skill-memory-refresh.js", () => ({
156
+ refreshSkillCapabilityMemories: mockRefreshSkillCapabilityMemories,
179
157
  }));
180
158
 
181
159
  mock.module("../util/platform.js", () => ({
@@ -204,45 +182,48 @@ mock.module("../daemon/config-watcher.js", () => ({
204
182
  }));
205
183
 
206
184
  // Import after mocking
207
- const { installSkill } = await import("../daemon/handlers/skills.js");
208
-
209
- // ---------------------------------------------------------------------------
210
- // Helpers
211
- // ---------------------------------------------------------------------------
185
+ const { installSkill, uninstallSkill } =
186
+ await import("../daemon/handlers/skills.js");
212
187
 
213
188
  // ---------------------------------------------------------------------------
214
189
  // Tests
215
190
  // ---------------------------------------------------------------------------
216
191
 
217
- describe("v2 skill re-seed gating in skill handlers", () => {
192
+ describe("v2 skill refresh delegation in skill handlers", () => {
218
193
  beforeEach(() => {
219
- flagsState.configV2Enabled = true;
220
- callOrder.length = 0;
221
- mockSeedSkillGraphNodes.mockClear();
222
- mockMaybeSeedMemoryV2Skills.mockClear();
223
- mockMaybeSeedMemoryV2Skills.mockImplementation((config) => {
224
- if (!config.memory.v2.enabled) return;
225
- callOrder.push("v2");
226
- });
194
+ configState.v2Enabled = true;
195
+ mockRefreshSkillCapabilityMemories.mockClear();
227
196
  });
228
197
 
229
- test("config on maybeSeedMemoryV2Skills invoked after seedSkillGraphNodes", async () => {
198
+ test("enabled config → refresh helper invoked with live config", async () => {
230
199
  const result = await installSkill({ slug: "bundled-skill" });
231
200
 
232
201
  expect(result.success).toBe(true);
233
- expect(mockSeedSkillGraphNodes).toHaveBeenCalledTimes(1);
234
- expect(mockMaybeSeedMemoryV2Skills).toHaveBeenCalledTimes(1);
235
- expect(callOrder).toEqual(["v1", "v2"]);
202
+ expect(mockRefreshSkillCapabilityMemories).toHaveBeenCalledTimes(1);
203
+ expect(mockRefreshSkillCapabilityMemories.mock.calls[0]?.[0]).toEqual({
204
+ memory: { v2: { enabled: true } },
205
+ });
236
206
  });
237
207
 
238
- test("config.memory.v2.enabled off → seed mock observes config and skips", async () => {
239
- flagsState.configV2Enabled = false;
208
+ test("config.memory.v2.enabled off → helper receives disabled config", async () => {
209
+ configState.v2Enabled = false;
240
210
 
241
211
  const result = await installSkill({ slug: "bundled-skill" });
242
212
 
243
213
  expect(result.success).toBe(true);
244
- expect(mockSeedSkillGraphNodes).toHaveBeenCalledTimes(1);
245
- expect(mockMaybeSeedMemoryV2Skills).toHaveBeenCalledTimes(1);
246
- expect(callOrder).toEqual(["v1"]);
214
+ expect(mockRefreshSkillCapabilityMemories).toHaveBeenCalledTimes(1);
215
+ expect(mockRefreshSkillCapabilityMemories.mock.calls[0]?.[0]).toEqual({
216
+ memory: { v2: { enabled: false } },
217
+ });
218
+ });
219
+
220
+ test("uninstall delegates to refresh helper", async () => {
221
+ const result = await uninstallSkill("managed-skill");
222
+
223
+ expect(result.success).toBe(true);
224
+ expect(mockRefreshSkillCapabilityMemories).toHaveBeenCalledTimes(1);
225
+ expect(mockRefreshSkillCapabilityMemories.mock.calls[0]?.[0]).toEqual({
226
+ memory: { v2: { enabled: true } },
227
+ });
247
228
  });
248
229
  });
@@ -0,0 +1,39 @@
1
+ export function makeTarEntry(name: string, content: string): Buffer {
2
+ const header = Buffer.alloc(512, 0);
3
+ const nameBuffer = Buffer.from(name, "utf-8");
4
+ nameBuffer.copy(header, 0, 0, Math.min(nameBuffer.length, 100));
5
+
6
+ Buffer.from("0000644\0", "ascii").copy(header, 100);
7
+ Buffer.from("0000000\0", "ascii").copy(header, 108);
8
+ Buffer.from("0000000\0", "ascii").copy(header, 116);
9
+ Buffer.from(
10
+ `${content.length.toString(8).padStart(11, "0")}\0`,
11
+ "ascii",
12
+ ).copy(header, 124);
13
+ Buffer.from("00000000000\0", "ascii").copy(header, 136);
14
+ Buffer.from(" ", "ascii").copy(header, 148);
15
+ header[156] = "0".charCodeAt(0);
16
+ Buffer.from("ustar\0", "ascii").copy(header, 257);
17
+ Buffer.from("00", "ascii").copy(header, 263);
18
+
19
+ let sum = 0;
20
+ for (let i = 0; i < 512; i += 1) sum += header[i] ?? 0;
21
+ Buffer.from(`${sum.toString(8).padStart(6, "0")}\0 `, "ascii").copy(
22
+ header,
23
+ 148,
24
+ );
25
+
26
+ const data = Buffer.from(content, "utf-8");
27
+ const padded = Buffer.alloc(Math.ceil(data.length / 512) * 512, 0);
28
+ data.copy(padded);
29
+ return Buffer.concat([header, padded]);
30
+ }
31
+
32
+ export function makeTar(
33
+ entries: Array<{ name: string; content: string }>,
34
+ ): Buffer {
35
+ return Buffer.concat([
36
+ ...entries.map((entry) => makeTarEntry(entry.name, entry.content)),
37
+ Buffer.alloc(1024, 0),
38
+ ]);
39
+ }
@@ -0,0 +1,21 @@
1
+ export async function waitFor(
2
+ predicate: () => boolean | Promise<boolean>,
3
+ options: {
4
+ timeoutMs?: number;
5
+ intervalMs?: number;
6
+ message?: string;
7
+ } = {},
8
+ ): Promise<void> {
9
+ const timeoutMs = options.timeoutMs ?? 500;
10
+ const intervalMs = options.intervalMs ?? 5;
11
+ const deadline = Date.now() + timeoutMs;
12
+
13
+ while (Date.now() < deadline) {
14
+ if (await predicate()) {
15
+ return;
16
+ }
17
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
18
+ }
19
+
20
+ throw new Error(options.message ?? "Timed out waiting for test condition");
21
+ }
@@ -248,7 +248,6 @@ describe("historyRepair pipeline — end-to-end via runPipeline", () => {
248
248
  manifest: {
249
249
  name: "observer-plugin",
250
250
  version: "0.0.1",
251
- requires: { pluginRuntime: "v1", historyRepairApi: "v1" },
252
251
  },
253
252
  middleware: { historyRepair: observer },
254
253
  });
@@ -297,7 +296,6 @@ describe("historyRepair pipeline — end-to-end via runPipeline", () => {
297
296
  manifest: {
298
297
  name: "override-plugin",
299
298
  version: "0.0.1",
300
- requires: { pluginRuntime: "v1", historyRepairApi: "v1" },
301
299
  },
302
300
  middleware: { historyRepair: shortCircuit },
303
301
  });
@@ -342,7 +340,6 @@ describe("historyRepair pipeline — end-to-end via runPipeline", () => {
342
340
  manifest: {
343
341
  name: "late-user-plugin",
344
342
  version: "0.0.1",
345
- requires: { pluginRuntime: "v1", historyRepairApi: "v1" },
346
343
  },
347
344
  middleware: { historyRepair: userMiddleware },
348
345
  });
@@ -766,6 +766,79 @@ describe("repairHistory", () => {
766
766
  (b as { text: string }).text.includes("srvtoolu_orphan"),
767
767
  );
768
768
  expect(downgraded).toBeDefined();
769
+ // Titles/URLs from the original results must survive the downgrade so
770
+ // the model can still reason about what was searched.
771
+ const text = (downgraded as { text: string }).text;
772
+ expect(text).toContain("Example");
773
+ expect(text).toContain("https://example.com");
774
+ });
775
+
776
+ test("preserves all titles/URLs when downgrading multi-result orphan", () => {
777
+ const messages: Message[] = [
778
+ { role: "user", content: [{ type: "text", text: "search" }] },
779
+ {
780
+ role: "assistant",
781
+ content: [
782
+ {
783
+ type: "web_search_tool_result",
784
+ tool_use_id: "srvtoolu_multi",
785
+ content: [
786
+ {
787
+ type: "web_search_result",
788
+ url: "https://alpha.test",
789
+ title: "Alpha",
790
+ encrypted_content: "enc_a",
791
+ },
792
+ {
793
+ type: "web_search_result",
794
+ url: "https://beta.test",
795
+ title: "Beta",
796
+ encrypted_content: "enc_b",
797
+ },
798
+ ],
799
+ },
800
+ ],
801
+ },
802
+ ];
803
+
804
+ const { messages: repaired } = repairHistory(messages);
805
+ const downgraded = repaired[1].content.find((b) => b.type === "text") as
806
+ | { text: string }
807
+ | undefined;
808
+ expect(downgraded).toBeDefined();
809
+ expect(downgraded!.text).toContain("Alpha");
810
+ expect(downgraded!.text).toContain("https://alpha.test");
811
+ expect(downgraded!.text).toContain("Beta");
812
+ expect(downgraded!.text).toContain("https://beta.test");
813
+ // Must NOT emit the legacy fixed placeholder.
814
+ expect(downgraded!.text).not.toContain("[web search result]");
815
+ });
816
+
817
+ test("downgrades error-envelope web_search orphan to a stable marker", () => {
818
+ const messages: Message[] = [
819
+ { role: "user", content: [{ type: "text", text: "search" }] },
820
+ {
821
+ role: "assistant",
822
+ content: [
823
+ {
824
+ type: "web_search_tool_result",
825
+ tool_use_id: "srvtoolu_err",
826
+ content: {
827
+ type: "web_search_tool_result_error",
828
+ error_code: "unavailable",
829
+ },
830
+ },
831
+ ],
832
+ },
833
+ ];
834
+
835
+ const { messages: repaired } = repairHistory(messages);
836
+ const downgraded = repaired[1].content.find((b) => b.type === "text") as
837
+ | { text: string }
838
+ | undefined;
839
+ expect(downgraded).toBeDefined();
840
+ expect(downgraded!.text).toContain("srvtoolu_err");
841
+ expect(downgraded!.text).toContain("results unavailable");
769
842
  });
770
843
 
771
844
  test("repairs both orphan directions within the same assistant message", () => {
@@ -27,10 +27,8 @@ interface RegisteredInteraction {
27
27
  const registeredInteractions: RegisteredInteraction[] = [];
28
28
 
29
29
  mock.module("../runtime/pending-interactions.js", () => ({
30
- register: (
31
- _requestId: string,
32
- entry: RegisteredInteraction,
33
- ) => registeredInteractions.push(entry),
30
+ register: (_requestId: string, entry: RegisteredInteraction) =>
31
+ registeredInteractions.push(entry),
34
32
  resolve: (requestId: string) => {
35
33
  resolvedInteractionIds.push(requestId);
36
34
  return undefined;
@@ -594,6 +592,264 @@ describe("HostAppControlProxy", () => {
594
592
  });
595
593
  });
596
594
 
595
+ // -------------------------------------------------------------------------
596
+ // (c.1) Failed re-start restores the prior session
597
+ // -------------------------------------------------------------------------
598
+
599
+ describe("failed re-start restores prior session", () => {
600
+ test("non-running re-start in the same conversation restores the prior session", async () => {
601
+ const proxy = new HostAppControlProxy("conv-1");
602
+ const ctrl = new AbortController();
603
+
604
+ // Establish an active session targeting the editor.
605
+ const p1 = proxy.request(
606
+ "app_control_start",
607
+ { tool: "start", app: "com.example.editor" },
608
+ "conv-1",
609
+ ctrl.signal,
610
+ );
611
+ proxy.resolve(
612
+ (sentMessages[0] as Record<string, unknown>).requestId as string,
613
+ payload({ pngBase64: PNG_A }),
614
+ );
615
+ await p1;
616
+ expect(_getActiveAppControlSession()?.app).toBe("com.example.editor");
617
+
618
+ // Re-start against a different app — host returns "missing".
619
+ sentMessages.length = 0;
620
+ const p2 = proxy.request(
621
+ "app_control_start",
622
+ { tool: "start", app: "com.example.other" },
623
+ "conv-1",
624
+ ctrl.signal,
625
+ );
626
+ proxy.resolve(
627
+ (sentMessages[0] as Record<string, unknown>).requestId as string,
628
+ payload({ state: "missing" }),
629
+ );
630
+ await p2;
631
+
632
+ // Prior session restored (editor) — not stranded as undefined and not
633
+ // overwritten with the failed re-start target.
634
+ const session = _getActiveAppControlSession();
635
+ expect(session?.conversationId).toBe("conv-1");
636
+ expect(session?.app).toBe("com.example.editor");
637
+
638
+ proxy.dispose();
639
+ });
640
+
641
+ test("dispatch failure on re-start in the same conversation restores the prior session", async () => {
642
+ const proxy = new HostAppControlProxy("conv-1");
643
+ const ctrl = new AbortController();
644
+
645
+ const p1 = proxy.request(
646
+ "app_control_start",
647
+ { tool: "start", app: "com.example.editor" },
648
+ "conv-1",
649
+ ctrl.signal,
650
+ );
651
+ proxy.resolve(
652
+ (sentMessages[0] as Record<string, unknown>).requestId as string,
653
+ payload({ pngBase64: PNG_A }),
654
+ );
655
+ await p1;
656
+ expect(_getActiveAppControlSession()?.app).toBe("com.example.editor");
657
+
658
+ // Re-start against a different app, then abort before the host
659
+ // responds. The catch path in `request()` should restore the prior
660
+ // session rather than stranding the lock.
661
+ sentMessages.length = 0;
662
+ const reCtrl = new AbortController();
663
+ const p2 = proxy.request(
664
+ "app_control_start",
665
+ { tool: "start", app: "com.example.other" },
666
+ "conv-1",
667
+ reCtrl.signal,
668
+ );
669
+ reCtrl.abort();
670
+ const r = await p2;
671
+ expect(r.isError).toBe(true);
672
+ expect(r.content).toContain("Aborted");
673
+
674
+ const session = _getActiveAppControlSession();
675
+ expect(session?.conversationId).toBe("conv-1");
676
+ expect(session?.app).toBe("com.example.editor");
677
+
678
+ proxy.dispose();
679
+ });
680
+
681
+ test("late-failing start does not clobber a newer successful start (out-of-order rollback)", async () => {
682
+ // Overlapping starts from the same conversation where the older one
683
+ // fails AFTER the newer succeeds. Identity-keyed rollback must make
684
+ // the stale failure a no-op rather than restoring the pre-A session.
685
+ const proxy = new HostAppControlProxy("conv-1");
686
+ const ctrl = new AbortController();
687
+
688
+ // Establish prior session A.
689
+ const pA = proxy.request(
690
+ "app_control_start",
691
+ { tool: "start", app: "com.example.a" },
692
+ "conv-1",
693
+ ctrl.signal,
694
+ );
695
+ const reqIdA = (sentMessages[0] as Record<string, unknown>)
696
+ .requestId as string;
697
+ proxy.resolve(reqIdA, payload({ pngBase64: PNG_A }));
698
+ await pA;
699
+ expect(_getActiveAppControlSession()?.app).toBe("com.example.a");
700
+
701
+ // Start B is dispatched but its host response is delayed.
702
+ sentMessages.length = 0;
703
+ const pB = proxy.request(
704
+ "app_control_start",
705
+ { tool: "start", app: "com.example.b" },
706
+ "conv-1",
707
+ ctrl.signal,
708
+ );
709
+ const reqIdB = (sentMessages[0] as Record<string, unknown>)
710
+ .requestId as string;
711
+
712
+ // Start C overtakes B and succeeds first.
713
+ sentMessages.length = 0;
714
+ const pC = proxy.request(
715
+ "app_control_start",
716
+ { tool: "start", app: "com.example.c" },
717
+ "conv-1",
718
+ ctrl.signal,
719
+ );
720
+ const reqIdC = (sentMessages[0] as Record<string, unknown>)
721
+ .requestId as string;
722
+ proxy.resolve(reqIdC, payload({ pngBase64: PNG_A }));
723
+ await pC;
724
+ expect(_getActiveAppControlSession()?.app).toBe("com.example.c");
725
+
726
+ // Now B finally fails — rollback must NOT restore A or clobber C.
727
+ proxy.resolve(reqIdB, payload({ state: "missing" }));
728
+ await pB;
729
+
730
+ const session = _getActiveAppControlSession();
731
+ expect(session?.conversationId).toBe("conv-1");
732
+ expect(session?.app).toBe("com.example.c");
733
+
734
+ proxy.dispose();
735
+ });
736
+
737
+ test("both-fail overlapping starts release the lock (no phantom session)", async () => {
738
+ // Two same-conversation starts overlap, both fail. The later
739
+ // rollback must release the lock instead of resurrecting the
740
+ // earlier (never-confirmed) optimistic write.
741
+ const proxy = new HostAppControlProxy("conv-1");
742
+ const ctrl = new AbortController();
743
+
744
+ const pA = proxy.request(
745
+ "app_control_start",
746
+ { tool: "start", app: "com.example.a" },
747
+ "conv-1",
748
+ ctrl.signal,
749
+ );
750
+ const reqIdA = (sentMessages[0] as Record<string, unknown>)
751
+ .requestId as string;
752
+
753
+ sentMessages.length = 0;
754
+ const pC = proxy.request(
755
+ "app_control_start",
756
+ { tool: "start", app: "com.example.c" },
757
+ "conv-1",
758
+ ctrl.signal,
759
+ );
760
+ const reqIdC = (sentMessages[0] as Record<string, unknown>)
761
+ .requestId as string;
762
+ expect(_getActiveAppControlSession()?.app).toBe("com.example.c");
763
+
764
+ // A fails first — identity check makes it a no-op (C is live).
765
+ proxy.resolve(reqIdA, payload({ state: "missing" }));
766
+ await pA;
767
+ expect(_getActiveAppControlSession()?.app).toBe("com.example.c");
768
+
769
+ // C fails second — must roll back to confirmed (undefined), not
770
+ // to A's never-confirmed optimistic write.
771
+ proxy.resolve(reqIdC, payload({ state: "missing" }));
772
+ await pC;
773
+
774
+ expect(_getActiveAppControlSession()).toBeUndefined();
775
+
776
+ proxy.dispose();
777
+ });
778
+
779
+ test("late running confirmation from older overlapping start is preserved", async () => {
780
+ // Same-conversation overlapping starts where the older one's `running`
781
+ // response arrives AFTER a newer optimistic start has superseded it,
782
+ // and the newer one then fails. The host has actually confirmed A as
783
+ // running, so the lock must remain held for A rather than going
784
+ // undefined and desyncing from the host.
785
+ const proxy = new HostAppControlProxy("conv-1");
786
+ const ctrl = new AbortController();
787
+
788
+ // A is dispatched; do not resolve yet.
789
+ const pA = proxy.request(
790
+ "app_control_start",
791
+ { tool: "start", app: "com.example.a" },
792
+ "conv-1",
793
+ ctrl.signal,
794
+ );
795
+ const reqIdA = (sentMessages[0] as Record<string, unknown>)
796
+ .requestId as string;
797
+
798
+ // C is dispatched, overwriting the optimistic active pointer to C.
799
+ sentMessages.length = 0;
800
+ const pC = proxy.request(
801
+ "app_control_start",
802
+ { tool: "start", app: "com.example.c" },
803
+ "conv-1",
804
+ ctrl.signal,
805
+ );
806
+ const reqIdC = (sentMessages[0] as Record<string, unknown>)
807
+ .requestId as string;
808
+ expect(_getActiveAppControlSession()?.app).toBe("com.example.c");
809
+
810
+ // A finally returns running — by object identity active is no longer A,
811
+ // but the host has confirmed A so we must still record it as confirmed.
812
+ proxy.resolve(reqIdA, payload({ pngBase64: PNG_A }));
813
+ await pA;
814
+ // Active is still the newer optimistic C; nothing has rolled it back.
815
+ expect(_getActiveAppControlSession()?.app).toBe("com.example.c");
816
+
817
+ // C fails — rollback must restore active to A (the current confirmed),
818
+ // not to undefined (the snapshot prior at C's dispatch time).
819
+ proxy.resolve(reqIdC, payload({ state: "missing" }));
820
+ await pC;
821
+
822
+ const session = _getActiveAppControlSession();
823
+ expect(session?.conversationId).toBe("conv-1");
824
+ expect(session?.app).toBe("com.example.a");
825
+
826
+ proxy.dispose();
827
+ });
828
+
829
+ test("first-start failure releases the lock (no prior session to restore)", async () => {
830
+ const proxy = new HostAppControlProxy("conv-1");
831
+ const ctrl = new AbortController();
832
+
833
+ // No prior session; re-start the first time and get a non-running.
834
+ const p1 = proxy.request(
835
+ "app_control_start",
836
+ { tool: "start", app: "com.example.editor" },
837
+ "conv-1",
838
+ ctrl.signal,
839
+ );
840
+ proxy.resolve(
841
+ (sentMessages[0] as Record<string, unknown>).requestId as string,
842
+ payload({ state: "missing" }),
843
+ );
844
+ await p1;
845
+
846
+ // Lock released so another conversation can acquire.
847
+ expect(_getActiveAppControlSession()).toBeUndefined();
848
+
849
+ proxy.dispose();
850
+ });
851
+ });
852
+
597
853
  // -------------------------------------------------------------------------
598
854
  // (d) dispose releases the lock
599
855
  // -------------------------------------------------------------------------
@@ -805,8 +1061,8 @@ describe("HostAppControlProxy", () => {
805
1061
  { tool: "observe", app: "com.example.app" },
806
1062
  "conv-1",
807
1063
  ctrl.signal,
808
- "actor-principal-1", // sourceActorPrincipalId
809
- "client-A", // targetClientId
1064
+ "actor-principal-1", // sourceActorPrincipalId
1065
+ "client-A", // targetClientId
810
1066
  );
811
1067
 
812
1068
  expect(sentMessages).toHaveLength(1);
@@ -840,8 +1096,8 @@ describe("HostAppControlProxy", () => {
840
1096
  { tool: "observe", app: "com.example.app" },
841
1097
  "conv-1",
842
1098
  ctrl.signal,
843
- "user-1", // sourceActorPrincipalId
844
- "client-A", // targetClientId → hub resolves actorPrincipalId = "user-1"
1099
+ "user-1", // sourceActorPrincipalId
1100
+ "client-A", // targetClientId → hub resolves actorPrincipalId = "user-1"
845
1101
  );
846
1102
 
847
1103
  const sent = sentMessages[0] as Record<string, unknown>;
@@ -863,8 +1119,8 @@ describe("HostAppControlProxy", () => {
863
1119
  { tool: "start", app: "com.example.app" },
864
1120
  "conv-1",
865
1121
  ctrl.signal,
866
- "user-1", // sourceActorPrincipalId
867
- undefined, // no targetClientId
1122
+ "user-1", // sourceActorPrincipalId
1123
+ undefined, // no targetClientId
868
1124
  );
869
1125
 
870
1126
  const sent = sentMessages[0] as Record<string, unknown>;
@@ -12,7 +12,7 @@ mock.module("../security/secure-keys.js", () => ({
12
12
  getProviderKeyAsync: async (_provider: string) => mockProviderKey,
13
13
  }));
14
14
 
15
- mock.module("../providers/managed-proxy/context.js", () => ({
15
+ mock.module("../providers/platform-proxy/context.js", () => ({
16
16
  resolveManagedProxyContext: async () => ({
17
17
  enabled: !!mockPlatformBaseUrl && !!mockAssistantApiKey,
18
18
  platformBaseUrl: mockPlatformBaseUrl,