@vellumai/assistant 0.8.10 → 0.8.11-staging.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (400) hide show
  1. package/bun.lock +62 -1
  2. package/docs/workspace-tools.md +196 -0
  3. package/examples/plugins/echo/README.md +3 -3
  4. package/knip.json +1 -0
  5. package/openapi.yaml +460 -128
  6. package/package.json +2 -1
  7. package/scripts/build-plugin-api.ts +299 -0
  8. package/src/__tests__/agent-loop-callsite-precedence.test.ts +7 -0
  9. package/src/__tests__/agent-loop-compaction-events.test.ts +197 -0
  10. package/src/__tests__/agent-loop-exit-reason.test.ts +93 -96
  11. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +2 -0
  12. package/src/__tests__/agent-loop-output-hooks.test.ts +274 -1
  13. package/src/__tests__/agent-loop-override-profile.test.ts +3 -0
  14. package/src/__tests__/agent-loop-provider-error-recording.test.ts +4 -0
  15. package/src/__tests__/agent-loop-thinking.test.ts +4 -0
  16. package/src/__tests__/agent-loop.test.ts +578 -5
  17. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +0 -1
  18. package/src/__tests__/approval-cascade.test.ts +1 -0
  19. package/src/__tests__/background-workers-disk-pressure.test.ts +0 -2
  20. package/src/__tests__/btw-routes.test.ts +0 -1
  21. package/src/__tests__/build-persisted-content.test.ts +75 -1
  22. package/src/__tests__/catalog-install-normalize.test.ts +141 -0
  23. package/src/__tests__/ces-startup-timeout.test.ts +60 -0
  24. package/src/__tests__/compaction-events.test.ts +1 -0
  25. package/src/__tests__/config-managed-gemini-defaults.test.ts +2 -46
  26. package/src/__tests__/context-overflow-reducer.test.ts +264 -124
  27. package/src/__tests__/context-window-manager-overflow-rung.test.ts +351 -0
  28. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  29. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +13 -5
  30. package/src/__tests__/conversation-agent-loop-overflow.test.ts +284 -455
  31. package/src/__tests__/conversation-agent-loop.test.ts +131 -551
  32. package/src/__tests__/conversation-app-control-instantiation.test.ts +13 -0
  33. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -0
  34. package/src/__tests__/conversation-fork-crud.test.ts +259 -0
  35. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  36. package/src/__tests__/conversation-lifecycle.test.ts +257 -1
  37. package/src/__tests__/conversation-process-callsite.test.ts +1 -0
  38. package/src/__tests__/conversation-provider-retry-repair.test.ts +38 -377
  39. package/src/__tests__/conversation-queue.test.ts +1 -39
  40. package/src/__tests__/conversation-runtime-assembly.test.ts +119 -8
  41. package/src/__tests__/conversation-skill-tools.test.ts +491 -5
  42. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  43. package/src/__tests__/conversation-slash-unknown.test.ts +1 -0
  44. package/src/__tests__/conversation-speed-override.test.ts +1 -0
  45. package/src/__tests__/conversation-store.test.ts +74 -0
  46. package/src/__tests__/conversation-surfaces-app-control.test.ts +4 -1
  47. package/src/__tests__/conversation-tool-setup-attribution.test.ts +323 -0
  48. package/src/__tests__/conversation-tool-setup-tools-disabled.test.ts +34 -0
  49. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -0
  50. package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
  51. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -0
  52. package/src/__tests__/corrected-target.test.ts +93 -0
  53. package/src/__tests__/credential-execution-feature-gates.test.ts +3 -5
  54. package/src/__tests__/credential-execution-tools.test.ts +23 -11
  55. package/src/__tests__/credential-security-invariants.test.ts +6 -1
  56. package/src/__tests__/db-schedule-syntax-migration.test.ts +80 -0
  57. package/src/__tests__/device-id.test.ts +70 -1
  58. package/src/__tests__/embedding-managed-proxy-selection.test.ts +6 -40
  59. package/src/__tests__/empty-response-hook.test.ts +242 -66
  60. package/src/__tests__/external-plugin-loader.test.ts +0 -31
  61. package/src/__tests__/get-skill-detail-audit.test.ts +43 -1
  62. package/src/__tests__/guardian-routing-invariants.test.ts +91 -0
  63. package/src/__tests__/history-repair-hook.test.ts +228 -3
  64. package/src/__tests__/host-app-control-proxy.test.ts +45 -0
  65. package/src/__tests__/host-browser-proxy.test.ts +254 -9
  66. package/src/__tests__/identity-routes.test.ts +1 -0
  67. package/src/__tests__/image-recovery-hook.test.ts +387 -0
  68. package/src/__tests__/injector-chain.test.ts +5 -4
  69. package/src/__tests__/injector-v3-suppression.test.ts +373 -47
  70. package/src/__tests__/intent-routing.test.ts +7 -0
  71. package/src/__tests__/memory-retrieval-hook.test.ts +117 -15
  72. package/src/__tests__/notification-decision-strategy.test.ts +3 -3
  73. package/src/__tests__/oauth-store.test.ts +0 -85
  74. package/src/__tests__/{context-overflow-policy.test.ts → overflow-policy.test.ts} +1 -1
  75. package/src/__tests__/parallel-tool.benchmark.test.ts +4 -0
  76. package/src/__tests__/persist-unsendable-image-downscale.test.ts +29 -9
  77. package/src/__tests__/persist-unsendable-image.test.ts +4 -4
  78. package/src/__tests__/persistence-secret-redaction.test.ts +78 -0
  79. package/src/__tests__/plugin-bootstrap.test.ts +82 -73
  80. package/src/__tests__/plugin-tool-contribution.test.ts +7 -4
  81. package/src/__tests__/plugin-types.test.ts +0 -8
  82. package/src/__tests__/provider-catalog-visibility.test.ts +1 -9
  83. package/src/__tests__/prune-old-conversations-job.test.ts +99 -0
  84. package/src/__tests__/registry.test.ts +240 -1
  85. package/src/__tests__/require-fresh-approval.test.ts +3 -0
  86. package/src/__tests__/schedule-routes.test.ts +116 -1
  87. package/src/__tests__/schedule-store.test.ts +28 -0
  88. package/src/__tests__/schedule-tools.test.ts +94 -1
  89. package/src/__tests__/server-history-render.test.ts +39 -0
  90. package/src/__tests__/skill-projection-feature-flag.test.ts +13 -0
  91. package/src/__tests__/skill-projection.benchmark.test.ts +25 -7
  92. package/src/__tests__/skills.test.ts +202 -0
  93. package/src/__tests__/slim-skill-category.test.ts +195 -0
  94. package/src/__tests__/strip-memory-injections.test.ts +33 -39
  95. package/src/__tests__/test-support/tool-invocation-seed.ts +79 -0
  96. package/src/__tests__/title-generate-hook.test.ts +9 -7
  97. package/src/__tests__/tool-audit-listener.test.ts +264 -1
  98. package/src/__tests__/tool-error-hook.test.ts +4 -3
  99. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -0
  100. package/src/__tests__/tool-executor-lifecycle-events.test.ts +273 -0
  101. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
  102. package/src/__tests__/tool-start-timestamp.test.ts +218 -0
  103. package/src/__tests__/tools-get-route.test.ts +202 -0
  104. package/src/__tests__/workspace-tool-loader.test.ts +319 -0
  105. package/src/__tests__/workspace-tools-watcher-flag.test.ts +70 -0
  106. package/src/agent/loop.ts +569 -319
  107. package/src/api/events/tool-result.ts +9 -0
  108. package/src/api/events/tool-use-start.ts +7 -0
  109. package/src/api/index.ts +10 -0
  110. package/src/api/responses/conversation-message.ts +135 -27
  111. package/src/api/responses/memory-v3-selection-log.ts +4 -4
  112. package/src/approvals/guardian-request-resolvers.ts +26 -0
  113. package/src/browser-session/backends/host-bridge.ts +29 -0
  114. package/src/browser-session/index.ts +1 -0
  115. package/src/browser-session/types.ts +5 -1
  116. package/src/cli/commands/__tests__/schedules.test.ts +62 -4
  117. package/src/cli/commands/__tests__/skills.test.ts +53 -0
  118. package/src/cli/commands/channel-verification-sessions.ts +6 -6
  119. package/src/cli/commands/inference-providers.ts +0 -8
  120. package/src/cli/commands/plugins.ts +2 -2
  121. package/src/cli/commands/schedules.ts +27 -4
  122. package/src/cli/commands/skills.ts +187 -146
  123. package/src/cli/commands/tools.ts +106 -0
  124. package/src/cli/lib/__tests__/install-from-github.test.ts +256 -328
  125. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +6 -2
  126. package/src/cli/lib/__tests__/plugin-details.test.ts +10 -16
  127. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +2 -2
  128. package/src/cli/lib/__tests__/search-plugins.test.ts +145 -240
  129. package/src/cli/lib/install-from-github.ts +187 -117
  130. package/src/cli/lib/plugin-catalog-cache.ts +9 -9
  131. package/src/cli/lib/plugin-details.ts +38 -68
  132. package/src/cli/lib/plugin-marketplace.ts +42 -14
  133. package/src/cli/lib/search-plugins.ts +29 -129
  134. package/src/cli/program.ts +2 -0
  135. package/src/config/bundled-skills/acp/SKILL.md +1 -0
  136. package/src/config/bundled-skills/app-builder/SKILL.md +1 -0
  137. package/src/config/bundled-skills/app-control/SKILL.md +1 -0
  138. package/src/config/bundled-skills/computer-use/SKILL.md +1 -0
  139. package/src/config/bundled-skills/contacts/SKILL.md +1 -0
  140. package/src/config/bundled-skills/document-editor/SKILL.md +1 -0
  141. package/src/config/bundled-skills/followups/SKILL.md +1 -0
  142. package/src/config/bundled-skills/image-studio/SKILL.md +1 -0
  143. package/src/config/bundled-skills/media-processing/SKILL.md +1 -0
  144. package/src/config/bundled-skills/messaging/SKILL.md +1 -0
  145. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -0
  146. package/src/config/bundled-skills/playbooks/SKILL.md +1 -0
  147. package/src/config/bundled-skills/schedule/SKILL.md +1 -0
  148. package/src/config/bundled-skills/schedule/TOOLS.json +11 -3
  149. package/src/config/bundled-skills/sequences/SKILL.md +1 -0
  150. package/src/config/bundled-skills/settings/SKILL.md +1 -0
  151. package/src/config/bundled-skills/skill-management/SKILL.md +101 -1
  152. package/src/config/bundled-skills/subagent/SKILL.md +1 -0
  153. package/src/config/bundled-skills/transcribe/SKILL.md +1 -0
  154. package/src/config/env-registry.ts +23 -0
  155. package/src/config/feature-flag-registry.json +17 -81
  156. package/src/config/loader.ts +5 -22
  157. package/src/config/schema.ts +2 -0
  158. package/src/config/schemas/__tests__/compaction-logs.test.ts +56 -0
  159. package/src/config/schemas/__tests__/memory-v2.test.ts +0 -1
  160. package/src/config/schemas/__tests__/memory-v3.test.ts +61 -1
  161. package/src/config/schemas/compaction-logs.ts +79 -0
  162. package/src/config/schemas/memory-v2.ts +0 -8
  163. package/src/config/schemas/memory-v3.ts +104 -33
  164. package/src/config/seed-inference-profiles.ts +1 -1
  165. package/src/config/skills.ts +117 -47
  166. package/src/context/compactor.ts +11 -0
  167. package/src/context/strip-injections.ts +38 -4
  168. package/src/credential-execution/feature-gates.ts +0 -21
  169. package/src/credential-execution/startup-timeout.ts +32 -4
  170. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +18 -0
  171. package/src/daemon/conversation-agent-loop-handlers.ts +140 -91
  172. package/src/daemon/conversation-agent-loop.ts +123 -658
  173. package/src/daemon/conversation-error.ts +6 -33
  174. package/src/daemon/conversation-lifecycle.ts +1 -1
  175. package/src/daemon/conversation-runtime-assembly.ts +183 -22
  176. package/src/daemon/conversation-skill-tools.ts +137 -8
  177. package/src/daemon/conversation-slash.ts +0 -14
  178. package/src/daemon/conversation-store.ts +2 -19
  179. package/src/daemon/conversation-tool-setup.ts +87 -1
  180. package/src/daemon/conversation.ts +119 -50
  181. package/src/daemon/external-plugins-bootstrap.ts +36 -95
  182. package/src/daemon/handlers/config-channels.ts +11 -2
  183. package/src/daemon/handlers/shared.ts +9 -1
  184. package/src/daemon/handlers/skills.ts +10 -4
  185. package/src/daemon/host-app-control-proxy.ts +72 -57
  186. package/src/daemon/host-browser-proxy.ts +117 -22
  187. package/src/daemon/lifecycle.ts +34 -5
  188. package/src/daemon/message-protocol.ts +0 -7
  189. package/src/daemon/message-types/schedules.ts +1 -0
  190. package/src/daemon/message-types/skills.ts +17 -0
  191. package/src/daemon/providers-setup.ts +3 -0
  192. package/src/daemon/server.ts +3 -3
  193. package/src/daemon/tool-setup-types.ts +9 -3
  194. package/src/daemon/trust-context.ts +23 -0
  195. package/src/daemon/workspace-tools-watcher.ts +324 -0
  196. package/src/events/tool-audit-listener.ts +78 -16
  197. package/src/events/tool-metrics-listener.ts +2 -5
  198. package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +227 -0
  199. package/src/memory/__tests__/conversation-queries.test.ts +176 -0
  200. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +20 -32
  201. package/src/memory/compaction-log-writer-clickhouse.ts +418 -0
  202. package/src/memory/conversation-crud.ts +134 -3
  203. package/src/memory/conversation-queries.ts +64 -7
  204. package/src/memory/db-init.ts +14 -0
  205. package/src/memory/embedding-backend.test.ts +130 -1
  206. package/src/memory/embedding-backend.ts +79 -106
  207. package/src/memory/embedding-gemini.ts +5 -0
  208. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +12 -0
  209. package/src/memory/graph/__tests__/handle-remember-v2.test.ts +19 -0
  210. package/src/memory/graph/conversation-graph-memory.ts +36 -25
  211. package/src/memory/graph/tool-handlers.ts +3 -0
  212. package/src/memory/job-handlers/cleanup.ts +3 -1
  213. package/src/memory/jobs-store.ts +0 -28
  214. package/src/memory/jobs-worker.ts +10 -22
  215. package/src/memory/memory-marker.ts +29 -0
  216. package/src/memory/memory-retrospective-startup-cleanup.ts +1 -1
  217. package/src/memory/migrations/268-add-memory-v3-selections.ts +6 -0
  218. package/src/memory/migrations/270-schedule-description.ts +36 -0
  219. package/src/memory/migrations/275-tool-invocations-add-skill-id.test.ts +81 -0
  220. package/src/memory/migrations/275-tool-invocations-add-skill-id.ts +20 -0
  221. package/src/memory/migrations/276-tool-invocations-created-at-id-index.test.ts +68 -0
  222. package/src/memory/migrations/276-tool-invocations-created-at-id-index.ts +20 -0
  223. package/src/memory/migrations/277-add-memory-v3-ever-injected.ts +29 -0
  224. package/src/memory/migrations/278-tool-invocations-telemetry-columns.test.ts +96 -0
  225. package/src/memory/migrations/278-tool-invocations-telemetry-columns.ts +39 -0
  226. package/src/memory/migrations/279-create-skill-loaded-events.test.ts +84 -0
  227. package/src/memory/migrations/279-create-skill-loaded-events.ts +26 -0
  228. package/src/memory/migrations/280-conversations-surfaced-at.test.ts +88 -0
  229. package/src/memory/migrations/280-conversations-surfaced-at.ts +24 -0
  230. package/src/memory/migrations/index.ts +10 -0
  231. package/src/memory/migrations/registry.ts +8 -0
  232. package/src/memory/schema/conversations.ts +16 -0
  233. package/src/memory/schema/infrastructure.ts +26 -0
  234. package/src/memory/skill-loaded-events-store.test.ts +160 -0
  235. package/src/memory/skill-loaded-events-store.ts +95 -0
  236. package/src/memory/tool-executed-events-store.test.ts +219 -0
  237. package/src/memory/tool-executed-events-store.ts +102 -0
  238. package/src/memory/tool-usage-store.ts +15 -3
  239. package/src/memory/v2/__tests__/consolidation-job.test.ts +117 -12
  240. package/src/memory/v2/__tests__/consolidation-prompt-flag-gating-guard.test.ts +189 -0
  241. package/src/memory/v2/__tests__/injected-block-slugs.test.ts +90 -0
  242. package/src/memory/v2/__tests__/page-store.test.ts +33 -0
  243. package/src/memory/v2/__tests__/prompts-consolidation.test.ts +88 -15
  244. package/src/memory/v2/activation-store.ts +50 -1
  245. package/src/memory/v2/consolidation-job.ts +113 -29
  246. package/src/memory/v2/injected-block-slugs.ts +79 -0
  247. package/src/memory/v2/injection.ts +6 -1
  248. package/src/memory/v2/prompts/consolidation.ts +414 -13
  249. package/src/memory/v2/router.ts +2 -28
  250. package/src/memory/v2/static-context.ts +1 -1
  251. package/src/memory/v2/types.ts +16 -0
  252. package/src/notifications/__tests__/copy-composer.test.ts +244 -0
  253. package/src/notifications/access-request-copy.ts +298 -0
  254. package/src/notifications/adapters/slack.ts +3 -3
  255. package/src/notifications/adapters/telegram.ts +2 -1
  256. package/src/notifications/copy-composer.ts +49 -267
  257. package/src/notifications/decision-engine.ts +16 -35
  258. package/src/notifications/home-feed-side-effect.ts +1 -6
  259. package/src/oauth/oauth-store.ts +0 -9
  260. package/src/permissions/checker.test.ts +83 -1
  261. package/src/permissions/checker.ts +25 -2
  262. package/src/permissions/gateway-threshold-reader.test.ts +182 -0
  263. package/src/permissions/gateway-threshold-reader.ts +80 -0
  264. package/src/platform/client.ts +1 -3
  265. package/src/platform/feature-gate.ts +3 -12
  266. package/src/plugin-api/constants.ts +4 -2
  267. package/src/plugin-api/index.ts +63 -11
  268. package/src/plugin-api/types.ts +236 -71
  269. package/src/plugins/defaults/compaction/compact.ts +66 -2
  270. package/src/plugins/defaults/compaction/context-overflow-reducer.ts +240 -32
  271. package/src/plugins/defaults/compaction/corrected-target.ts +53 -0
  272. package/src/{daemon/context-overflow-policy.ts → plugins/defaults/compaction/overflow-policy.ts} +1 -1
  273. package/src/plugins/defaults/compaction/window-manager.ts +303 -1
  274. package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +173 -0
  275. package/src/plugins/defaults/empty-response/hooks/stop.ts +11 -115
  276. package/src/plugins/defaults/empty-response/nudge-state-store.ts +46 -0
  277. package/src/plugins/defaults/history-repair/hooks/post-model-call.ts +50 -0
  278. package/src/plugins/defaults/history-repair/hooks/stop.ts +22 -0
  279. package/src/plugins/defaults/history-repair/repair-state-store.ts +51 -0
  280. package/src/plugins/defaults/history-repair/terminal.ts +39 -2
  281. package/src/plugins/defaults/image-recovery/detect.ts +25 -0
  282. package/src/plugins/defaults/image-recovery/hooks/post-model-call.ts +73 -0
  283. package/src/plugins/defaults/image-recovery/hooks/stop.ts +22 -0
  284. package/src/plugins/defaults/image-recovery/image-recovery-state-store.ts +48 -0
  285. package/src/plugins/defaults/image-recovery/package.json +14 -0
  286. package/src/{daemon/persist-unsendable-image.ts → plugins/defaults/image-recovery/recover.ts} +67 -14
  287. package/src/plugins/defaults/index.ts +71 -5
  288. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +76 -112
  289. package/src/plugins/defaults/memory-retrieval/hooks/{user-prompt-submit-temp.ts → user-prompt-submit.ts} +83 -74
  290. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +14 -8
  291. package/src/plugins/defaults/memory-retrieval/injectors.ts +2 -18
  292. package/src/plugins/defaults/memory-retrieval/package.json +14 -0
  293. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1157 -0
  294. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +683 -0
  295. package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +161 -140
  296. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +160 -0
  297. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +335 -316
  298. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +145 -53
  299. package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +38 -1
  300. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +18 -8
  301. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +112 -71
  302. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +192 -51
  303. package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +4 -16
  304. package/src/plugins/defaults/memory-v3-shadow/card.test.ts +173 -0
  305. package/src/plugins/defaults/memory-v3-shadow/card.ts +116 -0
  306. package/src/plugins/defaults/memory-v3-shadow/core-set.test.ts +104 -0
  307. package/src/plugins/defaults/memory-v3-shadow/core-set.ts +59 -0
  308. package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.test.ts +305 -0
  309. package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.ts +278 -0
  310. package/src/plugins/defaults/memory-v3-shadow/hot-set.test.ts +138 -0
  311. package/src/plugins/defaults/memory-v3-shadow/hot-set.ts +85 -0
  312. package/src/plugins/defaults/memory-v3-shadow/injector.ts +331 -24
  313. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +119 -13
  314. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +169 -114
  315. package/src/plugins/defaults/memory-v3-shadow/page-content.ts +47 -16
  316. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +144 -66
  317. package/src/plugins/defaults/memory-v3-shadow/prune.test.ts +758 -0
  318. package/src/plugins/defaults/memory-v3-shadow/prune.ts +471 -0
  319. package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +68 -16
  320. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +19 -12
  321. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +96 -43
  322. package/src/plugins/defaults/memory-v3-shadow/types.ts +34 -17
  323. package/src/plugins/defaults/title-generate/hooks/stop.ts +9 -11
  324. package/src/plugins/pipeline.ts +8 -5
  325. package/src/plugins/types.ts +5 -51
  326. package/src/providers/cache-control.ts +26 -0
  327. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +1 -2
  328. package/src/providers/model-catalog.ts +13 -1
  329. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +147 -0
  330. package/src/providers/openai/chat-completions-provider.ts +46 -0
  331. package/src/providers/openai/responses-provider.ts +45 -0
  332. package/src/providers/registry.ts +0 -8
  333. package/src/runtime/__tests__/agent-wake.test.ts +0 -1
  334. package/src/runtime/agent-wake.ts +13 -0
  335. package/src/runtime/routes/__tests__/acp-routes.test.ts +151 -0
  336. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +12 -50
  337. package/src/runtime/routes/__tests__/conversation-surface-routes.test.ts +322 -0
  338. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +0 -62
  339. package/src/runtime/routes/__tests__/plugins-routes.test.ts +31 -11
  340. package/src/runtime/routes/acp-routes.test.ts +106 -0
  341. package/src/runtime/routes/acp-routes.ts +248 -2
  342. package/src/runtime/routes/browser-tabs-routes.ts +1 -1
  343. package/src/runtime/routes/channel-verification-routes.ts +14 -5
  344. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +0 -14
  345. package/src/runtime/routes/consolidation-routes.ts +6 -82
  346. package/src/runtime/routes/conversation-list-routes.ts +6 -0
  347. package/src/runtime/routes/conversation-management-routes.ts +71 -0
  348. package/src/runtime/routes/identity-routes.ts +8 -0
  349. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +33 -34
  350. package/src/runtime/routes/inference-provider-connection-routes.ts +0 -45
  351. package/src/runtime/routes/plugins-routes.ts +34 -45
  352. package/src/runtime/routes/schedule-routes.ts +43 -5
  353. package/src/runtime/routes/settings-routes.ts +140 -15
  354. package/src/runtime/routes/skills-routes.ts +18 -6
  355. package/src/runtime/services/__tests__/conversation-serializer.test.ts +140 -0
  356. package/src/runtime/services/conversation-serializer.ts +38 -1
  357. package/src/runtime/verification-outbound-actions.ts +147 -2
  358. package/src/runtime/verification-templates.ts +29 -3
  359. package/src/schedule/schedule-store.ts +19 -0
  360. package/src/skills/catalog-install.ts +77 -13
  361. package/src/tasks/task-scheduler.ts +1 -0
  362. package/src/telemetry/types.ts +66 -1
  363. package/src/telemetry/usage-telemetry-reporter.test.ts +542 -13
  364. package/src/telemetry/usage-telemetry-reporter.ts +213 -20
  365. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +49 -2
  366. package/src/tools/browser/__tests__/browser-status.test.ts +29 -5
  367. package/src/tools/browser/browser-execution.ts +27 -9
  368. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +380 -4
  369. package/src/tools/browser/cdp-client/__tests__/host-bridge-cdp-client.test.ts +107 -0
  370. package/src/tools/browser/cdp-client/__tests__/types.test.ts +6 -1
  371. package/src/tools/browser/cdp-client/factory.ts +217 -17
  372. package/src/tools/browser/cdp-client/host-bridge-cdp-client.ts +67 -0
  373. package/src/tools/browser/cdp-client/types.ts +22 -2
  374. package/src/tools/credential-execution/make-authenticated-request.ts +2 -1
  375. package/src/tools/credential-execution/manage-secure-command-tool.ts +169 -164
  376. package/src/tools/credential-execution/run-authenticated-command.ts +2 -1
  377. package/src/tools/executor.ts +39 -7
  378. package/src/tools/registry.ts +387 -5
  379. package/src/tools/schedule/create.ts +16 -0
  380. package/src/tools/schedule/list.ts +12 -4
  381. package/src/tools/schedule/update.ts +12 -0
  382. package/src/tools/skills/load.ts +11 -6
  383. package/src/tools/terminal/safe-env.ts +2 -0
  384. package/src/tools/types.ts +65 -9
  385. package/src/tools/workspace-tools/loader.ts +673 -0
  386. package/src/usage/attribution.ts +28 -0
  387. package/src/util/device-id.ts +17 -3
  388. package/src/util/platform.ts +16 -0
  389. package/tsconfig.plugin-api.json +13 -0
  390. package/src/__tests__/plugin-external-api.test.ts +0 -68
  391. package/src/__tests__/plugin-skill-contribution.test.ts +0 -355
  392. package/src/daemon/message-types/browser.ts +0 -10
  393. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +0 -187
  394. package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/eval-turns.json +0 -36
  395. package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/live-turns.json +0 -37
  396. package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-eviction.test.ts +0 -106
  397. package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-skeleton.test.ts +0 -44
  398. package/src/plugins/defaults/memory-v3-shadow/working-set.ts +0 -91
  399. package/src/plugins/external-api.ts +0 -114
  400. package/src/plugins/plugin-skill-contributions.ts +0 -292
