@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
@@ -1,258 +0,0 @@
1
- /**
2
- * Tests for the `CompactionCircuit` class.
3
- *
4
- * The breaker logic lives as direct methods on the per-conversation
5
- * `CompactionCircuit` state container (`agent/compaction-circuit.ts`). These
6
- * tests assert the threshold (3 consecutive failures) and cooldown (1 hour)
7
- * exactly match the user-visible behavior:
8
- * (a) counter increments on each failure outcome
9
- * (b) circuit opens after exactly 3 consecutive failures
10
- * (c) successful compaction resets counter and clears the circuit
11
- * (d) isOpen() reflects state and cooldown expiry
12
- * (e) circuit re-opens after cooldown expiry when 3 more failures
13
- * accumulate (guards the stale-timestamp regression)
14
- * (f) isOpen() is query-only and never mutates the counter
15
- * (g) open→closed transition emits `compaction_circuit_closed` exactly once
16
- * (h) closed→closed transition emits nothing
17
- */
18
-
19
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
20
-
21
- import {
22
- COMPACTION_CIRCUIT_COOLDOWN_MS,
23
- COMPACTION_CIRCUIT_FAILURE_THRESHOLD,
24
- CompactionCircuit,
25
- } from "../agent/compaction-circuit.js";
26
- import type { CompactionCircuitEvent } from "../plugins/types.js";
27
-
28
- // ─── Fixtures ───────────────────────────────────────────────────────────────
29
-
30
- const CONVERSATION_ID = "conv-breaker-test";
31
-
32
- function collectEvents(): {
33
- events: CompactionCircuitEvent[];
34
- onEvent: (msg: CompactionCircuitEvent) => void;
35
- } {
36
- const events: CompactionCircuitEvent[] = [];
37
- return { events, onEvent: (msg) => events.push(msg) };
38
- }
39
-
40
- describe("CompactionCircuit", () => {
41
- let originalDateNow: () => number;
42
-
43
- beforeEach(() => {
44
- originalDateNow = Date.now;
45
- });
46
-
47
- afterEach(() => {
48
- Date.now = originalDateNow;
49
- });
50
-
51
- test("threshold and cooldown constants match the user-visible contract", () => {
52
- expect(COMPACTION_CIRCUIT_FAILURE_THRESHOLD).toBe(3);
53
- expect(COMPACTION_CIRCUIT_COOLDOWN_MS).toBe(60 * 60 * 1000);
54
- });
55
-
56
- test("(a) counter increments on each failure outcome", async () => {
57
- const circuit = new CompactionCircuit(CONVERSATION_ID);
58
- const { onEvent, events } = collectEvents();
59
-
60
- await circuit.recordOutcome(true, onEvent);
61
- expect(circuit.consecutiveCompactionFailures).toBe(1);
62
- expect(circuit.compactionCircuitOpenUntil).toBeNull();
63
- expect(events).toHaveLength(0);
64
-
65
- await circuit.recordOutcome(true, onEvent);
66
- expect(circuit.consecutiveCompactionFailures).toBe(2);
67
- expect(circuit.compactionCircuitOpenUntil).toBeNull();
68
- expect(events).toHaveLength(0);
69
- });
70
-
71
- test("(b) circuit opens after exactly 3 consecutive failures", async () => {
72
- const fixedNow = 1_700_000_000_000;
73
- Date.now = () => fixedNow;
74
-
75
- const circuit = new CompactionCircuit(CONVERSATION_ID);
76
- const { onEvent, events } = collectEvents();
77
-
78
- await circuit.recordOutcome(true, onEvent);
79
- await circuit.recordOutcome(true, onEvent);
80
- // Two failures — circuit still closed.
81
- expect(circuit.compactionCircuitOpenUntil).toBeNull();
82
- expect(events).toHaveLength(0);
83
-
84
- await circuit.recordOutcome(true, onEvent);
85
- // Third failure — circuit trips and fires the event exactly once.
86
- expect(circuit.consecutiveCompactionFailures).toBe(3);
87
- expect(circuit.compactionCircuitOpenUntil).toBe(fixedNow + 60 * 60 * 1000);
88
- expect(await circuit.isOpen()).toBe(true);
89
- expect(events).toHaveLength(1);
90
- expect(events[0]).toEqual({
91
- type: "compaction_circuit_open",
92
- conversationId: CONVERSATION_ID,
93
- reason: "3_consecutive_failures",
94
- openUntil: fixedNow + 60 * 60 * 1000,
95
- });
96
-
97
- // Further failures do not re-fire the event while the circuit is open.
98
- await circuit.recordOutcome(true, onEvent);
99
- expect(circuit.consecutiveCompactionFailures).toBe(4);
100
- expect(events).toHaveLength(1);
101
- });
102
-
103
- test("(c) successful outcome resets counter and clears circuit", async () => {
104
- const fixedNow = 1_700_000_000_000;
105
- Date.now = () => fixedNow;
106
-
107
- const circuit = new CompactionCircuit(CONVERSATION_ID);
108
- const { onEvent } = collectEvents();
109
-
110
- // Trip the breaker.
111
- await circuit.recordOutcome(true, onEvent);
112
- await circuit.recordOutcome(true, onEvent);
113
- await circuit.recordOutcome(true, onEvent);
114
- expect(circuit.compactionCircuitOpenUntil).not.toBeNull();
115
-
116
- // Success resets state.
117
- await circuit.recordOutcome(false, onEvent);
118
- expect(circuit.consecutiveCompactionFailures).toBe(0);
119
- expect(circuit.compactionCircuitOpenUntil).toBeNull();
120
- });
121
-
122
- test("(d) isOpen() reflects state and expiry", async () => {
123
- const fixedNow = 1_700_000_000_000;
124
- Date.now = () => fixedNow;
125
-
126
- const circuit = new CompactionCircuit(CONVERSATION_ID);
127
- const { onEvent } = collectEvents();
128
-
129
- // Fresh state: closed.
130
- expect(await circuit.isOpen()).toBe(false);
131
-
132
- // Trip the breaker.
133
- await circuit.recordOutcome(true, onEvent);
134
- await circuit.recordOutcome(true, onEvent);
135
- await circuit.recordOutcome(true, onEvent);
136
-
137
- // While open.
138
- expect(await circuit.isOpen()).toBe(true);
139
-
140
- // After cooldown expires the breaker reports closed again, even without
141
- // an explicit reset — the open-until timestamp is the only source of
142
- // truth for the gate.
143
- Date.now = () => fixedNow + 60 * 60 * 1000 + 1;
144
- expect(await circuit.isOpen()).toBe(false);
145
- });
146
-
147
- test("(e) circuit re-opens after cooldown expiry when 3 more failures accumulate", async () => {
148
- // Regression: opening the breaker a second time must not require
149
- // `compactionCircuitOpenUntil === null`. Once a cooldown expires, the
150
- // gate correctly reports "closed" but a stale past-timestamp stays on the
151
- // state, so the next 3-strike window must still trip a new cooldown. Any
152
- // expired timestamp is treated the same as null.
153
- const t0 = 1_700_000_000_000;
154
- Date.now = () => t0;
155
-
156
- const circuit = new CompactionCircuit(CONVERSATION_ID);
157
- const { onEvent, events } = collectEvents();
158
-
159
- // Trip the breaker the first time.
160
- await circuit.recordOutcome(true, onEvent);
161
- await circuit.recordOutcome(true, onEvent);
162
- await circuit.recordOutcome(true, onEvent);
163
- expect(circuit.compactionCircuitOpenUntil).toBe(t0 + 60 * 60 * 1000);
164
- expect(events).toHaveLength(1);
165
-
166
- // Advance past the cooldown window. Reset the counter — in production this
167
- // happens when a subsequent `maybeCompact` call succeeds after the
168
- // cooldown elapses, but the bug manifests even when the counter is reset:
169
- // the stale `compactionCircuitOpenUntil` is what breaks re-opening.
170
- const t1 = t0 + 60 * 60 * 1000 + 1;
171
- Date.now = () => t1;
172
- expect(await circuit.isOpen()).toBe(false);
173
- circuit.consecutiveCompactionFailures = 0;
174
- // `compactionCircuitOpenUntil` is deliberately left as the old timestamp
175
- // to reproduce the bug condition.
176
- expect(circuit.compactionCircuitOpenUntil).toBe(t0 + 60 * 60 * 1000);
177
-
178
- // Three more failures must trip a fresh cooldown even though the old
179
- // timestamp is still set.
180
- await circuit.recordOutcome(true, onEvent);
181
- await circuit.recordOutcome(true, onEvent);
182
- await circuit.recordOutcome(true, onEvent);
183
- expect(circuit.consecutiveCompactionFailures).toBe(3);
184
- expect(circuit.compactionCircuitOpenUntil).toBe(t1 + 60 * 60 * 1000);
185
- expect(events).toHaveLength(2);
186
- expect(events[1]).toEqual({
187
- type: "compaction_circuit_open",
188
- conversationId: CONVERSATION_ID,
189
- reason: "3_consecutive_failures",
190
- openUntil: t1 + 60 * 60 * 1000,
191
- });
192
- });
193
-
194
- test("(f) isOpen() is query-only and never mutates the counter", async () => {
195
- // `maybeCompact()` early-return paths skip `recordOutcome` entirely and
196
- // only gate on `isOpen()`. The query must never touch the 3-strike
197
- // counter so those early returns can't silently reset it.
198
- const circuit = new CompactionCircuit(CONVERSATION_ID);
199
- const { onEvent } = collectEvents();
200
-
201
- await circuit.recordOutcome(true, onEvent);
202
- await circuit.recordOutcome(true, onEvent);
203
- expect(circuit.consecutiveCompactionFailures).toBe(2);
204
-
205
- // Query-only — must NOT touch the counter.
206
- expect(await circuit.isOpen()).toBe(false);
207
- expect(circuit.consecutiveCompactionFailures).toBe(2);
208
-
209
- // A third real failure then trips the breaker as expected.
210
- await circuit.recordOutcome(true, onEvent);
211
- expect(circuit.consecutiveCompactionFailures).toBe(3);
212
- expect(circuit.compactionCircuitOpenUntil).not.toBeNull();
213
- });
214
-
215
- test("(g) open→closed transition emits compaction_circuit_closed exactly once", async () => {
216
- // Regression: the reset branch must notify the client on open→closed.
217
- // Otherwise the Swift banner set from `compaction_circuit_open` stays
218
- // visible until the original `openUntil` deadline (up to 1h),
219
- // misrepresenting the live state.
220
- const fixedNow = 1_700_000_000_000;
221
- Date.now = () => fixedNow;
222
-
223
- const circuit = new CompactionCircuit(CONVERSATION_ID);
224
- const { onEvent, events } = collectEvents();
225
-
226
- // Force the circuit into the open state directly — the emitted-event
227
- // transition logic is what we're testing, not the tripping path.
228
- circuit.compactionCircuitOpenUntil = fixedNow + 60 * 60 * 1000;
229
- circuit.consecutiveCompactionFailures = 3;
230
-
231
- await circuit.recordOutcome(false, onEvent);
232
-
233
- expect(circuit.consecutiveCompactionFailures).toBe(0);
234
- expect(circuit.compactionCircuitOpenUntil).toBeNull();
235
- expect(events).toHaveLength(1);
236
- expect(events[0]).toEqual({
237
- type: "compaction_circuit_closed",
238
- conversationId: CONVERSATION_ID,
239
- });
240
- });
241
-
242
- test("(h) successful outcome against an already-closed circuit emits no event", async () => {
243
- // Emitting `compaction_circuit_closed` on every successful compaction
244
- // would spam the client (the breaker is closed in the common case).
245
- // Only the open→closed transition is meaningful.
246
- const circuit = new CompactionCircuit(CONVERSATION_ID);
247
- const { onEvent, events } = collectEvents();
248
-
249
- expect(circuit.compactionCircuitOpenUntil).toBeNull();
250
- await circuit.recordOutcome(false, onEvent);
251
- expect(circuit.compactionCircuitOpenUntil).toBeNull();
252
- expect(events).toHaveLength(0);
253
-
254
- // A second successful outcome while still closed — still no event.
255
- await circuit.recordOutcome(false, onEvent);
256
- expect(events).toHaveLength(0);
257
- });
258
- });
@@ -1,132 +0,0 @@
1
- /**
2
- * Tests for the default compaction plugin (`defaultCompact`).
3
- *
4
- * The agent loop calls {@link defaultCompact} directly with a
5
- * {@link CompactionContext} rather than routing through a middleware pipeline.
6
- * These tests assert that the default implementation delegates to the
7
- * supplied {@link ContextWindowManager} and forwards the request's
8
- * conversational options verbatim. The orchestrator integration path
9
- * (conversation-agent-loop) is exercised by
10
- * `conversation-agent-loop-overflow.test.ts`.
11
- */
12
-
13
- import { describe, expect, test } from "bun:test";
14
-
15
- import {
16
- type CompactionContext,
17
- defaultCompact,
18
- } from "../plugins/defaults/compaction/compact.js";
19
-
20
- type ContextWindowResultShape = {
21
- compacted: boolean;
22
- summaryText: string;
23
- messages: unknown[];
24
- previousEstimatedInputTokens: number;
25
- estimatedInputTokens: number;
26
- maxInputTokens: number;
27
- thresholdTokens: number;
28
- compactedMessages: number;
29
- compactedPersistedMessages: number;
30
- summaryCalls: number;
31
- summaryInputTokens: number;
32
- summaryOutputTokens: number;
33
- summaryModel: string;
34
- reason?: string;
35
- };
36
-
37
- function makeResult(
38
- overrides: Partial<ContextWindowResultShape> = {},
39
- ): ContextWindowResultShape {
40
- return {
41
- compacted: true,
42
- summaryText: "default-summary",
43
- messages: [],
44
- previousEstimatedInputTokens: 1000,
45
- estimatedInputTokens: 100,
46
- maxInputTokens: 100000,
47
- thresholdTokens: 80000,
48
- compactedMessages: 3,
49
- compactedPersistedMessages: 3,
50
- summaryCalls: 1,
51
- summaryInputTokens: 500,
52
- summaryOutputTokens: 120,
53
- summaryModel: "default-model",
54
- ...overrides,
55
- };
56
- }
57
-
58
- function makeManager(result: ContextWindowResultShape) {
59
- const observed: {
60
- messages: unknown;
61
- signal: unknown;
62
- options: unknown;
63
- }[] = [];
64
- const manager = {
65
- maybeCompact: async (
66
- messages: unknown,
67
- signal: unknown,
68
- options: unknown,
69
- ) => {
70
- observed.push({ messages, signal, options });
71
- return result;
72
- },
73
- } as unknown as CompactionContext["manager"];
74
- return { manager, observed };
75
- }
76
-
77
- describe("defaultCompact", () => {
78
- test("delegates to the manager and returns its result unchanged", async () => {
79
- // GIVEN a manager whose maybeCompact records its arguments
80
- const expected = makeResult({
81
- summaryText: "manager-summary",
82
- compactedMessages: 7,
83
- });
84
- const { manager, observed } = makeManager(expected);
85
- const messages = [{ role: "user", content: "hi" }] as never;
86
- const signal = new AbortController().signal;
87
-
88
- // WHEN defaultCompact runs with those messages and a signal
89
- const result = (await defaultCompact({
90
- manager,
91
- messages,
92
- signal,
93
- })) as unknown as ContextWindowResultShape;
94
-
95
- // THEN the manager saw the same messages and signal
96
- expect(observed).toHaveLength(1);
97
- expect(observed[0]!.messages).toBe(messages);
98
- expect(observed[0]!.signal).toBe(signal);
99
-
100
- // AND the returned result is the manager's object, unmodified
101
- expect(result).toBe(expected);
102
- expect(result.summaryText).toBe("manager-summary");
103
- expect(result.compactedMessages).toBe(7);
104
- });
105
-
106
- test("forwards the request's compaction options to the manager", async () => {
107
- // GIVEN a manager and a fully-populated compaction context
108
- const { manager, observed } = makeManager(makeResult());
109
-
110
- // WHEN defaultCompact runs with force/profile/trust options
111
- await defaultCompact({
112
- manager,
113
- messages: [] as never,
114
- force: true,
115
- overrideProfile: "fast-profile",
116
- precomputedEstimate: 1234,
117
- minKeepRecentUserTurns: 0,
118
- actorTrustClass: "guardian",
119
- });
120
-
121
- // THEN the manager received exactly those options, and the manager,
122
- // messages, and signal are not leaked into the options bag
123
- expect(observed).toHaveLength(1);
124
- expect(observed[0]!.options).toEqual({
125
- force: true,
126
- overrideProfile: "fast-profile",
127
- precomputedEstimate: 1234,
128
- minKeepRecentUserTurns: 0,
129
- actorTrustClass: "guardian",
130
- });
131
- });
132
- });
@@ -1,101 +0,0 @@
1
- /**
2
- * Tests that the conversations import route never persists non-renderable
3
- * roles. The messages store is UI-facing (`ConversationMessage`), so an
4
- * imported export carrying agent-context `system` rows must land only its
5
- * `user`/`assistant` turns — the `system` rows are dropped, not persisted.
6
- */
7
-
8
- import { beforeEach, describe, expect, mock, test } from "bun:test";
9
-
10
- mock.module("../util/logger.js", () => ({
11
- getLogger: () =>
12
- new Proxy({} as Record<string, unknown>, {
13
- get: () => () => {},
14
- }),
15
- }));
16
-
17
- mock.module("../config/loader.js", () => ({
18
- getConfig: () => ({
19
- ui: {},
20
- model: "test",
21
- provider: "test",
22
- memory: { enabled: false },
23
- rateLimit: { maxRequestsPerMinute: 0 },
24
- }),
25
- }));
26
-
27
- mock.module("../memory/indexer.js", () => ({
28
- indexMessageNow: async () => {},
29
- }));
30
-
31
- import { getMessages } from "../memory/conversation-crud.js";
32
- import { getDb } from "../memory/db-connection.js";
33
- import { initializeDb } from "../memory/db-init.js";
34
- import { conversations, messages } from "../memory/schema.js";
35
- import { ROUTES } from "../runtime/routes/conversations-import-routes.js";
36
- import type { RouteHandlerArgs } from "../runtime/routes/types.js";
37
-
38
- initializeDb();
39
-
40
- function resetTables() {
41
- const db = getDb();
42
- db.run("DELETE FROM message_attachments");
43
- db.run("DELETE FROM attachments");
44
- db.run("DELETE FROM messages");
45
- db.run("DELETE FROM conversation_keys");
46
- db.run("DELETE FROM conversations");
47
- }
48
-
49
- const importHandler = ROUTES.find(
50
- (r) => r.operationId === "conversations_import",
51
- )!.handler;
52
-
53
- describe("conversations import system-row filtering", () => {
54
- beforeEach(resetTables);
55
-
56
- test("imports renderable turns but drops system rows", async () => {
57
- // GIVEN an export whose conversation sandwiches a system row between two
58
- // renderable turns (e.g. agent-context scaffolding an export carried)
59
- const body = {
60
- conversations: [
61
- {
62
- sourceKey: "src-1",
63
- title: "Imported chat",
64
- messages: [
65
- { role: "user", content: "first visible" },
66
- { role: "system", content: "agent-context scaffolding" },
67
- { role: "assistant", content: "second visible" },
68
- ],
69
- },
70
- ],
71
- };
72
-
73
- // WHEN the conversation is imported
74
- const result = (await importHandler({
75
- body,
76
- } as unknown as RouteHandlerArgs)) as {
77
- ok: boolean;
78
- imported: number;
79
- messages: number;
80
- };
81
-
82
- // THEN the import succeeds and only counts the renderable turns
83
- expect(result.ok).toBe(true);
84
- expect(result.imported).toBe(1);
85
- expect(result.messages).toBe(2);
86
-
87
- // AND the persisted rows are exactly the user/assistant turns, never the
88
- // system scaffolding
89
- const db = getDb();
90
- const conv = db.select().from(conversations).all()[0];
91
- const rows = getMessages(conv.id);
92
- expect(rows.map((m) => m.role)).toEqual(["user", "assistant"]);
93
- expect(
94
- db
95
- .select()
96
- .from(messages)
97
- .all()
98
- .some((m) => m.role === "system"),
99
- ).toBe(false);
100
- });
101
- });
@@ -1,161 +0,0 @@
1
- /**
2
- * Unit tests for AcpAgentProcess capability getters.
3
- *
4
- * The getters reflect the InitializeResponse captured during initialize();
5
- * before that resolves (or after kill()) they must report false. The
6
- * connection is stubbed directly so no child process is spawned.
7
- */
8
-
9
- import { describe, expect, test } from "bun:test";
10
-
11
- import type { InitializeResponse } from "@agentclientprotocol/sdk";
12
-
13
- import { AcpAgentProcess } from "../agent-process.js";
14
-
15
- function makeProcess(): AcpAgentProcess {
16
- return new AcpAgentProcess(
17
- "test-agent",
18
- { command: "echo", args: [] },
19
- () => {
20
- throw new Error("client factory should not be called in this test");
21
- },
22
- );
23
- }
24
-
25
- /** Injects a stub connection whose initialize() resolves with `response`. */
26
- function stubConnection(
27
- proc: AcpAgentProcess,
28
- response: InitializeResponse,
29
- ): void {
30
- (
31
- proc as unknown as {
32
- connection: { initialize: () => Promise<InitializeResponse> };
33
- }
34
- ).connection = {
35
- initialize: () => Promise.resolve(response),
36
- };
37
- }
38
-
39
- describe("AcpAgentProcess capability getters", () => {
40
- test("both getters return false before initialize() resolves", () => {
41
- const proc = makeProcess();
42
- expect(proc.supportsLoadSession).toBe(false);
43
- expect(proc.supportsSessionResume).toBe(false);
44
- });
45
-
46
- test("supportsLoadSession reflects agentCapabilities.loadSession", async () => {
47
- const proc = makeProcess();
48
- stubConnection(proc, {
49
- protocolVersion: 1,
50
- agentCapabilities: { loadSession: true },
51
- });
52
-
53
- await proc.initialize();
54
-
55
- expect(proc.supportsLoadSession).toBe(true);
56
- expect(proc.supportsSessionResume).toBe(false);
57
- });
58
-
59
- test("supportsSessionResume reflects agentCapabilities.sessionCapabilities.resume", async () => {
60
- const proc = makeProcess();
61
- stubConnection(proc, {
62
- protocolVersion: 1,
63
- agentCapabilities: { sessionCapabilities: { resume: {} } },
64
- });
65
-
66
- await proc.initialize();
67
-
68
- expect(proc.supportsSessionResume).toBe(true);
69
- expect(proc.supportsLoadSession).toBe(false);
70
- });
71
-
72
- test("both getters return false when the agent advertises no capabilities", async () => {
73
- const proc = makeProcess();
74
- stubConnection(proc, { protocolVersion: 1 });
75
-
76
- await proc.initialize();
77
-
78
- expect(proc.supportsLoadSession).toBe(false);
79
- expect(proc.supportsSessionResume).toBe(false);
80
- });
81
-
82
- test("kill() clears the captured initialize response", async () => {
83
- const proc = makeProcess();
84
- stubConnection(proc, {
85
- protocolVersion: 1,
86
- agentCapabilities: {
87
- loadSession: true,
88
- sessionCapabilities: { resume: {} },
89
- },
90
- });
91
-
92
- await proc.initialize();
93
- expect(proc.supportsLoadSession).toBe(true);
94
- expect(proc.supportsSessionResume).toBe(true);
95
-
96
- proc.kill();
97
-
98
- expect(proc.supportsLoadSession).toBe(false);
99
- expect(proc.supportsSessionResume).toBe(false);
100
- });
101
- });
102
-
103
- describe("AcpAgentProcess loadSession/resumeSession", () => {
104
- /** Injects a stub connection that records loadSession/resumeSession params. */
105
- function stubSessionConnection(proc: AcpAgentProcess): {
106
- loadCalls: unknown[];
107
- resumeCalls: unknown[];
108
- } {
109
- const loadCalls: unknown[] = [];
110
- const resumeCalls: unknown[] = [];
111
- (proc as unknown as { connection: unknown }).connection = {
112
- loadSession: (params: unknown) => {
113
- loadCalls.push(params);
114
- return Promise.resolve({});
115
- },
116
- resumeSession: (params: unknown) => {
117
- resumeCalls.push(params);
118
- return Promise.resolve({});
119
- },
120
- };
121
- return { loadCalls, resumeCalls };
122
- }
123
-
124
- test("loadSession forwards { sessionId, cwd, mcpServers: [] } to the connection", async () => {
125
- const proc = makeProcess();
126
- const { loadCalls } = stubSessionConnection(proc);
127
-
128
- await proc.loadSession("session-1", "/tmp/project");
129
-
130
- expect(loadCalls).toEqual([
131
- { sessionId: "session-1", cwd: "/tmp/project", mcpServers: [] },
132
- ]);
133
- });
134
-
135
- test("resumeSession forwards { sessionId, cwd, mcpServers: [] } to the connection", async () => {
136
- const proc = makeProcess();
137
- const { resumeCalls } = stubSessionConnection(proc);
138
-
139
- await proc.resumeSession("session-2", "/tmp/project");
140
-
141
- expect(resumeCalls).toEqual([
142
- { sessionId: "session-2", cwd: "/tmp/project", mcpServers: [] },
143
- ]);
144
- });
145
-
146
- test("loadSession throws when the process is not spawned", async () => {
147
- const proc = makeProcess();
148
-
149
- await expect(proc.loadSession("session-1", "/tmp/project")).rejects.toThrow(
150
- 'ACP agent "test-agent" is not spawned',
151
- );
152
- });
153
-
154
- test("resumeSession throws when the process is not spawned", async () => {
155
- const proc = makeProcess();
156
-
157
- await expect(
158
- proc.resumeSession("session-1", "/tmp/project"),
159
- ).rejects.toThrow('ACP agent "test-agent" is not spawned');
160
- });
161
- });