@vellumai/assistant 0.8.9-staging.2 → 0.8.9-staging.4

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 (196) hide show
  1. package/docs/activation-funnel-telemetry.md +310 -0
  2. package/openapi.yaml +16 -115
  3. package/package.json +1 -1
  4. package/src/__tests__/activation-early-marking.test.ts +120 -0
  5. package/src/__tests__/agent-loop-output-hooks.test.ts +13 -13
  6. package/src/__tests__/anthropic-provider.test.ts +23 -8
  7. package/src/__tests__/approval-cascade.test.ts +1 -1
  8. package/src/__tests__/compaction-direct.test.ts +32 -18
  9. package/src/__tests__/compaction-events.test.ts +2 -2
  10. package/src/__tests__/compaction.benchmark.test.ts +1 -1
  11. package/src/__tests__/context-overflow-reducer.test.ts +5 -5
  12. package/src/__tests__/context-window-manager-compact-retry.test.ts +121 -15
  13. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  14. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
  15. package/src/__tests__/conversation-error.test.ts +15 -1
  16. package/src/__tests__/conversation-history-web-search.test.ts +5 -0
  17. package/src/__tests__/conversation-media-retry.test.ts +1 -1
  18. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +40 -0
  19. package/src/__tests__/conversation-process-callsite.test.ts +1 -1
  20. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
  21. package/src/__tests__/conversation-queue.test.ts +1 -1
  22. package/src/__tests__/conversation-runtime-assembly.test.ts +71 -0
  23. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  24. package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
  25. package/src/__tests__/conversation-speed-override.test.ts +1 -1
  26. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +395 -0
  27. package/src/__tests__/conversation-surfaces-app-control.test.ts +44 -0
  28. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +73 -4
  29. package/src/__tests__/conversation-undo.test.ts +2 -2
  30. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
  31. package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
  32. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  33. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  34. package/src/__tests__/cu-unified-flow.test.ts +36 -0
  35. package/src/__tests__/history-repair-hook.test.ts +2 -0
  36. package/src/__tests__/llm-resolver.test.ts +73 -0
  37. package/src/__tests__/memory-retrieval-hook.test.ts +1 -2
  38. package/src/__tests__/persist-unsendable-image-downscale.test.ts +145 -0
  39. package/src/__tests__/persist-unsendable-image.test.ts +97 -1
  40. package/src/__tests__/plugin-external-api.test.ts +68 -0
  41. package/src/__tests__/post-turn-tool-result-truncation.test.ts +69 -0
  42. package/src/__tests__/published-app-updater.test.ts +138 -0
  43. package/src/__tests__/skill-feature-flags-integration.test.ts +5 -7
  44. package/src/__tests__/title-generate-hook.test.ts +2 -0
  45. package/src/__tests__/web-fetch.test.ts +45 -0
  46. package/src/acp/__tests__/helpers/acp-config-stub.ts +0 -2
  47. package/src/acp/resolve-agent.test.ts +0 -56
  48. package/src/acp/resolve-agent.ts +10 -38
  49. package/src/agent/loop.ts +13 -27
  50. package/src/api/responses/memory-v3-selection-log.ts +19 -10
  51. package/src/cli/commands/__tests__/memory-v3.test.ts +191 -210
  52. package/src/cli/commands/memory-v3.ts +57 -199
  53. package/src/cli/lib/__tests__/install-from-github.test.ts +232 -29
  54. package/src/cli/lib/__tests__/plugin-details.test.ts +28 -19
  55. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +57 -7
  56. package/src/cli/lib/__tests__/search-plugins.test.ts +17 -10
  57. package/src/cli/lib/install-from-github.ts +258 -41
  58. package/src/cli/lib/plugin-details.ts +20 -13
  59. package/src/cli/lib/plugin-marketplace.ts +23 -5
  60. package/src/cli/lib/search-plugins.ts +14 -8
  61. package/src/config/acp-defaults.ts +3 -3
  62. package/src/config/acp-schema.ts +1 -7
  63. package/src/config/bundled-skills/acp/SKILL.md +4 -17
  64. package/src/config/bundled-skills/acp/TOOLS.json +2 -2
  65. package/src/config/call-site-defaults.ts +0 -1
  66. package/src/config/feature-flag-registry.json +3 -18
  67. package/src/config/llm-resolver.ts +39 -7
  68. package/src/config/schemas/__tests__/memory-v3.test.ts +25 -9
  69. package/src/config/schemas/call-site-catalog.ts +0 -7
  70. package/src/config/schemas/llm.ts +17 -1
  71. package/src/config/schemas/memory-v3.ts +58 -8
  72. package/src/config/seed-inference-profiles.ts +18 -0
  73. package/src/context/post-turn-tool-result-truncation.ts +39 -1
  74. package/src/daemon/conversation-agent-loop-handlers.ts +8 -1
  75. package/src/daemon/conversation-agent-loop.ts +87 -95
  76. package/src/daemon/conversation-error.ts +31 -4
  77. package/src/daemon/conversation-history.ts +1 -1
  78. package/src/daemon/conversation-media-retry.ts +19 -6
  79. package/src/daemon/conversation-messaging.ts +17 -0
  80. package/src/daemon/conversation-process.ts +14 -5
  81. package/src/daemon/conversation-queue-manager.ts +8 -0
  82. package/src/daemon/conversation-runtime-assembly.ts +37 -1
  83. package/src/daemon/conversation-surfaces.ts +141 -3
  84. package/src/daemon/conversation.ts +48 -13
  85. package/src/daemon/external-plugins-bootstrap.ts +8 -3
  86. package/src/daemon/persist-unsendable-image.ts +62 -25
  87. package/src/daemon/process-message.ts +1 -1
  88. package/src/daemon/tool-side-effects.ts +15 -0
  89. package/src/memory/__tests__/activation-session-store.test.ts +41 -0
  90. package/src/memory/__tests__/onboarding-events-store.test.ts +80 -0
  91. package/src/memory/activation-session-store.ts +43 -0
  92. package/src/memory/db-init.ts +4 -0
  93. package/src/memory/migrations/273-onboarding-events-funnel-columns.ts +46 -0
  94. package/src/memory/migrations/274-create-activation-sessions.ts +15 -0
  95. package/src/memory/migrations/index.ts +2 -0
  96. package/src/memory/onboarding-events-store.ts +66 -18
  97. package/src/memory/schema/infrastructure.ts +13 -0
  98. package/src/memory/v2/__tests__/consolidation-job.test.ts +4 -97
  99. package/src/memory/v2/__tests__/page-store.test.ts +22 -0
  100. package/src/memory/v2/consolidation-job.ts +2 -72
  101. package/src/memory/v2/types.ts +5 -0
  102. package/src/messaging/providers/telegram-bot/api.ts +14 -5
  103. package/src/notifications/adapters/telegram.ts +7 -1
  104. package/src/plugin-api/constants.ts +2 -2
  105. package/src/plugin-api/index.ts +2 -2
  106. package/src/plugin-api/types.ts +19 -5
  107. package/src/plugins/defaults/compaction/compact.ts +24 -15
  108. package/src/plugins/defaults/compaction/context-overflow-reducer.ts +4 -4
  109. package/src/plugins/defaults/compaction/manager-store.ts +1 -1
  110. package/src/{context → plugins/defaults/compaction}/window-manager.ts +68 -12
  111. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +12 -18
  112. package/src/plugins/defaults/memory-v3-shadow/__tests__/capabilities.test.ts +19 -66
  113. package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +181 -0
  114. package/src/plugins/defaults/memory-v3-shadow/__tests__/edge.test.ts +247 -0
  115. package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +139 -129
  116. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +332 -164
  117. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +516 -293
  118. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +306 -0
  119. package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +51 -21
  120. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +402 -0
  121. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-needle.test.ts +135 -0
  122. package/src/plugins/defaults/memory-v3-shadow/__tests__/sections.test.ts +125 -0
  123. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +66 -11
  124. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +446 -0
  125. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +271 -110
  126. package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +0 -22
  127. package/src/plugins/defaults/memory-v3-shadow/capabilities.ts +26 -62
  128. package/src/plugins/defaults/memory-v3-shadow/dense.ts +97 -0
  129. package/src/plugins/defaults/memory-v3-shadow/edge.ts +252 -0
  130. package/src/plugins/defaults/memory-v3-shadow/injector.ts +3 -2
  131. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +415 -181
  132. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +196 -56
  133. package/src/plugins/defaults/memory-v3-shadow/page-content.ts +39 -2
  134. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +204 -0
  135. package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +15 -7
  136. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +236 -0
  137. package/src/plugins/defaults/memory-v3-shadow/section-needle.ts +200 -0
  138. package/src/plugins/defaults/memory-v3-shadow/sections.ts +115 -0
  139. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +75 -3
  140. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +111 -78
  141. package/src/plugins/defaults/memory-v3-shadow/types.ts +52 -19
  142. package/src/plugins/defaults/memory-v3-shadow/working-set.ts +4 -1
  143. package/src/plugins/external-api.ts +114 -0
  144. package/src/prompts/system-prompt.ts +61 -10
  145. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +37 -2
  146. package/src/providers/anthropic/client.ts +9 -10
  147. package/src/providers/inference/kimi-cjk-token-ids.ts +493 -0
  148. package/src/providers/inference/logit-bias.ts +55 -0
  149. package/src/providers/openai/__tests__/vision-not-supported.test.ts +75 -0
  150. package/src/providers/openai/chat-completions-provider.ts +44 -1
  151. package/src/providers/retry.ts +22 -0
  152. package/src/providers/types.ts +6 -0
  153. package/src/runtime/routes/__tests__/stt-routes.test.ts +112 -0
  154. package/src/runtime/routes/acp-routes.test.ts +3 -20
  155. package/src/runtime/routes/app-management-routes.ts +3 -0
  156. package/src/runtime/routes/conversation-routes.ts +2 -0
  157. package/src/runtime/routes/memory-v3-routes.ts +64 -314
  158. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +1 -1
  159. package/src/runtime/routes/stt-routes.ts +45 -12
  160. package/src/runtime/routes/workspace-routes.ts +50 -15
  161. package/src/services/published-app-updater.ts +30 -8
  162. package/src/telemetry/__tests__/activation-funnel.test.ts +95 -0
  163. package/src/telemetry/activation-funnel.ts +167 -0
  164. package/src/telemetry/types.ts +13 -0
  165. package/src/telemetry/usage-telemetry-reporter.test.ts +154 -0
  166. package/src/telemetry/usage-telemetry-reporter.ts +26 -1
  167. package/src/tools/acp/list-agents.test.ts +2 -18
  168. package/src/tools/acp/list-agents.ts +3 -15
  169. package/src/tools/acp/spawn.test.ts +0 -10
  170. package/src/tools/browser/browser-execution.ts +12 -2
  171. package/src/tools/network/web-fetch.ts +65 -24
  172. package/src/tools/skills/load.ts +1 -1
  173. package/src/tools/ui-surface/definitions.ts +7 -0
  174. package/src/acp/feature-gate.test.ts +0 -48
  175. package/src/acp/feature-gate.ts +0 -34
  176. package/src/plugins/defaults/memory-v3-shadow/__tests__/assign.test.ts +0 -242
  177. package/src/plugins/defaults/memory-v3-shadow/__tests__/core.test.ts +0 -39
  178. package/src/plugins/defaults/memory-v3-shadow/__tests__/health.test.ts +0 -219
  179. package/src/plugins/defaults/memory-v3-shadow/__tests__/needle.test.ts +0 -107
  180. package/src/plugins/defaults/memory-v3-shadow/__tests__/provider-blocks.test.ts +0 -13
  181. package/src/plugins/defaults/memory-v3-shadow/__tests__/reconcile.test.ts +0 -274
  182. package/src/plugins/defaults/memory-v3-shadow/__tests__/router.test.ts +0 -337
  183. package/src/plugins/defaults/memory-v3-shadow/__tests__/selector.test.ts +0 -470
  184. package/src/plugins/defaults/memory-v3-shadow/__tests__/snapshot.test.ts +0 -168
  185. package/src/plugins/defaults/memory-v3-shadow/__tests__/tree.test.ts +0 -192
  186. package/src/plugins/defaults/memory-v3-shadow/assign.ts +0 -272
  187. package/src/plugins/defaults/memory-v3-shadow/core.ts +0 -26
  188. package/src/plugins/defaults/memory-v3-shadow/health.ts +0 -0
  189. package/src/plugins/defaults/memory-v3-shadow/needle.ts +0 -115
  190. package/src/plugins/defaults/memory-v3-shadow/provider-blocks.ts +0 -26
  191. package/src/plugins/defaults/memory-v3-shadow/reconcile.ts +0 -527
  192. package/src/plugins/defaults/memory-v3-shadow/router.ts +0 -190
  193. package/src/plugins/defaults/memory-v3-shadow/selector.ts +0 -226
  194. package/src/plugins/defaults/memory-v3-shadow/snapshot.ts +0 -209
  195. package/src/plugins/defaults/memory-v3-shadow/tree.ts +0 -174
  196. package/src/util/map-limit.ts +0 -27
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Tests for the `pre-model-call` and `assistant-message` plugin hooks: a plugin
2
+ * Tests for the `pre-model-call` and `post-model-call` plugin hooks: a plugin
3
3
  * can edit the outbound request, transform the finalized assistant message