@@ -1,23 +1,29 @@
1
1
  /**
2
- * Tests for the default `empty-response` plugin's `stop` hook.
2
+ * Tests for the default `empty-response` plugin's hooks.
3
3
  *
4
4
  * Covers:
5
- * - The default hook's binary decision for the canonical cases: empty turn
5
+ * - The `post-model-call` hook's decision for the canonical cases: empty turn
6
6
  * after a prior tool-use turn → continue (with the canonical nudge text);
7
7
  * visible text → stop; prior turn already delivered visible text → stop;
8
- * first model call with no prior turn → stop; provider refusal → continue
9
- * (with the refusal-specific nudge text); refusal-but-recovered → stop.
8
+ * first model call with no prior turn → stop; provider refusal → stop with
9
+ * the turn rewritten to the user-facing fallback; refusal-but-recovered →
10
+ * stop; refusal after a visible reply this run → stop with no rewrite.
10
11
  * - The hook scopes its prior-turn signals to the current response cycle (the
11
12
  * messages after the last genuine user prompt), so visible text from the
12
13
  * inbound conversation does not suppress the nudge.
13
14
  * - When the hook continues, it appends the nudge as a `user` message to
14
15
  * `messages`.
16
+ * - The retry bound is split across the two hooks: `post-model-call` marks the
17
+ * bound (nudging at most once per run) and the `stop` hook clears it on the
18
+ * definitive terminal so the next run nudges afresh.
19
+ * - The hook ignores outcomes it does not own: a provider rejection (it carries
20
+ * an `error`) and a tool-bearing turn (the loop continues on its own).
15
21
  * - End-to-end through `runHook` + the registry: registering the default
16
22
  * plugin makes the hook fire, and a later-registered user hook chains after
17
23
  * it and can read/override the decision.
18
24
  *
19
- * The loop's actual side-effects (retry-budget cap, history splice, log
20
- * emission) live in `agent/loop.ts` and are covered by integration tests in
25
+ * The loop's actual side-effects (per-run backstop, history splice, streamed
26
+ * output) live in `agent/loop.ts` and are covered by integration tests in
21
27
  * `conversation-agent-loop.test.ts` / `agent-loop.test.ts`. This file isolates
22
28
  * the hook.
23
29
  */
