@vellumai/assistant 0.8.9-staging.1 → 0.8.9-staging.3

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 (155) hide show
  1. package/ARCHITECTURE.md +6 -6
  2. package/docs/activation-funnel-telemetry.md +310 -0
  3. package/openapi.yaml +6 -7
  4. package/package.json +1 -1
  5. package/src/__tests__/activation-early-marking.test.ts +120 -0
  6. package/src/__tests__/agent-loop-exit-reason.test.ts +33 -18
  7. package/src/__tests__/agent-loop-output-hooks.test.ts +13 -13
  8. package/src/__tests__/app-builder-tool-scripts.test.ts +21 -0
  9. package/src/__tests__/app-executors.test.ts +132 -0
  10. package/src/__tests__/approval-cascade.test.ts +1 -1
  11. package/src/__tests__/compaction-direct.test.ts +32 -18
  12. package/src/__tests__/compaction-events.test.ts +2 -2
  13. package/src/__tests__/compaction.benchmark.test.ts +1 -1
  14. package/src/__tests__/compactor-web-search-strip.test.ts +213 -0
  15. package/src/__tests__/context-overflow-reducer.test.ts +6 -6
  16. package/src/__tests__/context-window-manager-compact-retry.test.ts +1 -1
  17. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  18. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +19 -16
  19. package/src/__tests__/conversation-agent-loop-overflow.test.ts +48 -29
  20. package/src/__tests__/conversation-agent-loop.test.ts +48 -96
  21. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
  22. package/src/__tests__/conversation-error.test.ts +15 -1
  23. package/src/__tests__/conversation-history-web-search.test.ts +6 -1
  24. package/src/__tests__/conversation-media-retry.test.ts +1 -1
  25. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +40 -0
  26. package/src/__tests__/conversation-process-callsite.test.ts +1 -1
  27. package/src/__tests__/conversation-provider-retry-repair.test.ts +53 -50
  28. package/src/__tests__/conversation-queue.test.ts +1 -1
  29. package/src/__tests__/conversation-runtime-assembly.test.ts +71 -0
  30. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  31. package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
  32. package/src/__tests__/conversation-speed-override.test.ts +1 -1
  33. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +395 -0
  34. package/src/__tests__/conversation-surfaces-app-control.test.ts +44 -0
  35. package/src/__tests__/conversation-undo.test.ts +2 -2
  36. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
  37. package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
  38. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  39. package/src/__tests__/cu-unified-flow.test.ts +36 -0
  40. package/src/__tests__/history-repair-hook.test.ts +2 -0
  41. package/src/__tests__/memory-retrieval-hook.test.ts +69 -32
  42. package/src/__tests__/persist-unsendable-image-downscale.test.ts +145 -0
  43. package/src/__tests__/persist-unsendable-image.test.ts +97 -1
  44. package/src/__tests__/post-turn-tool-result-truncation.test.ts +69 -0
  45. package/src/__tests__/pre-model-call-sanitize.test.ts +109 -0
  46. package/src/__tests__/skill-feature-flags-integration.test.ts +5 -7
  47. package/src/__tests__/title-generate-hook.test.ts +2 -0
  48. package/src/__tests__/web-fetch.test.ts +45 -0
  49. package/src/acp/__tests__/helpers/acp-config-stub.ts +0 -2
  50. package/src/acp/__tests__/helpers/exec-file-stub.ts +12 -7
  51. package/src/acp/__tests__/prepare-agent-env.test.ts +0 -40
  52. package/src/acp/__tests__/session-manager-resume.test.ts +179 -27
  53. package/src/acp/auto-install.test.ts +130 -46
  54. package/src/acp/auto-install.ts +86 -31
  55. package/src/acp/prepare-agent-env.ts +8 -9
  56. package/src/acp/resolve-agent.test.ts +26 -179
  57. package/src/acp/resolve-agent.ts +35 -122
  58. package/src/acp/resume-hint.ts +1 -3
  59. package/src/acp/session-manager.ts +32 -20
  60. package/src/acp/types.ts +0 -8
  61. package/src/agent/loop.ts +47 -45
  62. package/src/cli/lib/__tests__/install-from-github.test.ts +232 -29
  63. package/src/cli/lib/__tests__/plugin-details.test.ts +28 -19
  64. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +57 -7
  65. package/src/cli/lib/__tests__/search-plugins.test.ts +17 -10
  66. package/src/cli/lib/install-from-github.ts +258 -41
  67. package/src/cli/lib/plugin-details.ts +20 -13
  68. package/src/cli/lib/plugin-marketplace.ts +23 -5
  69. package/src/cli/lib/search-plugins.ts +14 -8
  70. package/src/config/acp-defaults.ts +3 -3
  71. package/src/config/acp-schema.ts +1 -7
  72. package/src/config/bundled-skills/acp/SKILL.md +16 -29
  73. package/src/config/bundled-skills/acp/TOOLS.json +2 -2
  74. package/src/config/bundled-skills/app-builder/SKILL.md +3 -3
  75. package/src/config/bundled-skills/app-builder/TOOLS.json +43 -2
  76. package/src/config/bundled-skills/app-builder/tools/app-update.ts +18 -0
  77. package/src/config/bundled-tool-registry.ts +2 -0
  78. package/src/config/call-site-defaults.ts +0 -2
  79. package/src/config/feature-flag-registry.json +4 -19
  80. package/src/config/schemas/call-site-catalog.ts +0 -14
  81. package/src/config/schemas/llm.ts +0 -2
  82. package/src/context/compactor.ts +22 -4
  83. package/src/context/post-turn-tool-result-truncation.ts +39 -1
  84. package/src/daemon/conversation-agent-loop-handlers.ts +8 -1
  85. package/src/daemon/conversation-agent-loop.ts +93 -135
  86. package/src/daemon/conversation-error.ts +31 -4
  87. package/src/daemon/conversation-history.ts +1 -1
  88. package/src/daemon/conversation-media-retry.ts +19 -6
  89. package/src/daemon/conversation-messaging.ts +17 -0
  90. package/src/daemon/conversation-process.ts +14 -5
  91. package/src/daemon/conversation-queue-manager.ts +8 -0
  92. package/src/daemon/conversation-runtime-assembly.ts +37 -1
  93. package/src/daemon/conversation-surfaces.ts +141 -3
  94. package/src/daemon/conversation.ts +66 -11
  95. package/src/daemon/disk-pressure-policy.ts +0 -1
  96. package/src/daemon/lifecycle.ts +0 -7
  97. package/src/daemon/persist-unsendable-image.ts +62 -25
  98. package/src/daemon/process-message.ts +1 -1
  99. package/src/memory/__tests__/activation-session-store.test.ts +41 -0
  100. package/src/memory/__tests__/onboarding-events-store.test.ts +80 -0
  101. package/src/memory/activation-session-store.ts +43 -0
  102. package/src/memory/db-init.ts +4 -0
  103. package/src/memory/migrations/273-onboarding-events-funnel-columns.ts +46 -0
  104. package/src/memory/migrations/274-create-activation-sessions.ts +15 -0
  105. package/src/memory/migrations/index.ts +2 -0
  106. package/src/memory/onboarding-events-store.ts +66 -18
  107. package/src/memory/schema/infrastructure.ts +13 -0
  108. package/src/messaging/providers/telegram-bot/api.ts +14 -5
  109. package/src/notifications/adapters/telegram.ts +7 -1
  110. package/src/plugin-api/constants.ts +2 -2
  111. package/src/plugin-api/index.ts +2 -2
  112. package/src/plugin-api/types.ts +19 -5
  113. package/src/plugins/defaults/compaction/compact.ts +24 -15
  114. package/src/{daemon → plugins/defaults/compaction}/context-overflow-reducer.ts +10 -10
  115. package/src/plugins/defaults/compaction/manager-store.ts +14 -1
  116. package/src/{context → plugins/defaults/compaction}/window-manager.ts +12 -12
  117. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +6 -23
  118. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +36 -43
  119. package/src/prompts/system-prompt.ts +61 -10
  120. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +37 -2
  121. package/src/providers/openai/__tests__/vision-not-supported.test.ts +75 -0
  122. package/src/providers/openai/chat-completions-provider.ts +25 -0
  123. package/src/runtime/routes/__tests__/acp-routes.test.ts +71 -39
  124. package/src/runtime/routes/__tests__/plugins-routes.test.ts +29 -4
  125. package/src/runtime/routes/__tests__/stt-routes.test.ts +112 -0
  126. package/src/runtime/routes/acp-routes.test.ts +3 -20
  127. package/src/runtime/routes/conversation-routes.ts +2 -0
  128. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +1 -1
  129. package/src/runtime/routes/plugins-routes.ts +10 -9
  130. package/src/runtime/routes/stt-routes.ts +45 -12
  131. package/src/runtime/routes/workspace-routes.ts +50 -15
  132. package/src/telemetry/__tests__/activation-funnel.test.ts +95 -0
  133. package/src/telemetry/activation-funnel.ts +167 -0
  134. package/src/telemetry/types.ts +13 -0
  135. package/src/telemetry/usage-telemetry-reporter.test.ts +154 -0
  136. package/src/telemetry/usage-telemetry-reporter.ts +26 -1
  137. package/src/tools/acp/list-agents.test.ts +4 -20
  138. package/src/tools/acp/list-agents.ts +3 -15
  139. package/src/tools/acp/spawn.test.ts +103 -235
  140. package/src/tools/acp/spawn.ts +10 -120
  141. package/src/tools/apps/executors.ts +153 -42
  142. package/src/tools/browser/browser-execution.ts +12 -2
  143. package/src/tools/network/web-fetch.ts +65 -24
  144. package/src/tools/ui-surface/definitions.ts +7 -0
  145. package/src/acp/feature-gate.test.ts +0 -48
  146. package/src/acp/feature-gate.ts +0 -34
  147. package/src/proactive-artifact/aux-message-injector.ts +0 -97
  148. package/src/proactive-artifact/decision.test.ts +0 -226
  149. package/src/proactive-artifact/decision.ts +0 -165
  150. package/src/proactive-artifact/index.ts +0 -7
  151. package/src/proactive-artifact/job.test.ts +0 -962
  152. package/src/proactive-artifact/job.ts +0 -372
  153. package/src/proactive-artifact/message-copy.ts +0 -58
  154. package/src/proactive-artifact/trigger-state.test.ts +0 -286
  155. package/src/proactive-artifact/trigger-state.ts +0 -123