4
4
  * (persisted + streamed), and defer the live stream so the transformed text is
5
5
  * emitted once. All hooks here use neutral transforms (redaction / uppercasing).
@@ -9,7 +9,7 @@ import { beforeEach, describe, expect, test } from "bun:test";
9
9
  import type { AgentEvent } from "../agent/loop.js";
10
10
  import { AgentLoop } from "../agent/loop.js";
11
11
  import type {
12
- AssistantMessageContext,
12
+ PostModelCallContext,
13
13
  PreModelCallContext,
14
14
  } from "../plugin-api/types.js";
15
15
  import { resetPluginRegistryAndRegisterDefaults } from "../plugins/defaults/index.js";
@@ -55,7 +55,7 @@ function lastAssistant(history: Message[]): Message {
55
55
 
56
56
  function registerOutputHookPlugin(hooks: {
57
57
  preModelCall?: (ctx: PreModelCallContext) => void;
58
- assistantMessage?: (ctx: AssistantMessageContext) => void;
58
+ postModelCall?: (ctx: PostModelCallContext) => void;
59
59
  }): void {
60
60
  registerPlugin({
61
61
  manifest: { name: "test-output-hooks", version: "0.0.0" },
@@ -67,10 +67,10 @@ function registerOutputHookPlugin(hooks: {
67
67
  },
68
68
  }
69
69
  : {}),
70
- ...(hooks.assistantMessage
70
+ ...(hooks.postModelCall
71
71
  ? {
72
- "assistant-message": async (ctx: AssistantMessageContext) => {
73
- hooks.assistantMessage!(ctx);
72
+ "post-model-call": async (ctx: PostModelCallContext) => {
73
+ hooks.postModelCall!(ctx);
74
74
  },
75
75
  }
76
76
  : {}),
@@ -83,9 +83,9 @@ describe("agent loop output hooks", () => {
83
83
  resetPluginRegistryAndRegisterDefaults();
84
84
  });
85
85
 
86
- test("assistant-message transforms the persisted message content", async () => {
86
+ test("post-model-call transforms the persisted message content", async () => {
87
87
  registerOutputHookPlugin({
88
- assistantMessage: (ctx) => {
88
+ postModelCall: (ctx) => {
89
89
  ctx.content = ctx.content.map((b) =>
90
90
  b.type === "text"
91
91
  ? { type: "text", text: b.text.replace("secret", "[redacted]") }
@@ -113,7 +113,7 @@ describe("agent loop output hooks", () => {
113
113
  preModelCall: (ctx) => {
114
114
  ctx.deferAssistantOutput = true;
115
115
  },
116
- assistantMessage: (ctx) => {
116
+ postModelCall: (ctx) => {
117
117
  ctx.content = [{ type: "text", text: "[filtered]" }];
118
118
  },
119
119
  });
@@ -137,7 +137,7 @@ describe("agent loop output hooks", () => {
137
137
 
138
138
  test("without defer, the real text still streams while storage is transformed", async () => {
139
139
  registerOutputHookPlugin({
140
- assistantMessage: (ctx) => {
140
+ postModelCall: (ctx) => {
141
141
  ctx.content = [{ type: "text", text: "[stored]" }];
142
142
  },
143
143
  });
@@ -168,7 +168,7 @@ describe("agent loop output hooks", () => {
168
168
  stopReason: "tool_use",
169
169
  };
170
170
  registerOutputHookPlugin({
171
- assistantMessage: (ctx) => {
171
+ postModelCall: (ctx) => {
172
172
  ctx.content = ctx.content.map((b) =>
173
173
  b.type === "text" ? { type: "text", text: b.text.toUpperCase() } : b,
174
174
  );
@@ -237,7 +237,7 @@ describe("agent loop output hooks", () => {
237
237
  // mid-mutation would survive into history. The loop must clone the
238
238
  // content before invoking the hook.
239
239
  registerOutputHookPlugin({
240
- assistantMessage: (ctx) => {
240
+ postModelCall: (ctx) => {
241
241
  ctx.content.push({ type: "text", text: "[INJECTED]" });
242
242
  throw new Error("boom");
243
243
  },
@@ -269,7 +269,7 @@ describe("agent loop output hooks", () => {
269
269
  preModelCall: (ctx) => {
270
270
  ctx.deferAssistantOutput = true;
271
271
  },
272
- assistantMessage: (ctx) => {
272
+ postModelCall: (ctx) => {
273
273
  seen.calls += 1;
274
274
  ctx.content = ctx.content.map((b) =>
275
275
  b.type === "text" ? { type: "text", text: b.text.toUpperCase() } : b,
@@ -2,7 +2,11 @@ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
3
  import type { Anthropic } from "@anthropic-ai/sdk";
4
4
 
5
- import type { Message, ToolDefinition } from "../providers/types.js";
5
+ import type {
6
+ ContentBlock,
7
+ Message,
8
+ ToolDefinition,
9
+ } from "../providers/types.js";
6
10
 
7
11
  // ---------------------------------------------------------------------------
8
12
  // Mock Anthropic SDK — must be before importing the provider
@@ -102,7 +106,6 @@ mock.module("@anthropic-ai/sdk", () => ({
102
106
  }));
103
107
 
104
108
  // Import after mocking
105
- import { cachedTextBlock } from "../plugins/defaults/memory-v3-shadow/provider-blocks.js";
106
109
  import { AnthropicProvider } from "../providers/anthropic/client.js";
107
110
  import {
108
111
  isPlaceholderSentinelText,
@@ -138,6 +141,18 @@ function toolResultMsg(toolUseId: string, content: string): Message {
138
141
  };
139
142
  }
140
143
 
144
+ // A stable prefix text block carrying a 1h-TTL cache_control breakpoint, as a
145
+ // caller may stamp before the provider sees it. The internal ContentBlock type
146
+ // omits cache_control (only the provider transforms it onto the wire), so reach
147
+ // it via a cast.
148
+ function cachedPrefixBlock(text: string): ContentBlock {
149
+ return {
150
+ type: "text",
151
+ text,
152
+ cache_control: { type: "ephemeral", ttl: "1h" },
153
+ } as unknown as ContentBlock;
154
+ }
155
+
141
156
  const sampleTools: ToolDefinition[] = [
142
157
  {
143
158
  name: "file_read",
@@ -325,13 +340,13 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
325
340
  });
326
341
 
327
342
  test("preserves a caller's 1h block cache_control and sends the extended-cache beta (non-Haiku)", async () => {
328
- // v3's `cachedTextBlock` stamps a stable prefix block with a 1h TTL; the
329
- // non-Haiku path must forward it unchanged and send the beta header.
343
+ // A caller that stamps a stable prefix block with a 1h TTL: the non-Haiku
344
+ // path must forward it unchanged and send the beta header.
330
345
  await provider.sendMessage([
331
346
  {
332
347
  role: "user",
333
348
  content: [
334
- cachedTextBlock("stable pages block"),
349
+ cachedPrefixBlock("stable pages block"),
335
350
  { type: "text", text: "volatile current message" },
336
351
  ],
337
352
  },
@@ -349,7 +364,7 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
349
364
 
350
365
  test("strips ttl from a caller's block cache_control and omits the beta for Haiku", async () => {
351
366
  // Haiku doesn't support the extended-cache-ttl beta, so a caller-stamped
352
- // 1h ttl (e.g. from `cachedTextBlock`) must be stripped before sending.
367
+ // 1h ttl on a message block must be stripped before sending.
353
368
  const haiku = new AnthropicProvider(
354
369
  "sk-ant-test",
355
370
  "claude-haiku-4-5-20251001",
@@ -358,7 +373,7 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
358
373
  {
359
374
  role: "user",
360
375
  content: [
361
- cachedTextBlock("stable pages block"),
376
+ cachedPrefixBlock("stable pages block"),
362
377
  { type: "text", text: "volatile current message" },
363
378
  ],
364
379
  },
@@ -385,7 +400,7 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
385
400
  {
386
401
  role: "user",
387
402
  content: [
388
- cachedTextBlock("stable pages block"),
403
+ cachedPrefixBlock("stable pages block"),
389
404
  { type: "text", text: "volatile current message" },
390
405
  ],
391
406
  },
@@ -173,7 +173,7 @@ mock.module("../memory/retriever.js", () => ({
173
173
  injectMemoryRecallAsUserBlock: (msgs: Message[]) => msgs,
174
174
  }));
175
175
 
176
- mock.module("../context/window-manager.js", () => ({
176
+ mock.module("../plugins/defaults/compaction/window-manager.js", () => ({
177
177
  ContextWindowManager: class {
178
178
  constructor() {}
179
179
  updateConfig() {}
@@ -3,19 +3,29 @@
3
3
  *
4
4
  * The agent loop calls {@link defaultCompact} directly with a
5
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
6
+ * These tests assert that the default implementation resolves the
7
+ * conversation's {@link ContextWindowManager} from the compaction store and
8
+ * forwards the request's conversational options verbatim. The orchestrator
9
+ * integration path (conversation-agent-loop) is exercised by
10
10
  * `conversation-agent-loop-overflow.test.ts`.
11
11
  */
12
12
 
13
- import { describe, expect, test } from "bun:test";
13
+ import { describe, expect, mock, test } from "bun:test";
14
14
 
15
- import {
16
- type CompactionContext,
17
- defaultCompact,
18
- } from "../plugins/defaults/compaction/compact.js";
15
+ // `defaultCompact` resolves the manager from the per-conversation compaction
16
+ // store. Register each test's canned stub in a map the mocked store reads from.
17
+ const fakeContextWindowManagers = new Map<string, unknown>();
18
+ mock.module("../plugins/defaults/compaction/manager-store.js", () => ({
19
+ createContextWindowManager: () => undefined,
20
+ getContextWindowManager: (conversationId: string) =>
21
+ fakeContextWindowManagers.get(conversationId),
22
+ disposeContextWindowManager: (conversationId: string) => {
23
+ fakeContextWindowManagers.delete(conversationId);
24
+ },
25
+ }));
26
+
27
+ const { defaultCompact } =
28
+ await import("../plugins/defaults/compaction/compact.js");
19
29
 
20
30
  type ContextWindowResultShape = {
21
31
  compacted: boolean;
@@ -55,13 +65,17 @@ function makeResult(
55
65
  };
56
66
  }
57
67
 
58
- function makeManager(result: ContextWindowResultShape) {
68
+ /** Register a stub manager under a conversation id and return its recorder. */
69
+ function registerManager(
70
+ conversationId: string,
71
+ result: ContextWindowResultShape,
72
+ ) {
59
73
  const observed: {
60
74
  messages: unknown;
61
75
  signal: unknown;
62
76
  options: unknown;
63
77
  }[] = [];
64
- const manager = {
78
+ fakeContextWindowManagers.set(conversationId, {
65
79
  maybeCompact: async (
66
80
  messages: unknown,
67
81
  signal: unknown,
@@ -70,8 +84,8 @@ function makeManager(result: ContextWindowResultShape) {
70
84
  observed.push({ messages, signal, options });
71
85
  return result;
72
86
  },
73
- } as unknown as CompactionContext["manager"];
74
- return { manager, observed };
87
+ });
88
+ return observed;
75
89
  }
76
90
 
77
91
  describe("defaultCompact", () => {
@@ -81,13 +95,13 @@ describe("defaultCompact", () => {
81
95
  summaryText: "manager-summary",
82
96
  compactedMessages: 7,
83
97
  });
84
- const { manager, observed } = makeManager(expected);
98
+ const observed = registerManager("conv-delegates", expected);
85
99
  const messages = [{ role: "user", content: "hi" }] as never;
86
100
  const signal = new AbortController().signal;
87
101
 
88
102
  // WHEN defaultCompact runs with those messages and a signal
89
103
  const result = (await defaultCompact({
90
- manager,
104
+ conversationId: "conv-delegates",
91
105
  messages,
92
106
  signal,
93
107
  })) as unknown as ContextWindowResultShape;
@@ -105,11 +119,11 @@ describe("defaultCompact", () => {
105
119
 
106
120
  test("forwards the request's compaction options to the manager", async () => {
107
121
  // GIVEN a manager and a fully-populated compaction context
108
- const { manager, observed } = makeManager(makeResult());
122
+ const observed = registerManager("conv-options", makeResult());
109
123
 
110
124
  // WHEN defaultCompact runs with force/profile/trust options
111
125
  await defaultCompact({
112
- manager,
126
+ conversationId: "conv-options",
113
127
  messages: [] as never,
114
128
  force: true,
115
129
  overrideProfile: "fast-profile",
@@ -118,7 +132,7 @@ describe("defaultCompact", () => {
118
132
  actorTrustClass: "guardian",
119
133
  });
120
134
 
121
- // THEN the manager received exactly those options, and the manager,
135
+ // THEN the manager received exactly those options, and the conversation id,
122
136
  // messages, and signal are not leaked into the options bag
123
137
  expect(observed).toHaveLength(1);
124
138
  expect(observed[0]!.options).toEqual({
@@ -12,8 +12,8 @@ import { describe, expect, mock, test } from "bun:test";
12
12
 
13
13
  import { CompactionCircuit } from "../agent/compaction-circuit.js";
14
14
  import type { AgentEvent } from "../agent/loop.js";
15
- import type { ContextWindowResult } from "../context/window-manager.js";
16
15
  import type { ServerMessage } from "../daemon/message-protocol.js";
16
+ import type { ContextWindowResult } from "../plugins/defaults/compaction/window-manager.js";
17
17
  import type { Message, ProviderResponse } from "../providers/types.js";
18
18
 
19
19
  // ---------------------------------------------------------------------------
@@ -187,7 +187,7 @@ let mockCompactResult: ContextWindowResult = {
187
187
  summaryText: "",
188
188
  };
189
189
 
190
- mock.module("../context/window-manager.js", () => ({
190
+ mock.module("../plugins/defaults/compaction/window-manager.js", () => ({
191
191
  ContextWindowManager: class {
192
192
  nonPersistedPrefixCount = 0;
193
193
  constructor() {}
@@ -12,7 +12,7 @@ import { describe, expect, mock, test } from "bun:test";
12
12
 
13
13
  import { DEFAULT_CONFIG } from "../config/defaults.js";
14
14
  import { estimatePromptTokens } from "../context/token-estimator.js";
15
- import { ContextWindowManager } from "../context/window-manager.js";
15
+ import { ContextWindowManager } from "../plugins/defaults/compaction/window-manager.js";
16
16
  import type { Message, Provider } from "../providers/types.js";
17
17
 
18
18
  mock.module("../util/logger.js", () => ({
@@ -1,17 +1,17 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
3
  import { estimatePromptTokens } from "../context/token-estimator.js";
4
- import type {
5
- ContextWindowCompactOptions,
6
- ContextWindowResult,
7
- } from "../context/window-manager.js";
8
- import { createContextSummaryMessage } from "../context/window-manager.js";
9
4
  import {
10
5
  createInitialReducerState,
11
6
  reduceContextOverflow,
12
7
  type ReducerConfig,
13
8
  type ReducerState,
14
9
  } from "../plugins/defaults/compaction/context-overflow-reducer.js";
10
+ import type {
11
+ ContextWindowCompactOptions,
12
+ ContextWindowResult,
13
+ } from "../plugins/defaults/compaction/window-manager.js";
14
+ import { createContextSummaryMessage } from "../plugins/defaults/compaction/window-manager.js";
15
15
  import type { Message } from "../providers/types.js";
16
16
 
17
17
  function msg(role: "user" | "assistant", text: string): Message {
@@ -56,6 +56,11 @@ let runCalls: Array<{
56
56
  let runResults: CompactionRunResult[] = [];
57
57
  const estimateReturns: number[] = [];
58
58
 
59
+ // Captured arguments from each runEmergencyCompaction invocation, so the
60
+ // emergencyCompact tests can assert the manager fills in the fields it owns.
61
+ let emergencyCalls: Record<string, unknown>[] = [];
62
+ let emergencyResult: CompactionRunResult | undefined;
63
+
59
64
  mock.module("../context/token-estimator.js", () => ({
60
65
  estimatePromptTokens: (): number => {
61
66
  const next = estimateReturns.shift();
@@ -92,9 +97,18 @@ mock.module("../context/compactor.js", () => ({
92
97
  }
93
98
  return result;
94
99
  },
100
+ runEmergencyCompaction: async (
101
+ args: Record<string, unknown>,
102
+ ): Promise<CompactionRunResult> => {
103
+ emergencyCalls.push(args);
104
+ if (!emergencyResult) {
105
+ throw new Error("emergencyResult not seeded");
106
+ }
107
+ return emergencyResult;
108
+ },
95
109
  }));
96
110
 
97
- import { ContextWindowManager } from "../context/window-manager.js";
111
+ import { ContextWindowManager } from "../plugins/defaults/compaction/window-manager.js";
98
112
  import type { Message, Provider } from "../providers/types.js";
99
113
 
100
114
  function makeProvider(): Provider {
@@ -142,24 +156,28 @@ function compactResult(
142
156
  };
143
157
  }
144
158
 
159
+ function makeConfig(maxAttempts: number = 3) {
160
+ return {
161
+ enabled: true,
162
+ maxInputTokens: 200_000,
163
+ targetBudgetRatio: 0.3,
164
+ compactThreshold: 0.8,
165
+ summaryBudgetRatio: 0.05,
166
+ overflowRecovery: {
167
+ enabled: true,
168
+ safetyMarginRatio: 0.05,
169
+ maxAttempts,
170
+ interactiveLatestTurnCompression: "summarize" as const,
171
+ nonInteractiveLatestTurnCompression: "summarize" as const,
172
+ },
173
+ };
174
+ }
175
+
145
176
  function buildManager(maxAttempts: number = 3): ContextWindowManager {
146
177
  return new ContextWindowManager({
147
178
  provider: makeProvider(),
148
179
  systemPrompt: "you are a test assistant",
149
- config: {
150
- enabled: true,
151
- maxInputTokens: 200_000,
152
- targetBudgetRatio: 0.3,
153
- compactThreshold: 0.8,
154
- summaryBudgetRatio: 0.05,
155
- overflowRecovery: {
156
- enabled: true,
157
- safetyMarginRatio: 0.05,
158
- maxAttempts,
159
- interactiveLatestTurnCompression: "summarize",
160
- nonInteractiveLatestTurnCompression: "summarize",
161
- },
162
- },
180
+ config: makeConfig(maxAttempts),
163
181
  conversationId: "conv-test",
164
182
  });
165
183
  }
@@ -289,3 +307,91 @@ describe("ContextWindowManager.maybeCompact retry budget", () => {
289
307
  expect(result.estimatedInputTokens).toBe(165_000);
290
308
  });
291
309
  });
310
+
311
+ describe("ContextWindowManager.emergencyCompact", () => {
312
+ beforeEach(() => {
313
+ emergencyCalls = [];
314
+ emergencyResult = undefined;
315
+ estimateReturns.length = 0;
316
+ });
317
+
318
+ test("supplies manager-owned fields and forwards only overflow inputs", async () => {
319
+ /**
320
+ * The manager owns provider, system prompt, token budget, conversation
321
+ * id, compaction config, and non-persisted prefix count; the caller
322
+ * provides only the overflow-specific inputs.
323
+ */
324
+ // GIVEN a manager seeded with a non-persisted prefix and a canned result
325
+ emergencyResult = compactResult({
326
+ compacted: true,
327
+ compactedMessages: 40,
328
+ summaryText: "emergency summary",
329
+ });
330
+ const manager = buildManager();
331
+ manager.seedNonPersistedPrefix(3);
332
+ const messages = makeMessages(8);
333
+ const signal = new AbortController().signal;
334
+
335
+ // WHEN emergency compaction runs
336
+ const result = await manager.emergencyCompact(
337
+ messages,
338
+ { previousEstimatedInputTokens: 242_201, overrideProfile: "fast" },
339
+ signal,
340
+ );
341
+
342
+ // THEN the manager fills its owned fields and forwards the caller inputs
343
+ expect(emergencyCalls.length).toBe(1);
344
+ const args = emergencyCalls[0]!;
345
+ expect(args.conversationId).toBe("conv-test");
346
+ expect(args.systemPrompt).toBe("you are a test assistant");
347
+ expect(args.maxInputTokens).toBe(200_000);
348
+ expect(args.nonPersistedPrefixCount).toBe(3);
349
+ expect(args.tools).toBeUndefined();
350
+ expect(args.force).toBe(true);
351
+ expect(args.signal).toBe(signal);
352
+ expect(args.messages).toBe(messages);
353
+ expect(args.previousEstimatedInputTokens).toBe(242_201);
354
+ expect(args.overrideProfile).toBe("fast");
355
+ expect((args.provider as { name: string }).name).toBe("mock-provider");
356
+ expect(result.summaryText).toBe("emergency summary");
357
+ });
358
+
359
+ test("defaults overrideProfile to null when omitted", async () => {
360
+ /**
361
+ * Callers that don't resolve a per-conversation profile get an explicit
362
+ * null forwarded to the summary call.
363
+ */
364
+ // GIVEN a manager and a canned result
365
+ emergencyResult = compactResult({ compacted: true });
366
+ const manager = buildManager();
367
+
368
+ // WHEN emergency compaction runs without an override profile
369
+ await manager.emergencyCompact(makeMessages(6), {
370
+ previousEstimatedInputTokens: 210_000,
371
+ });
372
+
373
+ // THEN overrideProfile is forwarded as null
374
+ expect(emergencyCalls[0]!.overrideProfile).toBeNull();
375
+ });
376
+
377
+ test("throws when the manager has no conversation id", async () => {
378
+ /**
379
+ * Emergency compaction needs a conversation id for attachment and
380
+ * timestamp lookups; a manager without one cannot run it.
381
+ */
382
+ // GIVEN a manager constructed without a conversation id
383
+ const manager = new ContextWindowManager({
384
+ provider: makeProvider(),
385
+ systemPrompt: "you are a test assistant",
386
+ config: makeConfig(),
387
+ });
388
+
389
+ // WHEN/THEN emergency compaction rejects without invoking the compactor
390
+ await expect(
391
+ manager.emergencyCompact(makeMessages(4), {
392
+ previousEstimatedInputTokens: 210_000,
393
+ }),
394
+ ).rejects.toThrow(/conversationId/);
395
+ expect(emergencyCalls.length).toBe(0);
396
+ });
397
+ });
@@ -167,7 +167,7 @@ mock.module("../memory/retriever.js", () => ({
167
167
  injectMemoryRecallAsUserBlock: (msgs: Message[]) => msgs,
168
168
  }));
169
169
 
170
- mock.module("../context/window-manager.js", () => ({
170
+ mock.module("../plugins/defaults/compaction/window-manager.js", () => ({
171
171
  ContextWindowManager: class {
172
172
  constructor() {}
173
173
  updateConfig() {}
@@ -164,7 +164,7 @@ mock.module("../memory/retriever.js", () => ({
164
164
  injectMemoryRecallAsUserBlock: (msgs: Message[]) => msgs,
165
165
  }));
166
166
 
167
- mock.module("../context/window-manager.js", () => ({
167
+ mock.module("../plugins/defaults/compaction/window-manager.js", () => ({
168
168
  ContextWindowManager: class {
169
169
  constructor() {}
170
170
  updateConfig() {}
@@ -319,7 +319,6 @@ describe("classifyConversationError", () => {
319
319
  expect(result.errorCategory).toBe("image_dimensions_too_large");
320
320
  expect(result.retryable).toBe(false);
321
321
  expect(result.userMessage).toContain("image");
322
- expect(result.userMessage).toContain("8000");
323
322
  });
324
323
 
325
324
  it("matches the singular 'image dimension exceeds' phrasing as well", () => {
@@ -333,6 +332,21 @@ describe("classifyConversationError", () => {
333
332
  expect(result.retryable).toBe(false);
334
333
  });
335
334
 
335
+ it("classifies the base64 payload 'exceeds 5 MB maximum' variant as IMAGE_TOO_LARGE (JARVIS-1041)", () => {
336
+ // This is the exact shape that wedged the reporting user: an oversized
337
+ // image nested in a tool_result. It must route to IMAGE_TOO_LARGE so the
338
+ // recovery path fires, not to a retryable PROVIDER_API loop.
339
+ const err = new ProviderError(
340
+ 'Anthropic API error (400): 400 {"type":"error","error":{"type":"invalid_request_error","message":"messages.28.content.1.tool_result.content.1.image.source.base64: image exceeds 5 MB maximum: 7465044 bytes > 5242880 bytes"},"request_id":"req_011CbbFqvxBmDQdhVlqxtAfW"}',
341
+ "anthropic",
342
+ 400,
343
+ );
344
+ const result = classifyConversationError(err, baseCtx);
345
+ expect(result.code).toBe("IMAGE_TOO_LARGE");
346
+ expect(result.errorCategory).toBe("image_dimensions_too_large");
347
+ expect(result.retryable).toBe(false);
348
+ });
349
+
336
350
  it("does not steal generic 400s that happen to mention 'image'", () => {
337
351
  const err = new ProviderError(
338
352
  "invalid request: image source is missing",
@@ -809,6 +809,11 @@ describe("web_search_tool_result structural guard", () => {
809
809
  // contentBlocks property, so it cannot contain nested media.
810
810
  "plugins/defaults/compaction/context-overflow-reducer.ts",
811
811
 
812
+ // Downgrades permanently-unsendable images nested in tool_result.contentBlocks
813
+ // (e.g. an oversized browser screenshot). web_search_tool_result has opaque
814
+ // content with no contentBlocks property, so it cannot hold such an image.
815
+ "daemon/persist-unsendable-image.ts",
816
+
812
817
  // Final orphan-pair safety pass in the Slack transcript renderer.
813
818
  // Server-side block types (`server_tool_use`, `web_search_tool_result`)
814
819
  // are stripped earlier by `buildMessageContentBlocks` and cannot reach
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
- import { createContextSummaryMessage } from "../context/window-manager.js";
4
3
  import { stripMediaPayloadsForRetry } from "../daemon/conversation-media-retry.js";
4
+ import { createContextSummaryMessage } from "../plugins/defaults/compaction/window-manager.js";
5
5
  import type { ContentBlock, Message } from "../providers/types.js";
6
6
 
7
7
  // ---------------------------------------------------------------------------
@@ -185,6 +185,7 @@ function makeQueuedMessage(opts: {
185
185
  requestId: string;
186
186
  content?: string;
187
187
  turnInterfaceContext?: TurnInterfaceContext;
188
+ sourceActorPrincipalId?: string;
188
189
  }): QueuedMessage {
189
190
  return {
190
191
  content: opts.content ?? "follow up",
@@ -194,6 +195,10 @@ function makeQueuedMessage(opts: {
194
195
  metadata: {},
195
196
  sentAt: Date.now(),
196
197
  turnInterfaceContext: opts.turnInterfaceContext,
198
+ sourceActorPrincipalId: opts.sourceActorPrincipalId,
199
+ authContext: opts.sourceActorPrincipalId
200
+ ? ({ actorPrincipalId: opts.sourceActorPrincipalId } as never)
201
+ : undefined,
197
202
  };
198
203
  }
199
204
 
@@ -332,6 +337,7 @@ describe("drainQueue preactivation re-add for host-proxy interfaces", () => {
332
337
  makeQueuedMessage({
333
338
  requestId: "req-web-1",
334
339
  turnInterfaceContext: ifCtx,
340
+ sourceActorPrincipalId: "user-1",
335
341
  }),
336
342
  );
337
343
  const ctx = makeFakeContext({ queue, turnInterfaceContext: ifCtx });
@@ -346,6 +352,39 @@ describe("drainQueue preactivation re-add for host-proxy interfaces", () => {
346
352
  expect(ctx.preactivatedSkillIdCalls).toContain("computer-use");
347
353
  });
348
354
 
355
+ test("drainSingleMessage filters cross-client preactivation by the queued requester, not guardian owner", async () => {
356
+ mockCapabilityClients = [
357
+ { clientId: "owner-macos-client", actorPrincipalId: "owner-user" },
358
+ ];
359
+ const queue = new MessageQueue();
360
+ const ifCtx: TurnInterfaceContext = {
361
+ userMessageInterface: "web",
362
+ assistantMessageInterface: "web",
363
+ };
364
+ queue.push(
365
+ makeQueuedMessage({
366
+ requestId: "req-web-attacker",
367
+ turnInterfaceContext: ifCtx,
368
+ sourceActorPrincipalId: "trusted-contact-user",
369
+ }),
370
+ );
371
+ const ctx = makeFakeContext({ queue, turnInterfaceContext: ifCtx });
372
+ ctx.trustContext = {
373
+ trustClass: "trusted_contact",
374
+ guardianPrincipalId: "owner-user",
375
+ sourceChannel: "vellum",
376
+ };
377
+
378
+ await drainQueue(ctx);
379
+
380
+ expect(ctx.preactivatedSkillIdCalls).not.toContain("computer-use");
381
+ expect(ctx.preactivatedSkillIdCalls).not.toContain("app-control");
382
+ expect(ctx.currentTurnAuthContext?.actorPrincipalId).toBe(
383
+ "trusted-contact-user",
384
+ );
385
+ expect(ctx.currentTurnSourceActorPrincipalId).toBe("trusted-contact-user");
386
+ });
387
+
349
388
  test("drainSingleMessage does NOT re-add 'app-control' for web-sourced message when no capable client is connected", async () => {
350
389
  // mockCapabilityClients remains [] (reset by afterEach from prior test)
351
390
  const queue = new MessageQueue();
@@ -380,6 +419,7 @@ describe("drainQueue preactivation re-add for host-proxy interfaces", () => {
380
419
  makeQueuedMessage({
381
420
  requestId: "req-web-3",
382
421
  turnInterfaceContext: ifCtx,
422
+ sourceActorPrincipalId: "user-1",
383
423
  }),
384
424
  );
385
425
  const ctx = makeFakeContext({ queue, turnInterfaceContext: ifCtx });
@@ -249,7 +249,7 @@ mock.module("../agent/loop.js", () => ({
249
249
  },
250
250
  }));
251
251
 
252
- mock.module("../context/window-manager.js", () => ({
252
+ mock.module("../plugins/defaults/compaction/window-manager.js", () => ({
253
253
  ContextWindowManager: class {
254
254
  constructor() {}
255
255
  updateConfig() {}