@@ -25,12 +31,22 @@
25
31
  import { beforeEach, describe, expect, test } from "bun:test";
26
32
 
27
33
  import { HOOKS } from "../plugin-api/constants.js";
28
- import type { PluginLogger, StopContext } from "../plugin-api/types.js";
34
+ import type {
35
+ PluginLogger,
36
+ PostModelCallContext,
37
+ StopContext,
38
+ } from "../plugin-api/types.js";
29
39
  import {
30
40
  NUDGE_TEXT,
31
- REFUSAL_NUDGE_TEXT,
32
- } from "../plugins/defaults/empty-response/hooks/stop.js";
41
+ REFUSAL_FALLBACK_TEXT,
42
+ } from "../plugins/defaults/empty-response/hooks/post-model-call.js";
43
+ import postModelCall from "../plugins/defaults/empty-response/hooks/post-model-call.js";
33
44
  import stop from "../plugins/defaults/empty-response/hooks/stop.js";
45
+ import {
46
+ isEmptyResponseNudged,
47
+ markEmptyResponseNudged,
48
+ resetEmptyResponseNudgeStoreForTests,
49
+ } from "../plugins/defaults/empty-response/nudge-state-store.js";
34
50
  import { defaultEmptyResponsePlugin } from "../plugins/defaults/index.js";
