@vellumai/assistant 0.8.7-dev.202606052232.2ddc989 → 0.8.8

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 (262) hide show
  1. package/bun.lock +2 -2
  2. package/docs/plugins.md +832 -0
  3. package/examples/plugins/echo/README.md +60 -61
  4. package/examples/plugins/echo/package.json +2 -1
  5. package/examples/plugins/echo/register.ts +143 -0
  6. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +6 -7
  7. package/openapi.yaml +5 -15
  8. package/package.json +2 -2
  9. package/src/__tests__/agent-loop-exit-reason.test.ts +56 -3
  10. package/src/__tests__/anthropic-provider.test.ts +1 -1
  11. package/src/__tests__/app-control-flow.test.ts +1 -1
  12. package/src/__tests__/app-dir-path-guard.test.ts +0 -1
  13. package/src/__tests__/approval-routes-http.test.ts +1 -4
  14. package/src/__tests__/channel-approval-routes.test.ts +1 -1
  15. package/src/__tests__/channel-approvals.test.ts +1 -1
  16. package/src/__tests__/circuit-breaker-pipeline.test.ts +405 -0
  17. package/src/__tests__/compaction-pipeline.test.ts +210 -0
  18. package/src/__tests__/compaction-timeout-recovery.test.ts +251 -0
  19. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +3 -0
  20. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +3 -0
  21. package/src/__tests__/conversation-agent-loop-overflow.test.ts +7 -3
  22. package/src/__tests__/conversation-agent-loop.test.ts +39 -42
  23. package/src/__tests__/conversation-clean-command.test.ts +2 -5
  24. package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -4
  25. package/src/__tests__/conversation-runtime-assembly.test.ts +71 -140
  26. package/src/__tests__/conversation-runtime-workspace.test.ts +27 -108
  27. package/src/__tests__/conversation-starter-routes.test.ts +6 -14
  28. package/src/__tests__/conversation-workspace-cache-state.test.ts +16 -17
  29. package/src/__tests__/conversation-workspace-injection.test.ts +1 -61
  30. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +6 -7
  31. package/src/__tests__/db-acp-history.test.ts +0 -101
  32. package/src/__tests__/dynamic-page-surface.test.ts +0 -31
  33. package/src/__tests__/file-write-tool.test.ts +0 -63
  34. package/src/__tests__/gateway-only-guard.test.ts +2 -12
  35. package/src/__tests__/guardian-grant-minting.test.ts +1 -1
  36. package/src/__tests__/guardian-routing-invariants.test.ts +4 -2
  37. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
  38. package/src/__tests__/heartbeat-disk-pressure.test.ts +0 -1
  39. package/src/__tests__/heartbeat-service.test.ts +0 -1
  40. package/src/__tests__/host-app-control-routes.test.ts +1 -1
  41. package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
  42. package/src/__tests__/injector-background-turn.test.ts +1 -1
  43. package/src/__tests__/injector-chain.test.ts +6 -34
  44. package/src/__tests__/injector-disk-pressure.test.ts +34 -77
  45. package/src/__tests__/injector-document-comments.test.ts +1 -1
  46. package/src/__tests__/list-messages-hidden-metadata.test.ts +0 -38
  47. package/src/__tests__/memory-v2-static-injector.test.ts +1 -1
  48. package/src/__tests__/{overflow-reduction-loop.test.ts → overflow-reduce-pipeline.test.ts} +284 -64
  49. package/src/__tests__/pipeline-runner.test.ts +554 -0
  50. package/src/__tests__/plugin-api-shim.test.ts +6 -3
  51. package/src/__tests__/plugin-bootstrap.test.ts +23 -12
  52. package/src/__tests__/plugin-registry.test.ts +49 -3
  53. package/src/__tests__/plugin-types.test.ts +70 -0
  54. package/src/__tests__/reaction-persistence.test.ts +1 -1
  55. package/src/__tests__/send-endpoint-busy.test.ts +1 -4
  56. package/src/__tests__/skill-feature-flags-integration.test.ts +0 -33
  57. package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
  58. package/src/__tests__/subagent-fork-notifications.test.ts +3 -1
  59. package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
  60. package/src/__tests__/subagent-manager-notify.test.ts +3 -1
  61. package/src/__tests__/subagent-notify-parent.test.ts +3 -1
  62. package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
  63. package/src/__tests__/user-plugin-loader.test.ts +286 -54
  64. package/src/acp/__tests__/client-handler.test.ts +0 -40
  65. package/src/acp/__tests__/prepare-agent-env.test.ts +0 -137
  66. package/src/acp/__tests__/session-manager-persistence.test.ts +28 -95
  67. package/src/acp/agent-process.ts +1 -61
  68. package/src/acp/client-handler.ts +0 -31
  69. package/src/acp/prepare-agent-env.ts +29 -83
  70. package/src/acp/resolve-agent.test.ts +7 -320
  71. package/src/acp/resolve-agent.ts +18 -182
  72. package/src/acp/session-manager.ts +73 -495
  73. package/src/acp/types.ts +0 -8
  74. package/src/agent/compaction-circuit.ts +102 -60
  75. package/src/agent/loop.ts +59 -32
  76. package/src/api/responses/conversation-message.ts +1 -7
  77. package/src/approvals/guardian-request-resolvers.ts +1 -1
  78. package/src/background-wake/next-wake.ts +0 -1
  79. package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
  80. package/src/config/acp-defaults.test.ts +0 -10
  81. package/src/config/acp-defaults.ts +0 -6
  82. package/src/config/bundled-skills/acp/SKILL.md +31 -83
  83. package/src/config/bundled-skills/acp/TOOLS.json +4 -4
  84. package/src/config/bundled-skills/app-builder/SKILL.md +381 -224
  85. package/src/config/bundled-skills/app-builder/TOOLS.json +0 -29
  86. package/src/config/bundled-skills/document-editor/SKILL.md +23 -28
  87. package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
  88. package/src/config/bundled-tool-registry.ts +0 -2
  89. package/src/config/feature-flag-registry.json +5 -14
  90. package/src/config/schemas/heartbeat.ts +0 -9
  91. package/src/context/strip-injections.ts +2 -8
  92. package/src/context/window-manager.ts +1 -2
  93. package/src/daemon/conversation-agent-loop-handlers.ts +11 -0
  94. package/src/daemon/conversation-agent-loop.ts +279 -62
  95. package/src/daemon/conversation-runtime-assembly.ts +69 -106
  96. package/src/daemon/conversation-store.ts +90 -9
  97. package/src/daemon/conversation-workspace.ts +0 -17
  98. package/src/daemon/conversation.ts +6 -0
  99. package/src/daemon/external-plugins-bootstrap.ts +11 -11
  100. package/src/daemon/handlers/conversations.ts +1 -3
  101. package/src/daemon/handlers/skills.ts +1 -4
  102. package/src/daemon/lifecycle.ts +0 -21
  103. package/src/daemon/server.ts +0 -2
  104. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -3
  105. package/src/heartbeat/heartbeat-run-store.ts +1 -23
  106. package/src/heartbeat/heartbeat-service.ts +0 -26
  107. package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
  108. package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
  109. package/src/ipc/skill-routes/__tests__/memory.test.ts +0 -15
  110. package/src/ipc/skill-routes/memory.ts +2 -4
  111. package/src/memory/conversation-starter-checkpoints.ts +0 -1
  112. package/src/memory/db-init.ts +0 -2
  113. package/src/memory/job-handlers/conversation-starters.ts +2 -13
  114. package/src/memory/jobs-worker.ts +1 -1
  115. package/src/memory/migrations/index.ts +0 -1
  116. package/src/memory/schema/acp.ts +0 -4
  117. package/src/memory/v2/__tests__/consolidation-job.test.ts +3 -3
  118. package/src/memory/v2/consolidation-job.ts +4 -13
  119. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/assign.test.ts +4 -4
  120. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/live-integration.test.ts +4 -4
  121. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/maintain-job.test.ts +5 -5
  122. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/orchestrate.test.ts +3 -3
  123. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/reconcile.test.ts +2 -2
  124. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/render-injection.test.ts +1 -1
  125. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/router.test.ts +3 -3
  126. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/selection-log-store.test.ts +8 -8
  127. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/selector.test.ts +3 -3
  128. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/shadow-plugin.test.ts +12 -12
  129. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/assign.ts +5 -5
  130. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/capabilities.ts +2 -2
  131. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/maintain-job.ts +8 -8
  132. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/page-content.ts +2 -2
  133. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/provider-blocks.ts +1 -1
  134. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/reconcile.ts +3 -3
  135. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/render-injection.ts +1 -1
  136. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/router.ts +3 -3
  137. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/selection-log-store.ts +4 -4
  138. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/selector.ts +4 -4
  139. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/shadow-plugin.ts +90 -28
  140. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/tree.ts +1 -1
  141. package/src/plugin-api/index.ts +5 -0
  142. package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +93 -0
  143. package/src/plugins/defaults/{memory-v3-shadow → circuit-breaker}/package.json +2 -2
  144. package/src/plugins/defaults/circuit-breaker/register.ts +39 -0
  145. package/src/plugins/defaults/compaction/middlewares/compaction.ts +25 -0
  146. package/src/plugins/defaults/compaction/package.json +1 -1
  147. package/src/plugins/defaults/compaction/register.ts +19 -8
  148. package/src/plugins/defaults/compaction/terminal.ts +73 -0
  149. package/src/plugins/defaults/index.ts +5 -3
  150. package/src/plugins/defaults/{memory-retrieval/injectors.ts → injectors/register.ts} +7 -45
  151. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +7 -11
  152. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
  153. package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +126 -0
  154. package/src/plugins/defaults/overflow-reduce/package.json +15 -0
  155. package/src/plugins/defaults/overflow-reduce/register.ts +42 -0
  156. package/src/plugins/external-api.ts +2 -2
  157. package/src/plugins/pipeline.ts +293 -6
  158. package/src/plugins/registry.ts +37 -9
  159. package/src/plugins/types.ts +336 -32
  160. package/src/plugins/user-loader.ts +127 -30
  161. package/src/proactive-artifact/aux-message-injector.ts +1 -1
  162. package/src/proactive-artifact/job.test.ts +1 -1
  163. package/src/prompts/__tests__/system-prompt.test.ts +0 -6
  164. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +2 -4
  165. package/src/runtime/__tests__/agent-wake.test.ts +5 -5
  166. package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
  167. package/src/runtime/agent-wake.ts +3 -0
  168. package/src/runtime/assistant-event-hub.ts +1 -1
  169. package/src/runtime/channel-approvals.ts +1 -1
  170. package/src/runtime/interactive-ui.ts +1 -1
  171. package/src/runtime/routes/__tests__/acp-routes.test.ts +55 -283
  172. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
  173. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +4 -5
  174. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +1 -4
  175. package/src/runtime/routes/acp-routes.test.ts +25 -89
  176. package/src/runtime/routes/acp-routes.ts +29 -81
  177. package/src/runtime/routes/approval-routes.ts +1 -1
  178. package/src/runtime/routes/browser-routes.ts +1 -1
  179. package/src/runtime/routes/browser-tabs-routes.ts +10 -6
  180. package/src/runtime/routes/conversation-cli-routes.ts +1 -1
  181. package/src/runtime/routes/conversation-list-routes.ts +1 -1
  182. package/src/runtime/routes/conversation-query-routes.ts +1 -1
  183. package/src/runtime/routes/conversation-routes.ts +2 -15
  184. package/src/runtime/routes/conversation-starter-routes.ts +7 -13
  185. package/src/runtime/routes/conversations-import-routes.ts +7 -24
  186. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  187. package/src/runtime/routes/host-cu-routes.ts +1 -1
  188. package/src/runtime/routes/identity-routes.ts +3 -18
  189. package/src/runtime/routes/inbound-message-handler.ts +1 -1
  190. package/src/runtime/routes/memory-v3-routes.ts +6 -16
  191. package/src/runtime/routes/playground/helpers.ts +1 -1
  192. package/src/runtime/routes/surface-conversation-resolver.ts +3 -4
  193. package/src/runtime/routes/work-items-routes.ts +4 -2
  194. package/src/runtime/services/conversation-serializer.ts +1 -1
  195. package/src/signals/cancel.ts +4 -2
  196. package/src/subagent/manager.ts +5 -17
  197. package/src/tools/acp/list-agents.test.ts +1 -7
  198. package/src/tools/acp/spawn.test.ts +55 -158
  199. package/src/tools/acp/spawn.ts +72 -47
  200. package/src/tools/acp/steer.test.ts +8 -105
  201. package/src/tools/acp/steer.ts +17 -48
  202. package/src/tools/apps/executors.ts +8 -13
  203. package/src/tools/filesystem/write.ts +0 -34
  204. package/src/tools/subagent/spawn.ts +4 -2
  205. package/src/tools/ui-surface/definitions.ts +4 -25
  206. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +5 -4
  207. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +45 -69
  208. package/examples/plugins/echo/hooks/post-tool-use.ts +0 -18
  209. package/examples/plugins/echo/hooks/stop.ts +0 -16
  210. package/examples/plugins/echo/hooks/user-prompt-submit.ts +0 -18
  211. package/examples/plugins/echo/src/emit.ts +0 -19
  212. package/src/__tests__/compaction-circuit.test.ts +0 -258
  213. package/src/__tests__/compaction-direct.test.ts +0 -132
  214. package/src/__tests__/conversations-import-system-filter.test.ts +0 -101
  215. package/src/acp/__tests__/agent-process.test.ts +0 -161
  216. package/src/acp/__tests__/helpers/acp-history-db.ts +0 -82
  217. package/src/acp/__tests__/helpers/exec-file-stub.ts +0 -101
  218. package/src/acp/__tests__/session-manager-resume.test.ts +0 -736
  219. package/src/acp/auto-install.test.ts +0 -196
  220. package/src/acp/auto-install.ts +0 -177
  221. package/src/acp/feature-gate.test.ts +0 -48
  222. package/src/acp/feature-gate.ts +0 -34
  223. package/src/acp/resume-hint.ts +0 -25
  224. package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +0 -48
  225. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +0 -57
  226. package/src/config/bundled-skills/app-builder/references/SLIDES.md +0 -38
  227. package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -62
  228. package/src/daemon/conversation-registry.ts +0 -159
  229. package/src/daemon/overflow-reduction-loop.ts +0 -230
  230. package/src/memory/migrations/272-acp-session-history-cwd.ts +0 -36
  231. package/src/plugins/defaults/compaction/compact.ts +0 -59
  232. package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +0 -14
  233. package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +0 -19
  234. package/src/plugins/defaults/memory-v3-shadow/injector.ts +0 -75
  235. package/src/plugins/defaults/memory-v3-shadow/register.ts +0 -26
  236. package/src/tools/acp/context.ts +0 -20
  237. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/capabilities.test.ts +0 -0
  238. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/core.test.ts +0 -0
  239. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/fixtures/eval-turns.json +0 -0
  240. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/fixtures/live-turns.json +0 -0
  241. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/health.test.ts +0 -0
  242. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/needle.test.ts +0 -0
  243. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/provider-blocks.test.ts +0 -0
  244. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/snapshot.test.ts +0 -0
  245. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/tree.test.ts +0 -0
  246. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/types.test.ts +0 -0
  247. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/working-set-eviction.test.ts +0 -0
  248. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/working-set-skeleton.test.ts +0 -0
  249. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/core.ts +0 -0
  250. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/README.md +0 -0
  251. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/assignments.json +0 -0
  252. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/core.json +0 -0
  253. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/leaves/domain-a/topic-x.md +0 -0
  254. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/leaves/domain-a/topic-y.md +0 -0
  255. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/leaves/domain-b/topic-z.md +0 -0
  256. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/health.ts +0 -0
  257. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/llm-retry.ts +0 -0
  258. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/needle.ts +0 -0
  259. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/orchestrate.ts +0 -0
  260. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/snapshot.ts +0 -0
  261. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/types.ts +0 -0
  262. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/working-set.ts +0 -0