@@ -102,23 +102,26 @@ mock.module("../context/token-estimator.js", () => ({
102
102
  estimateToolsTokens: () => 0,
103
103
  }));
104
104
 
105
- mock.module("../daemon/context-overflow-reducer.js", () => ({
106
- createInitialReducerState: () => ({
107
- appliedTiers: [],
108
- injectionMode: "full" as const,
109
- exhausted: false,
110
- }),
111
- reduceContextOverflow: async (msgs: Message[]) => ({
112
- messages: msgs,
113
- tier: "forced_compaction",
114
- state: {
115
- appliedTiers: ["forced_compaction"],
116
- injectionMode: "full",
117
- exhausted: true,
118
- },
119
- estimatedTokens: 1000,
105
+ mock.module(
106
+ "../plugins/defaults/compaction/context-overflow-reducer.js",
107
+ () => ({
108
+ createInitialReducerState: () => ({
109
+ appliedTiers: [],
110
+ injectionMode: "full" as const,
111
+ exhausted: false,
112
+ }),
113
+ reduceContextOverflow: async (msgs: Message[]) => ({
114
+ messages: msgs,
115
+ tier: "forced_compaction",
116
+ state: {
117
+ appliedTiers: ["forced_compaction"],
118
+ injectionMode: "full",
119
+ exhausted: true,
120
+ },
121
+ estimatedTokens: 1000,
122
+ }),
120
123
  }),
121
- }));
124
+ );
122
125
 
123
126
  mock.module("../daemon/context-overflow-policy.js", () => ({
124
127
  resolveOverflowAction: () => "fail_gracefully",
@@ -39,6 +39,20 @@ const conversationRuntimeAssemblyRealSnapshot = {
39
39
 
40
40
  // ── Module mocks (must precede imports of the module under test) ─────
41
41
 
42
+ // The real AgentLoop resolves the per-conversation ContextWindowManager from
43
+ // the compaction store keyed by conversationId. These overflow tests build
44
+ // fake conversations whose manager is a canned stub, so register each stub in a
45
+ // map the mocked store reads from.
46
+ const fakeContextWindowManagers = new Map<string, unknown>();
47
+ mock.module("../plugins/defaults/compaction/manager-store.js", () => ({
48
+ createContextWindowManager: () => undefined,
49
+ getContextWindowManager: (conversationId: string) =>
50
+ fakeContextWindowManagers.get(conversationId),
51
+ disposeContextWindowManager: (conversationId: string) => {
52
+ fakeContextWindowManagers.delete(conversationId);
53
+ },
54
+ }));
55
+
42
56
  mock.module("../util/logger.js", () => ({
43
57
  getLogger: () =>
44
58
  new Proxy({} as Record<string, unknown>, { get: () => () => {} }),
@@ -137,35 +151,38 @@ mock.module("../context/token-estimator.js", () => ({
137
151
  let mockReducerStepFn:
138
152
  | ((msgs: Message[], cfg: unknown, state: unknown) => unknown)
139
153
  | null = null;
140
- mock.module("../daemon/context-overflow-reducer.js", () => ({
141
- createInitialReducerState: () => ({
142
- appliedTiers: [],
143
- injectionMode: "full" as const,
144
- exhausted: false,
154
+ mock.module(
155
+ "../plugins/defaults/compaction/context-overflow-reducer.js",
156
+ () => ({
157
+ createInitialReducerState: () => ({
158
+ appliedTiers: [],
159
+ injectionMode: "full" as const,
160
+ exhausted: false,
161
+ }),
162
+ reduceContextOverflow: async (
163
+ msgs: Message[],
164
+ cfg: unknown,
165
+ state: unknown,
166
+ ) => {
167
+ if (mockReducerStepFn) return mockReducerStepFn(msgs, cfg, state);
168
+ return {
169
+ messages: msgs,
170
+ tier: "forced_compaction",
171
+ state: {
172
+ appliedTiers: [
173
+ "forced_compaction",
174
+ "tool_result_truncation",
175
+ "media_stubbing",
176
+ "injection_downgrade",
177
+ ],
178
+ injectionMode: "full",
179
+ exhausted: true,
180
+ },
181
+ estimatedTokens: 1000,
182
+ };
183
+ },
145
184
  }),
146
- reduceContextOverflow: async (
147
- msgs: Message[],
148
- cfg: unknown,
149
- state: unknown,
150
- ) => {
151
- if (mockReducerStepFn) return mockReducerStepFn(msgs, cfg, state);
152
- return {
153
- messages: msgs,
154
- tier: "forced_compaction",
155
- state: {
156
- appliedTiers: [
157
- "forced_compaction",
158
- "tool_result_truncation",
159
- "media_stubbing",
160
- "injection_downgrade",
161
- ],
162
- injectionMode: "full",
163
- exhausted: true,
164
- },
165
- estimatedTokens: 1000,
166
- };
167
- },
168
- }));
185
+ );
169
186
 
170
187
  // Policy: default to fail_gracefully
171
188
  let mockOverflowAction: string = "fail_gracefully";
@@ -471,7 +488,7 @@ function makeCtx(
471
488
  toolExecutor,
472
489
  });
473
490
 
474
- return {
491
+ const ctx = {
475
492
  conversationId: "test-conv",
476
493
  messages: [
477
494
  { role: "user", content: [{ type: "text", text: "Hello" }] },
@@ -582,6 +599,8 @@ function makeCtx(
582
599
 
583
600
  ...ctxOverrides,
584
601
  } as unknown as Conversation;
602
+ fakeContextWindowManagers.set(conversationId, ctx.contextWindowManager);
603
+ return ctx;
585
604
  }
586
605
 
587
606
  /**
@@ -28,6 +28,20 @@ let mockUiConfig: { userTimezone?: string; detectedTimezone?: string } = {};
28
28
 
29
29
  // ── Module mocks (must precede imports of the module under test) ─────
30
30
 
31
+ // The real AgentLoop resolves the per-conversation ContextWindowManager from
32
+ // the compaction store keyed by conversationId. These orchestrator tests build
33
+ // fake conversations whose manager is a canned stub, so register each stub in a
34
+ // map the mocked store reads from.
35
+ const fakeContextWindowManagers = new Map<string, unknown>();
36
+ mock.module("../plugins/defaults/compaction/manager-store.js", () => ({
37
+ createContextWindowManager: () => undefined,
38
+ getContextWindowManager: (conversationId: string) =>
39
+ fakeContextWindowManagers.get(conversationId),
40
+ disposeContextWindowManager: (conversationId: string) => {
41
+ fakeContextWindowManagers.delete(conversationId);
42
+ },
43
+ }));
44
+
31
45
  mock.module("../util/logger.js", () => ({
32
46
  getLogger: () =>
33
47
  new Proxy({} as Record<string, unknown>, { get: () => () => {} }),
@@ -100,35 +114,38 @@ mock.module("../context/token-estimator.js", () => ({
100
114
  let mockReducerStepFn:
101
115
  | ((msgs: Message[], cfg: unknown, state: unknown) => unknown)
102
116
  | null = null;
103
- mock.module("../daemon/context-overflow-reducer.js", () => ({
104
- createInitialReducerState: () => ({
105
- appliedTiers: [],
106
- injectionMode: "full" as const,
107
- exhausted: false,
117
+ mock.module(
118
+ "../plugins/defaults/compaction/context-overflow-reducer.js",
119
+ () => ({
120
+ createInitialReducerState: () => ({
121
+ appliedTiers: [],
122
+ injectionMode: "full" as const,
123
+ exhausted: false,
124
+ }),
125
+ reduceContextOverflow: async (
126
+ msgs: Message[],
127
+ cfg: unknown,
128
+ state: unknown,
129
+ ) => {
130
+ if (mockReducerStepFn) return mockReducerStepFn(msgs, cfg, state);
131
+ return {
132
+ messages: msgs,
133
+ tier: "forced_compaction",
134
+ state: {
135
+ appliedTiers: [
136
+ "forced_compaction",
137
+ "tool_result_truncation",
138
+ "media_stubbing",
139
+ "injection_downgrade",
140
+ ],
141
+ injectionMode: "full",
142
+ exhausted: true,
143
+ },
144
+ estimatedTokens: 1000,
145
+ };
146
+ },
108
147
  }),
109
- reduceContextOverflow: async (
110
- msgs: Message[],
111
- cfg: unknown,
112
- state: unknown,
113
- ) => {
114
- if (mockReducerStepFn) return mockReducerStepFn(msgs, cfg, state);
115
- return {
116
- messages: msgs,
117
- tier: "forced_compaction",
118
- state: {
119
- appliedTiers: [
120
- "forced_compaction",
121
- "tool_result_truncation",
122
- "media_stubbing",
123
- "injection_downgrade",
124
- ],
125
- injectionMode: "full",
126
- exhausted: true,
127
- },
128
- estimatedTokens: 1000,
129
- };
130
- },
131
- }));
148
+ );
132
149
 
133
150
  // Policy: default to fail_gracefully
134
151
  let mockOverflowAction: string = "fail_gracefully";
@@ -520,17 +537,6 @@ mock.module("../memory/llm-request-log-store.js", () => ({
520
537
  setAgentLoopExitReasonOnLatestLog: setAgentLoopExitReasonOnLatestLogMock,
521
538
  }));
522
539
 
523
- let mockHasProactiveArtifactCompleted = true;
524
- let mockTryClaimProactiveArtifactTrigger = false;
525
- const runProactiveArtifactJobMock = mock(
526
- async (_params: Record<string, unknown>) => {},
527
- );
528
- mock.module("../proactive-artifact/index.js", () => ({
529
- hasProactiveArtifactCompleted: () => mockHasProactiveArtifactCompleted,
530
- tryClaimProactiveArtifactTrigger: () => mockTryClaimProactiveArtifactTrigger,
531
- runProactiveArtifactJob: runProactiveArtifactJobMock,
532
- }));
533
-
534
540
  // ── Imports (after mocks) ────────────────────────────────────────────
535
541
 
536
542
  import { AgentLoop } from "../agent/loop.js";
@@ -594,7 +600,7 @@ function makeCtx(
594
600
  toolExecutor,
595
601
  });
596
602
 
597
- return {
603
+ const ctx = {
598
604
  conversationId: "test-conv",
599
605
  messages: [
600
606
  { role: "user", content: [{ type: "text", text: "Hello" }] },
@@ -710,6 +716,8 @@ function makeCtx(
710
716
 
711
717
  ...ctxOverrides,
712
718
  } as unknown as Conversation;
719
+ fakeContextWindowManagers.set(conversationId, ctx.contextWindowManager);
720
+ return ctx;
713
721
  }
714
722
 
715
723
  type CompactionResult = Parameters<typeof applyCompactionResult>[1];
@@ -765,9 +773,6 @@ beforeEach(() => {
765
773
  title: null,
766
774
  };
767
775
  mockMessageById = null;
768
- mockHasProactiveArtifactCompleted = true;
769
- mockTryClaimProactiveArtifactTrigger = false;
770
- runProactiveArtifactJobMock.mockClear();
771
776
  setConversationHistoryStrippedAtMock.mockClear();
772
777
  setConversationHistoryStrippedAtMock.mockImplementation(() => {});
773
778
  applyRuntimeInjectionsMock.mockClear();
@@ -862,59 +867,6 @@ describe("session-agent-loop", () => {
862
867
  });
863
868
  });
864
869
 
865
- describe("proactive artifact trigger", () => {
866
- test("does not start proactive artifact jobs after foreground user turns", async () => {
867
- mockConversationRow = {
868
- ...mockConversationRow,
869
- id: "test-conv",
870
- conversationType: "standard",
871
- };
872
- mockMessageById = {
873
- id: "user-msg-1",
874
- conversationId: "test-conv",
875
- createdAt: 1000,
876
- };
877
- mockHasProactiveArtifactCompleted = false;
878
- mockTryClaimProactiveArtifactTrigger = true;
879
-
880
- // A two-call agent turn: the model invokes `app_create`, then wraps up
881
- // with a final text reply.
882
- const ctx = makeCtx({
883
- conversationId: "test-conv",
884
- providerResponses: [
885
- {
886
- content: [
887
- { type: "text", text: "I'll build that app." },
888
- {
889
- type: "tool_use",
890
- id: "tool-1",
891
- name: "app_create",
892
- input: { name: "Flow" },
893
- },
894
- ],
895
- model: "mock-model",
896
- usage: { inputTokens: 10, outputTokens: 5 },
897
- stopReason: "tool_use",
898
- },
899
- textResponse("Done."),
900
- ],
901
- toolExecutor: async () => ({ content: "{}", isError: false }),
902
- });
903
- await runAgentLoopImpl(
904
- ctx,
905
- "build a kanban app",
906
- "user-msg-1",
907
- () => {},
908
- {
909
- isUserMessage: true,
910
- },
911
- );
912
- await new Promise((resolve) => setTimeout(resolve, 0));
913
-
914
- expect(runProactiveArtifactJobMock).toHaveBeenCalledTimes(0);
915
- });
916
- });
917
-
918
870
  describe("disk pressure injection context", () => {
919
871
  // The loop sets `ctx.diskPressureCleanupModeActive` for the duration of the
920
872
  // turn (the disk-pressure-warning injector reads it via the per-conversation
@@ -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",
@@ -807,7 +807,12 @@ describe("web_search_tool_result structural guard", () => {
807
807
  // Media token counting iterates tool_result.contentBlocks for nested
808
808
  // image/file blocks. web_search_tool_result has opaque content with no
809
809
  // contentBlocks property, so it cannot contain nested media.
810
- "daemon/context-overflow-reducer.ts",
810
+ "plugins/defaults/compaction/context-overflow-reducer.ts",
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",
811
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`)
@@ -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() {}
@@ -100,56 +100,59 @@ mock.module("../context/token-estimator.js", () => ({
100
100
  // but these tests exercise the Conversation-level flow, not the reducer internals.
101
101
  // The reducer mock delegates to the compactFn to simulate a forced compaction
102
102
  // tier, matching the real reducer's behavior for Tier 1.
103
- mock.module("../daemon/context-overflow-reducer.js", () => ({
104
- createInitialReducerState: () => ({
105
- appliedTiers: [],
106
- injectionMode: "full" as const,
107
- exhausted: false,
108
- }),
109
- reduceContextOverflow: async (
110
- msgs: Message[],
111
- _cfg: unknown,
112
- _state: unknown,
113
- compactFn?: (
114
- m: Message[],
115
- s: AbortSignal | undefined,
116
- o: Record<string, unknown>,
117
- ) => Promise<{
118
- compacted: boolean;
119
- messages: Message[];
120
- compactedPersistedMessages?: number;
121
- summaryText?: string;
122
- [k: string]: unknown;
123
- }>,
124
- signal?: AbortSignal,
125
- ) => {
126
- let resultMessages = msgs;
127
- let compactionResult;
128
- if (compactFn) {
129
- const cr = await compactFn(msgs, signal, { force: true });
130
- if (cr.compacted) {
131
- resultMessages = cr.messages;
132
- compactionResult = cr;
103
+ mock.module(
104
+ "../plugins/defaults/compaction/context-overflow-reducer.js",
105
+ () => ({
106
+ createInitialReducerState: () => ({
107
+ appliedTiers: [],
108
+ injectionMode: "full" as const,
109
+ exhausted: false,
110
+ }),
111
+ reduceContextOverflow: async (
112
+ msgs: Message[],
113
+ _cfg: unknown,
114
+ _state: unknown,
115
+ compactFn?: (
116
+ m: Message[],
117
+ s: AbortSignal | undefined,
118
+ o: Record<string, unknown>,
119
+ ) => Promise<{
120
+ compacted: boolean;
121
+ messages: Message[];
122
+ compactedPersistedMessages?: number;
123
+ summaryText?: string;
124
+ [k: string]: unknown;
125
+ }>,
126
+ signal?: AbortSignal,
127
+ ) => {
128
+ let resultMessages = msgs;
129
+ let compactionResult;
130
+ if (compactFn) {
131
+ const cr = await compactFn(msgs, signal, { force: true });
132
+ if (cr.compacted) {
133
+ resultMessages = cr.messages;
134
+ compactionResult = cr;
135
+ }
133
136
  }
134
- }
135
- return {
136
- messages: resultMessages,
137
- tier: "forced_compaction",
138
- state: {
139
- appliedTiers: [
140
- "forced_compaction",
141
- "tool_result_truncation",
142
- "media_stubbing",
143
- "injection_downgrade",
144
- ],
145
- injectionMode: "full",
146
- exhausted: true,
147
- },
148
- estimatedTokens: 1000,
149
- compactionResult,
150
- };
151
- },
152
- }));
137
+ return {
138
+ messages: resultMessages,
139
+ tier: "forced_compaction",
140
+ state: {
141
+ appliedTiers: [
142
+ "forced_compaction",
143
+ "tool_result_truncation",
144
+ "media_stubbing",
145
+ "injection_downgrade",
146
+ ],
147
+ injectionMode: "full",
148
+ exhausted: true,
149
+ },
150
+ estimatedTokens: 1000,
151
+ compactionResult,
152
+ };
153
+ },
154
+ }),
155
+ );
153
156
 
154
157
  mock.module("../daemon/context-overflow-policy.js", () => ({
155
158
  resolveOverflowAction: () => "fail_gracefully",
@@ -218,7 +221,7 @@ mock.module("../memory/retriever.js", () => ({
218
221
  let maybeCompactCalls: Array<{ force: boolean }> = [];
219
222
  let forceCompactionEnabled = false;
220
223
 
221
- mock.module("../context/window-manager.js", () => ({
224
+ mock.module("../plugins/defaults/compaction/window-manager.js", () => ({
222
225
  ContextWindowManager: class {
223
226
  constructor() {}
224
227
  updateConfig() {}
@@ -262,7 +262,7 @@ mock.module("../memory/retriever.js", () => ({
262
262
  injectMemoryRecallAsUserBlock: (msgs: Message[]) => msgs,
263
263
  }));
264
264
 
265
- mock.module("../context/window-manager.js", () => ({
265
+ mock.module("../plugins/defaults/compaction/window-manager.js", () => ({
266
266
  ContextWindowManager: class {
267
267
  constructor() {}
268
268
  updateConfig() {}