35
51
  import { runHook } from "../plugins/pipeline.js";
36
52
  import {
@@ -67,11 +83,14 @@ function userPrompt(text: string): Message {
67
83
  return { role: "user", content: [{ type: "text", text }] };
68
84
  }
69
85
 
70
- function makeCtx(overrides: Partial<StopContext> = {}): StopContext {
86
+ function makeCtx(
87
+ overrides: Partial<PostModelCallContext> = {},
88
+ ): PostModelCallContext {
71
89
  return {
72
- conversationId: "conv-stop",
90
+ conversationId: "conv-pmc",
91
+ callSite: "mainAgent",
92
+ content: [],
73
93
  messages: [],
74
- responseContent: [],
75
94
  stopReason: null,
76
95
  decision: "stop",
77
96
  logger: noopLogger,
@@ -79,19 +98,23 @@ function makeCtx(overrides: Partial<StopContext> = {}): StopContext {
79
98
  };
80
99
  }
81
100
 
101
+ beforeEach(() => {
102
+ resetEmptyResponseNudgeStoreForTests();
103
+ });
104
+
82
105
  // ─── Default decisions ───────────────────────────────────────────────────────
83
106
 
84
- describe("empty-response stop hook — default decisions", () => {
107
+ describe("empty-response post-model-call hook — default decisions", () => {
85
108
  test("empty turn after a prior tool-use turn → continue with canonical nudge", async () => {
86
109
  // GIVEN a run that already issued a tool call, then returned an empty
87
110
  // (whitespace-only) assistant turn with no visible text.
88
111
  const ctx = makeCtx({
89
112
  messages: [priorToolUseTurn],
90
- responseContent: [emptyTextBlock],
113
+ content: [emptyTextBlock],
91
114
  });
92
115
 
93
- // WHEN the default stop hook runs.
94
- await stop(ctx);
116
+ // WHEN the default post-model-call hook runs.
117
+ await postModelCall(ctx);
95
118
 
96
119
  // THEN it asks the loop to continue and appends the canonical nudge.
97
120
  expect(ctx.decision).toBe("continue");
@@ -105,28 +128,44 @@ describe("empty-response stop hook — default decisions", () => {
105
128
  // GIVEN a turn that delivered visible text.
106
129
  const ctx = makeCtx({
107
130
  messages: [priorToolUseTurn],
108
- responseContent: [{ type: "text", text: "here is a summary" }],
131
+ content: [{ type: "text", text: "here is a summary" }],
109
132
  });
110
133
 
111
134
  // WHEN the hook runs.
112
- await stop(ctx);
135
+ await postModelCall(ctx);
113
136
 
114
137
  // THEN the decision stays at stop and nothing is appended.
115
138
  expect(ctx.decision).toBe("stop");
116
139
  expect(ctx.messages).toEqual([priorToolUseTurn]);
117
140
  });
118
141
 
142
+ test("tool-bearing turn is ignored — no nudge, no rewrite", async () => {
143
+ // GIVEN a turn that carries a tool call (the loop continues on its own to
144
+ // run the tool, so the retry decision is not the hook's to make).
145
+ const ctx = makeCtx({
146
+ messages: [priorToolUseTurn],
147
+ content: [{ type: "tool_use", id: "tu_2", name: "read_file", input: {} }],
148
+ });
149
+
150
+ // WHEN the hook runs.
151
+ await postModelCall(ctx);
152
+
153
+ // THEN it leaves the decision and history untouched.
154
+ expect(ctx.decision).toBe("stop");
155
+ expect(ctx.messages).toEqual([priorToolUseTurn]);
156
+ });
157
+
119
158
  test("prior turn already delivered visible text → stop", async () => {
120
159
  // GIVEN the model said its piece in an earlier turn this run, then ended
121
160
  // with a side-effect tool and returned empty. Nudging would force a
122
161
  // verbatim re-send of text the user already saw.
123
162
  const ctx = makeCtx({
124
163
  messages: [userPrompt("do X"), priorVisibleTextTurn, priorToolUseTurn],
125
- responseContent: [],
164
+ content: [],
126
165
  });
127
166
 
128
167
  // WHEN the hook runs.
129
- await stop(ctx);
168
+ await postModelCall(ctx);
130
169
 
131
170
  // THEN the decision stays at stop.
132
171
  expect(ctx.decision).toBe("stop");
@@ -143,11 +182,11 @@ describe("empty-response stop hook — default decisions", () => {
143
182
  userPrompt("do the next thing"),
144
183
  priorToolUseTurn,
145
184
  ],
146
- responseContent: [],
185
+ content: [],
147
186
  });
148
187
 
149
188
  // WHEN the hook runs.
150
- await stop(ctx);
189
+ await postModelCall(ctx);
151
190
 
152
191
  // THEN it continues — the cycle scope sees only the tool-use turn, not the
153
192
  // inbound conversation's visible text.
@@ -161,10 +200,10 @@ describe("empty-response stop hook — default decisions", () => {
161
200
  test("first model call with no prior turn → stop", async () => {
162
201
  // GIVEN an empty first assistant response with no prior turn this run and
163
202
  // no refusal — not the pattern the organic-empty-turn nudge guards against.
164
- const ctx = makeCtx({ messages: [], responseContent: [] });
203
+ const ctx = makeCtx({ messages: [], content: [] });
165
204
 
166
205
  // WHEN the hook runs.
167
- await stop(ctx);
206
+ await postModelCall(ctx);
168
207
 
169
208
  // THEN the decision stays at stop.
170
209
  expect(ctx.decision).toBe("stop");
@@ -172,88 +211,225 @@ describe("empty-response stop hook — default decisions", () => {
172
211
 
173
212
  // ─── Refusal stop ──────────────────────────────────────────────────────────
174
213
 
175
- test("refusal on the first call with no content → continue with refusal nudge", async () => {
214
+ test("refusal on the first call with no content → stop with rewritten fallback", async () => {
176
215
  // GIVEN the canonical failure mode: the provider's safety classifier zeros
177
216
  // the response on the very first model call, returning `stopReason:
178
217
  // "refusal"` and no visible text.
179
218
  const ctx = makeCtx({
180
219
  messages: [],
181
- responseContent: [],
220
+ content: [],
182
221
  stopReason: "refusal",
183
222
  });
184
223
 
185
224
  // WHEN the hook runs.
186
- await stop(ctx);
225
+ await postModelCall(ctx);
187
226
 
188
- // THEN it continues with the refusal-specific nudge, even with no prior turn.
189
- expect(ctx.decision).toBe("continue");
190
- expect(ctx.messages.at(-1)).toEqual({
191
- role: "user",
192
- content: [{ type: "text", text: REFUSAL_NUDGE_TEXT }],
193
- });
227
+ // THEN it lets the turn end and rewrites the turn content to the
228
+ // user-facing fallback, without nudging the model.
229
+ expect(ctx.decision).toBe("stop");
230
+ expect(ctx.content).toEqual([
231
+ { type: "text", text: REFUSAL_FALLBACK_TEXT },
232
+ ]);
233
+ expect(ctx.messages).toEqual([]);
194
234
  });
195
235
 
196
- test("refusal with a thinking-only block still continues", async () => {
236
+ test("refusal with a thinking-only block is still rewritten", async () => {
197
237
  // GIVEN a refusal whose only content is a thinking block — the user sees
198
238
  // nothing.
199
239
  const ctx = makeCtx({
200
240
  messages: [],
201
- responseContent: [
202
- { type: "thinking", thinking: "...", signature: "sig" },
203
- ],
241
+ content: [{ type: "thinking", thinking: "...", signature: "sig" }],
204
242
  stopReason: "refusal",
205
243
  });
206
244
 
207
245
  // WHEN the hook runs.
208
- await stop(ctx);
246
+ await postModelCall(ctx);
209
247
 
210
- // THEN it continues with the refusal-specific nudge.
211
- expect(ctx.decision).toBe("continue");
212
- expect(ctx.messages.at(-1)).toEqual({
213
- role: "user",
214
- content: [{ type: "text", text: REFUSAL_NUDGE_TEXT }],
215
- });
248
+ // THEN it stops and rewrites the turn content to the fallback.
249
+ expect(ctx.decision).toBe("stop");
250
+ expect(ctx.content).toEqual([
251
+ { type: "text", text: REFUSAL_FALLBACK_TEXT },
252
+ ]);
216
253
  });
217
254
 
218
- test("refusal but visible text present → stop (model recovered)", async () => {
255
+ test("refusal but visible text present → stop, no rewrite (model recovered)", async () => {
219
256
  // GIVEN a refusal that still delivered some visible text before refusing —
220
257
  // the user has something to see.
221
258
  const ctx = makeCtx({
222
- responseContent: [{ type: "text", text: "partial answer" }],
259
+ content: [{ type: "text", text: "partial answer" }],
223
260
  stopReason: "refusal",
224
261
  });
225
262
 
226
263
  // WHEN the hook runs.
227
- await stop(ctx);
264
+ await postModelCall(ctx);
228
265
 
229
- // THEN the decision stays at stop.
266
+ // THEN the decision stays at stop and the turn content is left untouched.
230
267
  expect(ctx.decision).toBe("stop");
268
+ expect(ctx.content).toEqual([{ type: "text", text: "partial answer" }]);
231
269
  });
232
270
 
233
- test("refusal beats the post-tool-empty nudge wording", async () => {
271
+ test("refusal after a visible reply this run → stop, no rewrite", async () => {
272
+ // GIVEN an earlier turn this run already delivered a real answer (text
273
+ // alongside a tool call), then the trailing turn refuses after the tool
274
+ // result. Rewriting here would stack an apology beneath the real answer.
275
+ const ctx = makeCtx({
276
+ messages: [userPrompt("do X"), priorVisibleTextTurn, priorToolUseTurn],
277
+ content: [],
278
+ stopReason: "refusal",
279
+ });
280
+
281
+ // WHEN the hook runs.
282
+ await postModelCall(ctx);
283
+
284
+ // THEN the refusal is left as-is — no fallback is stacked beneath the
285
+ // earlier reply.
286
+ expect(ctx.decision).toBe("stop");
287
+ expect(ctx.content).toEqual([]);
288
+ });
289
+
290
+ test("refusal beats the post-tool-empty nudge", async () => {
234
291
  // GIVEN conditions that would trip both the refusal branch and the
235
- // post-tool-empty branch.
292
+ // post-tool-empty branch (a prior tool-use turn with no visible text).
236
293
  const ctx = makeCtx({
237
294
  messages: [priorToolUseTurn],
238
- responseContent: [],
295
+ content: [],
239
296
  stopReason: "refusal",
240
297
  });
241
298
 
242
299
  // WHEN the hook runs.
243
- await stop(ctx);
300
+ await postModelCall(ctx);
244
301
 
245
- // THEN refusal wins because its wording is more accurate.
246
- expect(ctx.decision).toBe("continue");
247
- expect(ctx.messages.at(-1)).toEqual({
248
- role: "user",
249
- content: [{ type: "text", text: REFUSAL_NUDGE_TEXT }],
302
+ // THEN refusal wins: the turn content is rewritten to the fallback and the
303
+ // model is not nudged.
304
+ expect(ctx.decision).toBe("stop");
305
+ expect(ctx.content).toEqual([
306
+ { type: "text", text: REFUSAL_FALLBACK_TEXT },
307
+ ]);
308
+ expect(ctx.messages).toEqual([priorToolUseTurn]);
309
+ });
310
+
311
+ test("provider rejection is ignored — no nudge, no rewrite", async () => {
312
+ // GIVEN a rejection outcome (the call threw before any reply existed). The
313
+ // shape otherwise matches the post-tool-empty nudge case, but no response
314
+ // was produced, so this plugin defers to the recovery hooks.
315
+ const ctx = makeCtx({
316
+ messages: [priorToolUseTurn],
317
+ content: [],
318
+ error: new Error("provider rejected the request"),
319
+ });
320
+
321
+ // WHEN the hook runs.
322
+ await postModelCall(ctx);
323
+
324
+ // THEN it leaves the decision and history untouched — empty-response only
325
+ // acts on a finalized (successful) reply.
326
+ expect(ctx.decision).toBe("stop");
327
+ expect(ctx.messages).toEqual([priorToolUseTurn]);
328
+ });
329
+ });
330
+
331
+ // ─── One-shot retry bound ────────────────────────────────────────────────────
332
+
333
+ describe("empty-response post-model-call hook — one-shot bound", () => {
334
+ test("a second empty turn this run is not nudged again", async () => {
335
+ // GIVEN the hook already nudged once this run for the conversation.
336
+ const first = makeCtx({
337
+ conversationId: "conv-bound",
338
+ messages: [priorToolUseTurn],
339
+ content: [],
340
+ });
341
+ await postModelCall(first);
342
+ expect(first.decision).toBe("continue");
343
+
344
+ // WHEN a second empty-after-tools turn arrives for the same conversation.
345
+ const second = makeCtx({
346
+ conversationId: "conv-bound",
347
+ messages: [priorToolUseTurn],
348
+ content: [],
349
+ });
350
+ await postModelCall(second);
351
+
352
+ // THEN the hook lets the turn end rather than nudging again.
353
+ expect(second.decision).toBe("stop");
354
+ expect(second.messages).toEqual([priorToolUseTurn]);
355
+ });
356
+
357
+ test("the hook never clears its own bound — clearing is the stop hook's job", async () => {
358
+ // GIVEN the hook nudged once this run for the conversation.
359
+ const nudged = makeCtx({
360
+ conversationId: "conv-no-self-clear",
361
+ messages: [priorToolUseTurn],
362
+ content: [],
250
363
  });
364
+ await postModelCall(nudged);
365
+
366
+ // WHEN the re-queried call comes back as a provider rejection, then as a
367
+ // recovered visible-text reply — outcomes that end the turn.
368
+ await postModelCall(
369
+ makeCtx({
370
+ conversationId: "conv-no-self-clear",
371
+ messages: [priorToolUseTurn],
372
+ content: [],
373
+ error: new Error("provider rejected the request"),
374
+ }),
375
+ );
376
+ await postModelCall(
377
+ makeCtx({
378
+ conversationId: "conv-no-self-clear",
379
+ messages: [priorToolUseTurn],
380
+ content: [{ type: "text", text: "here is the summary" }],
381
+ }),
382
+ );
383
+
384
+ // THEN the bound stays marked — `post-model-call` only ever marks; the
385
+ // sibling `stop` hook is what clears it at the turn boundary.
386
+ expect(isEmptyResponseNudged("conv-no-self-clear")).toBe(true);
387
+ });
388
+ });
389
+
390
+ // ─── stop hook (terminal cleanup) ────────────────────────────────────────────
391
+
392
+ function makeStopCtx(overrides: Partial<StopContext> = {}): StopContext {
393
+ return {
394
+ conversationId: "conv-stop",
395
+ messages: [],
396
+ exitReason: "no_tool_calls",
397
+ logger: noopLogger,
398
+ ...overrides,
399
+ };
400
+ }
401
+
402
+ describe("empty-response stop hook — terminal cleanup", () => {
403
+ test("a terminal stop clears the nudge bound", async () => {
404
+ // GIVEN a conversation that nudged this run.
405
+ const conversationId = "conv-stop-clear";
406
+ markEmptyResponseNudged(conversationId);
407
+
408
+ // WHEN the terminal stop hook runs.
409
+ await stop(makeStopCtx({ conversationId }));
410
+
411
+ // THEN the bound is cleared so the next run nudges afresh.
412
+ expect(isEmptyResponseNudged(conversationId)).toBe(false);
413
+ });
414
+
415
+ test("clears the bound regardless of how the turn ended", async () => {
416
+ // GIVEN a conversation that nudged this run, ending on an abort rather than
417
+ // a finalized reply.
418
+ const conversationId = "conv-stop-abort";
419
+ markEmptyResponseNudged(conversationId);
420
+
421
+ // WHEN the terminal stop hook runs for that exit.
422
+ await stop(makeStopCtx({ conversationId, exitReason: "aborted_pre_call" }));
423
+
424
+ // THEN the bound is still cleared — `stop` is the definitive terminal, so
425
+ // the next run always nudges afresh.
426
+ expect(isEmptyResponseNudged(conversationId)).toBe(false);
251
427
  });
252
428
  });
253
429
 
254
430
  // ─── Via runHook + registry ──────────────────────────────────────────────────
255
431
 
256
- describe("empty-response stop hook — via runHook", () => {
432
+ describe("empty-response post-model-call hook — via runHook", () => {
257
433
  beforeEach(() => {
258
434
  resetPluginRegistryForTests();
259
435
  });
@@ -262,10 +438,10 @@ describe("empty-response stop hook — via runHook", () => {
262
438
  // GIVEN the default empty-response plugin is registered.
263
439
  registerPlugin(defaultEmptyResponsePlugin);
264
440
 
265
- // WHEN the stop chain runs over an empty-after-tools context.
266
- const result = await runHook<StopContext>(
267
- HOOKS.STOP,
268
- makeCtx({ messages: [priorToolUseTurn], responseContent: [] }),
441
+ // WHEN the post-model-call chain runs over an empty-after-tools context.
442
+ const result = await runHook<PostModelCallContext>(
443
+ HOOKS.POST_MODEL_CALL,
444
+ makeCtx({ messages: [priorToolUseTurn], content: [] }),
269
445
  );
270
446
 
271
447
  // THEN the chain settles on continue with the canonical nudge appended.
@@ -284,7 +460,7 @@ describe("empty-response stop hook — via runHook", () => {
284
460
  registerPlugin({
285
461
  manifest: { name: "force-stop", version: "0.0.1" },
286
462
  hooks: {
287
- stop: async (ctx: StopContext) => {
463
+ "post-model-call": async (ctx: PostModelCallContext) => {
288
464
  observedDecision = ctx.decision;
289
465
  ctx.decision = "stop";
290
466
  },
@@ -292,9 +468,9 @@ describe("empty-response stop hook — via runHook", () => {
292
468
  });
293
469
 
294
470
  // WHEN the chain runs over an empty-after-tools context.
295
- const result = await runHook<StopContext>(
296
- HOOKS.STOP,
297
- makeCtx({ messages: [priorToolUseTurn], responseContent: [] }),
471
+ const result = await runHook<PostModelCallContext>(
472
+ HOOKS.POST_MODEL_CALL,
473
+ makeCtx({ messages: [priorToolUseTurn], content: [] }),
298
474
  );
299
475
 
300
476
  // THEN the user hook saw the default's continue, and its override wins.
@@ -521,34 +521,3 @@ export default async function init(_ctx: unknown): Promise<void> {}
521
521
  expect(registeredNames()).not.toContain("hanging-init");
522
522
  });
523
523
  });
524
-
525
- describe("loadExternalPlugin — end-to-end @vellumai/simple-memory", () => {
526
- test("loads the in-tree simple-memory plugin", async () => {
527
- // Resolve the real on-disk plugin from the worktree. This double-acts
528
- // as the loader's contract test against the canonical Phase 0 plugin
529
- // and exercises the relative `../src/state.js` imports inside the
530
- // plugin's surface files.
531
- const pluginDir = join(
532
- import.meta.dir,
533
- "..",
534
- "..",
535
- "..",
536
- "experimental",
537
- "plugins",
538
- "simple-memory",
539
- );
540
-
541
- await loadExternalPlugin(pluginDir);
542
-
543
- const registered = getRegisteredPlugins().find(
544
- (p) => p.manifest.name === "simple-memory",
545
- );
546
- expect(registered).toBeDefined();
547
- expect(typeof registered?.hooks?.init).toBe("function");
548
- expect(typeof registered?.hooks?.shutdown).toBe("function");
549
- const toolNames = (registered?.tools ?? [])
550
- .map((t) => (t as { name: string }).name)
551
- .sort();
552
- expect(toolNames).toEqual(["recall", "remember"]);
553
- });
554
- });
@@ -11,8 +11,15 @@ const mockResolveSkillStates = mock(
11
11
  displayName: string;
12
12
  description: string;
13
13
  emoji?: string;
14
- source: "bundled" | "managed" | "workspace" | "extra" | "catalog";
14
+ source:
15
+ | "bundled"
16
+ | "managed"
17
+ | "workspace"
18
+ | "extra"
19
+ | "catalog"
20
+ | "plugin";
15
21
  directoryPath: string;
22
+ owner?: { kind: "skill" | "mcp" | "plugin" | "workspace"; id: string };
16
23
  };
17
24
  state: "enabled" | "disabled";
18
25
  }> => [],
@@ -313,4 +320,39 @@ describe("getSkill — skillssh audit enrichment", () => {
313
320
  // fetchSkillAudits was still called (just failed)
314
321
  expect(mockFetchSkillAudits).toHaveBeenCalledTimes(1);
315
322
  });
323
+
324
+ test("attributes a plugin-resident skill to its owning plugin on the detail response", async () => {
325
+ // GIVEN a skill shipped inside an installed plugin, resolved with a
326
+ // `plugin` source and a plugin owner descriptor
327
+ mockResolveSkillStates.mockReturnValue([
328
+ {
329
+ summary: {
330
+ id: "caveman",
331
+ displayName: "Caveman",
332
+ description: "Ultra-compressed communication mode",
333
+ source: "plugin" as const,
334
+ directoryPath: "/tmp/test-skills/plugins/caveman/skills/caveman",
335
+ owner: { kind: "plugin", id: "caveman" },
336
+ },
337
+ state: "enabled" as const,
338
+ },
339
+ ]);
340
+
341
+ // WHEN we fetch its detail
342
+ const result = await getSkill("caveman");
343
+
344
+ // THEN it resolves to the vellum origin (plugin skills are mapped to
345
+ // bundled/vellum for older clients) while preserving plugin attribution
346
+ // on the owner descriptor
347
+ expect("skill" in result).toBe(true);
348
+ if (!("skill" in result)) throw new Error("Expected skill response");
349
+
350
+ const detail = result.skill;
351
+ expect(detail.origin).toBe("vellum");
352
+ expect(detail.id).toBe("caveman");
353
+ // AND the owner identifies the plugin so detail consumers can attribute it
354
+ if (detail.origin === "vellum") {
355
+ expect(detail.owner).toEqual({ kind: "plugin", id: "caveman" });
356
+ }
357
+ });
316
358
  });
@@ -1332,6 +1332,97 @@ describe("routing invariant: callback buttons route through canonical primitive"
1332
1332
  });
1333
1333
  });
1334
1334
 
1335
+ // ===========================================================================
1336
+ // SECTION 8b: route-owned (directResolve) confirmations resolve via the
1337
+ // channel/guardian-action path too.
1338
+ //
1339
+ // The ACP spawn/steer approval gate (acp-routes.ts, ATL-822) registers
1340
+ // pending confirmations with a `directResolve` callback rather than a
1341
+ // Conversation.prompter. The tool_approval resolver must fire that callback
1342
+ // directly; otherwise a guardian approval submitted over a channel never
1343
+ // reaches the waiting route and the spawn/resume hangs until timeout.
1344
+ // ===========================================================================
1345
+
1346
+ describe("routing invariant: directResolve interactions resolve via guardian decisions", () => {
1347
+ beforeEach(() => resetTables());
1348
+
1349
+ function registerDirectResolveInteraction(
1350
+ requestId: string,
1351
+ conversationId: string,
1352
+ ): string[] {
1353
+ const decisions: string[] = [];
1354
+ pendingInteractions.register(requestId, {
1355
+ conversationId,
1356
+ kind: "confirmation",
1357
+ confirmationDetails: {
1358
+ toolName: "acp_spawn",
1359
+ input: { agent: "claude", task: "t", cwd: "/work" },
1360
+ riskLevel: "high",
1361
+ executionTarget: "host",
1362
+ allowlistOptions: [],
1363
+ scopeOptions: [],
1364
+ persistentDecisionsAllowed: false,
1365
+ },
1366
+ directResolve: (d) => decisions.push(d),
1367
+ });
1368
+ return decisions;
1369
+ }
1370
+
1371
+ test("channel approval fires directResolve('allow') with no live Conversation", async () => {
1372
+ const req = createCanonicalGuardianRequest({
1373
+ kind: "tool_approval",
1374
+ sourceType: "channel",
1375
+ conversationId: "conv-acp",
1376
+ guardianExternalUserId: "guardian-1",
1377
+ guardianPrincipalId: TEST_PRINCIPAL_ID,
1378
+ toolName: "acp_spawn",
1379
+ expiresAt: Date.now() + 60_000,
1380
+ });
1381
+ // Deliberately no _conversationMocks entry: the prompter path would have
1382
+ // failed with conversation_not_found, proving directResolve is what runs.
1383
+ const decisions = registerDirectResolveInteraction(req.id, "conv-acp");
1384
+
1385
+ const result = await routeGuardianReply(
1386
+ replyCtx({
1387
+ messageText: "",
1388
+ callbackData: `apr:${req.id}:approve_once`,
1389
+ conversationId: "conv-acp",
1390
+ }),
1391
+ );
1392
+
1393
+ expect(result.decisionApplied).toBe(true);
1394
+ expect(decisions).toEqual(["allow"]);
1395
+ expect(pendingInteractions.get(req.id)).toBeUndefined();
1396
+ expect(getCanonicalGuardianRequest(req.id)!.status).toBe("approved");
1397
+ });
1398
+
1399
+ test("channel rejection fires directResolve('deny')", async () => {
1400
+ const req = createCanonicalGuardianRequest({
1401
+ kind: "tool_approval",
1402
+ sourceType: "channel",
1403
+ conversationId: "conv-acp",
1404
+ guardianExternalUserId: "guardian-1",
1405
+ guardianPrincipalId: TEST_PRINCIPAL_ID,
1406
+ toolName: "acp_spawn",
1407
+ expiresAt: Date.now() + 60_000,
1408
+ });
1409
+ const decisions = registerDirectResolveInteraction(req.id, "conv-acp");
1410
+
1411
+ const result = await routeGuardianReply(
1412
+ replyCtx({
1413
+ messageText: "",
1414
+ callbackData: `apr:${req.id}:reject`,
1415
+ conversationId: "conv-acp",
1416
+ }),
1417
+ );
1418
+
1419
+ expect(result.decisionApplied).toBe(true);
1420
+ expect(decisions).toEqual(["deny"]);
1421
+ expect(pendingInteractions.get(req.id)).toBeUndefined();
1422
+ expect(getCanonicalGuardianRequest(req.id)!.status).toBe("denied");
1423
+ });
1424
+ });
1425
+
1335
1426
  // ===========================================================================
1336
1427
  // SECTION 9: Destination hints do not bypass principal binding for tool_approval
1337
1428
  // ===========================================================================