@@ -0,0 +1,251 @@
1
+ /**
2
+ * Tests for the compaction call-site recovery path added in JARVIS-587.
3
+ *
4
+ * When the `compaction` pipeline exceeds its 30s budget (manifest-wide
5
+ * `DEFAULT_TIMEOUTS.compaction`), the pipeline runner throws
6
+ * `PluginTimeoutError`. The three compaction call sites in
7
+ * `conversation-agent-loop.ts` (start-of-turn, mid-loop, emergency) catch
8
+ * that error, record the failure on the loop-held `CompactionCircuit`
9
+ * (`recordOutcome(turn, true, ...)`) so the circuit breaker counts it, and degrade
10
+ * gracefully.
11
+ *
12
+ * This file asserts the tight coupling between:
13
+ * (1) a `PluginTimeoutError`-driven failure and
14
+ * (2) the compaction circuit breaker's 3-strike threshold.
15
+ *
16
+ * The existing `circuit-breaker-pipeline.test.ts` already exercises the
17
+ * breaker's transitions end-to-end. These tests verify that our new
18
+ * catch-blocks feed the breaker the same `"failure"` outcome a normal
19
+ * summary-LLM throw would, and that repeated timeouts therefore trip the
20
+ * same 3-strike trip.
21
+ */
22
+
23
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
24
+
25
+ import {
26
+ type CircuitTurnInfo,
27
+ CompactionCircuit,
28
+ } from "../agent/compaction-circuit.js";
29
+ import type { ServerMessage } from "../daemon/message-protocol.js";
30
+ import type { TrustContext } from "../daemon/trust-context.js";
31
+ import { COMPACTION_CIRCUIT_FAILURE_THRESHOLD } from "../plugins/defaults/circuit-breaker/middlewares/circuitBreaker.js";
32
+ import { defaultCircuitBreakerPlugin } from "../plugins/defaults/circuit-breaker/register.js";
33
+ import { DEFAULT_TIMEOUTS, runPipeline } from "../plugins/pipeline.js";
34
+ import {
35
+ getMiddlewaresFor,
36
+ registerPlugin,
37
+ resetPluginRegistryForTests,
38
+ } from "../plugins/registry.js";
39
+ import {
40
+ type CompactionArgs,
41
+ type CompactionResult,
42
+ type Middleware,
43
+ PluginTimeoutError,
44
+ type TurnContext,
45
+ } from "../plugins/types.js";
46
+
47
+ // ─── Fixtures ───────────────────────────────────────────────────────────────
48
+
49
+ function makeCircuit(conversationId = "conv-timeout-test"): CompactionCircuit {
50
+ return new CompactionCircuit(conversationId);
51
+ }
52
+
53
+ const turn: CircuitTurnInfo = {
54
+ turnCount: 0,
55
+ trustContext: { sourceChannel: "vellum", trustClass: "guardian" },
56
+ };
57
+
58
+ const trust: TrustContext = {
59
+ sourceChannel: "vellum",
60
+ trustClass: "guardian",
61
+ };
62
+
63
+ function makeTurnCtx(conversationId: string): TurnContext {
64
+ return {
65
+ requestId: "req-timeout-test",
66
+ conversationId,
67
+ turnIndex: 0,
68
+ trust,
69
+ };
70
+ }
71
+
72
+ function collectEvents(): {
73
+ events: ServerMessage[];
74
+ onEvent: (msg: ServerMessage) => void;
75
+ } {
76
+ const events: ServerMessage[] = [];
77
+ return { events, onEvent: (msg) => events.push(msg) };
78
+ }
79
+
80
+ // ─── Tests ──────────────────────────────────────────────────────────────────
81
+
82
+ describe("compaction timeout recovery (JARVIS-587)", () => {
83
+ beforeEach(() => {
84
+ resetPluginRegistryForTests();
85
+ registerPlugin(defaultCircuitBreakerPlugin);
86
+ });
87
+
88
+ afterEach(() => {
89
+ resetPluginRegistryForTests();
90
+ });
91
+
92
+ test("runPipeline('compaction', ...) throws PluginTimeoutError on budget breach", async () => {
93
+ // Baseline: the compaction pipeline still surfaces PluginTimeoutError when
94
+ // its timer fires. This guards the outer race from silently swallowing
95
+ // the timeout when the inner call is aborted by our Part A wiring.
96
+ const hang: Middleware<CompactionArgs, CompactionResult> = async (
97
+ _args,
98
+ _next,
99
+ ) =>
100
+ new Promise<CompactionResult>(() => {
101
+ // intentionally never resolves
102
+ });
103
+
104
+ let caught: unknown;
105
+ try {
106
+ await runPipeline<CompactionArgs, CompactionResult>(
107
+ "compaction",
108
+ [hang],
109
+ async () => ({ compacted: false }) as unknown as CompactionResult,
110
+ { messages: [] as unknown, signal: undefined, options: undefined },
111
+ makeTurnCtx("conv-budget-breach"),
112
+ // Override the manifest timeout to keep the test fast.
113
+ 20,
114
+ );
115
+ } catch (err) {
116
+ caught = err;
117
+ }
118
+ expect(caught).toBeInstanceOf(PluginTimeoutError);
119
+ expect((caught as PluginTimeoutError).pipeline).toBe("compaction");
120
+ });
121
+
122
+ test("recordOutcome(failed=true) driven by PluginTimeoutError trips the breaker at the 3rd strike", async () => {
123
+ // Simulates the production sequence: each mid-loop compaction hits the
124
+ // pipeline's 30s ceiling, the catch path calls
125
+ // `circuit.recordOutcome(turn, true, onEvent)`. After three such catches
126
+ // the circuit breaker must be open — matching the same invariant the
127
+ // existing breaker test file exercises for normal summary-LLM throws.
128
+ const circuit = makeCircuit();
129
+ const { onEvent, events } = collectEvents();
130
+
131
+ // First two timeouts — circuit still closed.
132
+ await circuit.recordOutcome(turn, true, onEvent);
133
+ await circuit.recordOutcome(turn, true, onEvent);
134
+ expect(circuit.consecutiveCompactionFailures).toBe(2);
135
+ expect(circuit.compactionCircuitOpenUntil).toBeNull();
136
+ expect(events).toHaveLength(0);
137
+
138
+ // Third timeout — breaker trips and emits the canonical transition event.
139
+ await circuit.recordOutcome(turn, true, onEvent);
140
+ expect(circuit.consecutiveCompactionFailures).toBe(
141
+ COMPACTION_CIRCUIT_FAILURE_THRESHOLD,
142
+ );
143
+ expect(circuit.compactionCircuitOpenUntil).not.toBeNull();
144
+ expect(events).toHaveLength(1);
145
+ expect(events[0]).toEqual({
146
+ type: "compaction_circuit_open",
147
+ conversationId: circuit.conversationId,
148
+ reason: "3_consecutive_failures",
149
+ openUntil: circuit.compactionCircuitOpenUntil as number,
150
+ });
151
+ });
152
+
153
+ test("a successful compaction after two timeouts resets the counter", async () => {
154
+ // The recovery path doesn't interfere with the breaker's normal reset —
155
+ // once a compaction call eventually succeeds, the streak is broken and
156
+ // the next failure starts counting from 1.
157
+ const circuit = makeCircuit();
158
+ const { onEvent } = collectEvents();
159
+
160
+ await circuit.recordOutcome(turn, true, onEvent);
161
+ await circuit.recordOutcome(turn, true, onEvent);
162
+ expect(circuit.consecutiveCompactionFailures).toBe(2);
163
+
164
+ await circuit.recordOutcome(turn, false, onEvent);
165
+ expect(circuit.consecutiveCompactionFailures).toBe(0);
166
+ expect(circuit.compactionCircuitOpenUntil).toBeNull();
167
+
168
+ await circuit.recordOutcome(turn, true, onEvent);
169
+ expect(circuit.consecutiveCompactionFailures).toBe(1);
170
+ });
171
+
172
+ test("compaction call-site recovery remains defense-in-depth even when DEFAULT_TIMEOUTS.compaction is null", () => {
173
+ // At the time this PR landed, `DEFAULT_TIMEOUTS.compaction` was null
174
+ // (pipeline timeouts globally disabled — see #27608). That makes the
175
+ // call-site catch blocks unreachable in production right now, but the
176
+ // catch blocks still matter: any future reintroduction of a per-pipeline
177
+ // compaction timeout immediately benefits from circuit-breaker recording
178
+ // and graceful-degradation without needing to re-touch every call site.
179
+ //
180
+ // This test just documents the current value so a future change that
181
+ // reintroduces a timeout must also decide (intentionally) whether the
182
+ // recovery path should continue to fire.
183
+ const value = DEFAULT_TIMEOUTS.compaction;
184
+ expect(value === null || typeof value === "number").toBe(true);
185
+ });
186
+ });
187
+
188
+ describe("abort propagation end-to-end (Part A + updateSummary fallback)", () => {
189
+ beforeEach(() => {
190
+ resetPluginRegistryForTests();
191
+ registerPlugin(defaultCircuitBreakerPlugin);
192
+ });
193
+
194
+ afterEach(() => {
195
+ resetPluginRegistryForTests();
196
+ });
197
+
198
+ test("caller-provided signal is replaced with a linked signal that fires on timeout", async () => {
199
+ // Minimal proof that Part A actually wires the signal through the
200
+ // compaction pipeline: when the pipeline runner's timer fires, the
201
+ // signal seen by the inner terminal is aborted — allowing
202
+ // `updateSummary`'s try/catch around `provider.sendMessage` to trigger
203
+ // the local-fallback path instead of the call hanging indefinitely.
204
+ let observedSignal: AbortSignal | undefined;
205
+ const waitForAbort: Middleware<CompactionArgs, CompactionResult> = async (
206
+ args,
207
+ _next,
208
+ ) => {
209
+ observedSignal = args.signal;
210
+ return new Promise<CompactionResult>((_resolve, reject) => {
211
+ args.signal?.addEventListener("abort", () => {
212
+ reject(
213
+ Object.assign(new Error("aborted by signal"), {
214
+ name: "AbortError",
215
+ }),
216
+ );
217
+ });
218
+ });
219
+ };
220
+
221
+ const callerController = new AbortController();
222
+
223
+ let caught: unknown;
224
+ try {
225
+ await runPipeline<CompactionArgs, CompactionResult>(
226
+ "compaction",
227
+ [waitForAbort, ...getMiddlewaresFor("compaction")],
228
+ async () => ({ compacted: false }) as unknown as CompactionResult,
229
+ {
230
+ messages: [] as unknown,
231
+ signal: callerController.signal,
232
+ options: undefined,
233
+ },
234
+ makeTurnCtx("conv-abort-test"),
235
+ 20,
236
+ );
237
+ } catch (err) {
238
+ caught = err;
239
+ }
240
+
241
+ expect(caught).toBeInstanceOf(PluginTimeoutError);
242
+ expect(observedSignal).toBeDefined();
243
+ // The runner swaps the caller's signal for a linked signal — the two are
244
+ // distinct objects but both should end up aborted once the timer fires.
245
+ expect(observedSignal).not.toBe(callerController.signal);
246
+ expect(observedSignal!.aborted).toBe(true);
247
+ // Caller's own signal is untouched — pipeline timeout does not cascade
248
+ // outward onto the caller's controller.
249
+ expect(callerController.signal.aborted).toBe(false);
250
+ });
251
+ });
@@ -145,6 +145,8 @@ function makeCtx(
145
145
  surfaceActionRequestIds: new Set<string>(),
146
146
  currentTurnSurfaces: [],
147
147
  workingDir: "/tmp",
148
+ workspaceTopLevelContext: null,
149
+ workspaceTopLevelDirty: false,
148
150
  channelCapabilities: undefined,
149
151
  commandIntent: undefined,
150
152
  trustContext: undefined,
@@ -170,6 +172,7 @@ function makeCtx(
170
172
  hasNoClient: false,
171
173
  prompter: {} as Context["prompter"],
172
174
  queue: {} as Context["queue"],
175
+ refreshWorkspaceTopLevelContextIfNeeded: () => {},
173
176
  markWorkspaceTopLevelDirty: () => {},
174
177
  emitActivityState: () => {},
175
178
  getQueueDepth: () => 0,
@@ -435,6 +435,8 @@ function makeCtx(
435
435
  currentTurnSurfaces: [],
436
436
 
437
437
  workingDir: "/tmp",
438
+ workspaceTopLevelContext: null,
439
+ workspaceTopLevelDirty: false,
438
440
  channelCapabilities: undefined,
439
441
  commandIntent: undefined,
440
442
  trustContext: undefined,
@@ -471,6 +473,7 @@ function makeCtx(
471
473
  getWorkspaceGitService: () => ({ ensureInitialized: async () => {} }),
472
474
  commitTurnChanges: async () => {},
473
475
 
476
+ refreshWorkspaceTopLevelContextIfNeeded: () => {},
474
477
  markWorkspaceTopLevelDirty: () => {},
475
478
  emitActivityState: () => {},
476
479
  getQueueDepth: () => 0,
@@ -515,6 +515,8 @@ function makeCtx(
515
515
  currentTurnSurfaces: [],
516
516
 
517
517
  workingDir: "/tmp",
518
+ workspaceTopLevelContext: null,
519
+ workspaceTopLevelDirty: false,
518
520
  channelCapabilities: undefined,
519
521
  commandIntent: undefined,
520
522
  trustContext: undefined,
@@ -551,6 +553,7 @@ function makeCtx(
551
553
  getWorkspaceGitService: () => ({ ensureInitialized: async () => {} }),
552
554
  commitTurnChanges: async () => {},
553
555
 
556
+ refreshWorkspaceTopLevelContextIfNeeded: () => {},
554
557
  markWorkspaceTopLevelDirty: () => {},
555
558
  emitActivityState: () => {},
556
559
  getQueueDepth: () => 0,
@@ -648,9 +651,10 @@ beforeEach(() => {
648
651
  recordUsageMock.mockClear();
649
652
  setAgentLoopExitReasonOnLatestLogMock.mockClear();
650
653
  addMessageMock.mockClear();
651
- // Reset the plugin registry and re-register every default so the compaction
652
- // pipeline dispatches to the default middleware, which in turn hits the
653
- // mocked collaborators (`syncMessageToDisk`, …) these tests install.
654
+ // Reset the plugin registry and re-register every default so the
655
+ // orchestrator's pipelines (`overflowReduce`, `persistence`, …) dispatch to
656
+ // the default middleware, which in turn hits the mocked collaborators
657
+ // (`reduceContextOverflow`, `syncMessageToDisk`, …) these tests install.
654
658
  resetPluginRegistryAndRegisterDefaults();
655
659
  });
656
660
 
@@ -316,14 +316,12 @@ const buildUnifiedTurnContextBlockMock = mock(
316
316
  (options: Record<string, unknown>) =>
317
317
  `<turn_context>\ncurrent_time: ${String(options.timestamp)}\n</turn_context>`,
318
318
  );
319
- const defaultApplyRuntimeInjectionsImpl = async (
320
- msgs: Message[],
321
- _options?: unknown,
322
- ) => ({
323
- messages: msgs,
324
- blocks: { ...mockInjectionBlocks },
325
- });
326
- const applyRuntimeInjectionsMock = mock(defaultApplyRuntimeInjectionsImpl);
319
+ const applyRuntimeInjectionsMock = mock(
320
+ async (msgs: Message[], _options?: unknown) => ({
321
+ messages: msgs,
322
+ blocks: { ...mockInjectionBlocks },
323
+ }),
324
+ );
327
325
  let mockSlackChronologicalContext: {
328
326
  renderedMessages: Array<{
329
327
  message: Message;
@@ -638,6 +636,8 @@ function makeCtx(
638
636
  currentTurnSurfaces: [],
639
637
 
640
638
  workingDir: "/tmp",
639
+ workspaceTopLevelContext: null,
640
+ workspaceTopLevelDirty: false,
641
641
  channelCapabilities: undefined,
642
642
  commandIntent: undefined,
643
643
  trustContext: undefined,
@@ -674,6 +674,7 @@ function makeCtx(
674
674
  getWorkspaceGitService: () => ({ ensureInitialized: async () => {} }),
675
675
  commitTurnChanges: async () => {},
676
676
 
677
+ refreshWorkspaceTopLevelContextIfNeeded: () => {},
677
678
  markWorkspaceTopLevelDirty: () => {},
678
679
  emitActivityState: () => {},
679
680
  getQueueDepth: () => 0,
@@ -743,9 +744,6 @@ beforeEach(() => {
743
744
  setConversationHistoryStrippedAtMock.mockClear();
744
745
  setConversationHistoryStrippedAtMock.mockImplementation(() => {});
745
746
  applyRuntimeInjectionsMock.mockClear();
746
- applyRuntimeInjectionsMock.mockImplementation(
747
- defaultApplyRuntimeInjectionsImpl,
748
- );
749
747
  buildUnifiedTurnContextBlockMock.mockClear();
750
748
  resolveTurnTimezoneContextMock.mockClear();
751
749
  formatTurnTimestampMock.mockClear();
@@ -759,10 +757,11 @@ beforeEach(() => {
759
757
  projectAssistantMessageMock.mockClear();
760
758
  publishSyncInvalidationMock.mockClear();
761
759
  mockMessageById = null;
762
- // The compaction pipeline runs through the plugin registry; reset and
763
- // re-register every default so it dispatches to middleware backed by the
764
- // mocked collaborators these tests install (`syncMessageToDisk`, etc.)
765
- // instead of hitting the bare terminal.
760
+ // Orchestrator pipelines (overflowReduce, persistence, …) run through the
761
+ // plugin registry; reset and re-register every default so the pipelines
762
+ // dispatch to middleware backed by the mocked collaborators these tests
763
+ // install (`reduceContextOverflow`, `syncMessageToDisk`, etc.) instead of
764
+ // hitting the bare terminals.
766
765
  resetPluginRegistryAndRegisterDefaults();
767
766
  });
768
767
 
@@ -881,23 +880,7 @@ describe("session-agent-loop", () => {
881
880
  });
882
881
 
883
882
  describe("disk pressure injection context", () => {
884
- // The loop sets `ctx.diskPressureCleanupModeActive` for the duration of the
885
- // turn (the disk-pressure-warning injector reads it via the per-conversation
886
- // registry) and resets it in the turn-end cleanup path. Snapshot the flag at
887
- // each `applyRuntimeInjections` call so assertions observe its value while
888
- // injection runs, not the post-turn reset.
889
- function captureCleanupFlagDuringInjection(ctx: {
890
- diskPressureCleanupModeActive?: boolean;
891
- }): () => Array<boolean | undefined> {
892
- const observed: Array<boolean | undefined> = [];
893
- applyRuntimeInjectionsMock.mockImplementation(async (msgs: Message[]) => {
894
- observed.push(ctx.diskPressureCleanupModeActive);
895
- return { messages: msgs, blocks: { ...mockInjectionBlocks } };
896
- });
897
- return () => observed;
898
- }
899
-
900
- test("sets the cleanup-mode flag on the conversation for cleanup-mode turns", async () => {
883
+ test("passes cleanup context into runtime injections for cleanup-mode turns", async () => {
901
884
  mockDiskPressureDecision = {
902
885
  action: "allow-cleanup-mode",
903
886
  reason: "guardian",
@@ -920,7 +903,6 @@ describe("session-agent-loop", () => {
920
903
  trustClass: "guardian",
921
904
  } as AgentLoopConversationContext["trustContext"],
922
905
  });
923
- const cleanupFlagDuringInjection = captureCleanupFlagDuringInjection(ctx);
924
906
 
925
907
  await runAgentLoopImpl(ctx, "free up space", "msg-1", () => {});
926
908
 
@@ -939,16 +921,21 @@ describe("session-agent-loop", () => {
939
921
  },
940
922
  }),
941
923
  );
942
- expect(cleanupFlagDuringInjection()).toEqual([true]);
924
+ const firstInjectionOptions = applyRuntimeInjectionsMock.mock
925
+ .calls[0]![1] as {
926
+ diskPressureContext?: { cleanupModeActive: boolean } | null;
927
+ };
928
+ expect(firstInjectionOptions.diskPressureContext).toEqual({
929
+ cleanupModeActive: true,
930
+ });
943
931
  });
944
932
 
945
- test("sets the cleanup-mode flag on the conversation for local-owner turns", async () => {
933
+ test("passes cleanup context into runtime injections for local-owner turns", async () => {
946
934
  mockDiskPressureDecision = {
947
935
  action: "allow-cleanup-mode",
948
936
  reason: "local-owner",
949
937
  };
950
938
  const ctx = makeCtx();
951
- const cleanupFlagDuringInjection = captureCleanupFlagDuringInjection(ctx);
952
939
 
953
940
  await runAgentLoopImpl(ctx, "free up space", "msg-1", () => {});
954
941
 
@@ -960,10 +947,16 @@ describe("session-agent-loop", () => {
960
947
  trustContext: null,
961
948
  }),
962
949
  );
963
- expect(cleanupFlagDuringInjection()).toEqual([true]);
950
+ const firstInjectionOptions = applyRuntimeInjectionsMock.mock
951
+ .calls[0]![1] as {
952
+ diskPressureContext?: { cleanupModeActive: boolean } | null;
953
+ };
954
+ expect(firstInjectionOptions.diskPressureContext).toEqual({
955
+ cleanupModeActive: true,
956
+ });
964
957
  });
965
958
 
966
- test("keeps the cleanup-mode flag set across overflow recovery reinjection", async () => {
959
+ test("keeps cleanup context on overflow recovery reinjection", async () => {
967
960
  mockDiskPressureDecision = {
968
961
  action: "allow-cleanup-mode",
969
962
  reason: "guardian",
@@ -985,14 +978,18 @@ describe("session-agent-loop", () => {
985
978
  trustClass: "guardian",
986
979
  } as AgentLoopConversationContext["trustContext"],
987
980
  });
988
- const cleanupFlagDuringInjection = captureCleanupFlagDuringInjection(ctx);
989
981
 
990
982
  await runAgentLoopImpl(ctx, "free up space", "msg-1", () => {});
991
983
 
992
984
  expect(applyRuntimeInjectionsMock.mock.calls.length).toBeGreaterThan(1);
993
- const flags = cleanupFlagDuringInjection();
994
- expect(flags.length).toBeGreaterThan(1);
995
- expect(flags.every((flag) => flag === true)).toBe(true);
985
+ for (const call of applyRuntimeInjectionsMock.mock.calls) {
986
+ const options = call[1] as {
987
+ diskPressureContext?: { cleanupModeActive: boolean } | null;
988
+ };
989
+ expect(options.diskPressureContext).toEqual({
990
+ cleanupModeActive: true,
991
+ });
992
+ }
996
993
  });
997
994
 
998
995
  test("blocks policy-denied turns before runtime injection or model execution", async () => {
@@ -120,7 +120,7 @@ describe("stripInjectionsForCompaction preserves history shape", () => {
120
120
  expect(out[3]).toEqual(messages[3]);
121
121
  });
122
122
 
123
- test("strips <workspace> but leaves <turn_context> and <memory __injected> alone", () => {
123
+ test("leaves <turn_context>, <workspace>, and <memory __injected> alone", () => {
124
124
  const messages: Message[] = [
125
125
  {
126
126
  role: "user",
@@ -132,9 +132,6 @@ describe("stripInjectionsForCompaction preserves history shape", () => {
132
132
  },
133
133
  ];
134
134
  const out = stripInjectionsForCompaction(messages);
135
- expect(out[0].content).toEqual([
136
- { type: "text", text: "<turn_context>\nnow\n</turn_context>" },
137
- { type: "text", text: "<memory __injected>\nrecent\n</memory>" },
138
- ]);
135
+ expect(out[0].content).toEqual(messages[0].content);
139
136
  });
140
137
  });
@@ -486,10 +486,11 @@ describe("provider ordering error retry", () => {
486
486
  firstRunErrorMode = "ordering";
487
487
  maybeCompactCalls = [];
488
488
  forceCompactionEnabled = false;
489
- // The compaction pipeline runs through the plugin registry; re-register
490
- // every default so it has a middleware to dispatch to. Collaborators are
491
- // mocked above, so the default plugins' delegates go through the mocked
492
- // implementations.
489
+ // Orchestrator pipelines (`overflowReduce`, `persistence`, …) run through
490
+ // the plugin registry; re-register every default so each pipeline has a
491
+ // middleware to dispatch to. The `context-overflow-reducer` module itself
492
+ // (and other collaborators) are mocked above, so the default plugins'
493
+ // delegates go through the mocked implementations.
493
494
  resetPluginRegistryAndRegisterDefaults();
494
495
  });
495
496