@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.
- package/docs/activation-funnel-telemetry.md +310 -0
- package/openapi.yaml +16 -115
- package/package.json +1 -1
- package/src/__tests__/activation-early-marking.test.ts +120 -0
- package/src/__tests__/agent-loop-output-hooks.test.ts +13 -13
- package/src/__tests__/anthropic-provider.test.ts +23 -8
- package/src/__tests__/approval-cascade.test.ts +1 -1
- package/src/__tests__/compaction-direct.test.ts +32 -18
- package/src/__tests__/compaction-events.test.ts +2 -2
- package/src/__tests__/compaction.benchmark.test.ts +1 -1
- package/src/__tests__/context-overflow-reducer.test.ts +5 -5
- package/src/__tests__/context-window-manager-compact-retry.test.ts +121 -15
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
- package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
- package/src/__tests__/conversation-error.test.ts +15 -1
- package/src/__tests__/conversation-history-web-search.test.ts +5 -0
- package/src/__tests__/conversation-media-retry.test.ts +1 -1
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +40 -0
- package/src/__tests__/conversation-process-callsite.test.ts +1 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
- package/src/__tests__/conversation-queue.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +71 -0
- package/src/__tests__/conversation-slash-queue.test.ts +1 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
- package/src/__tests__/conversation-speed-override.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-activation-emit.test.ts +395 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +44 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +73 -4
- package/src/__tests__/conversation-undo.test.ts +2 -2
- package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/cu-unified-flow.test.ts +36 -0
- package/src/__tests__/history-repair-hook.test.ts +2 -0
- package/src/__tests__/llm-resolver.test.ts +73 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +1 -2
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +145 -0
- package/src/__tests__/persist-unsendable-image.test.ts +97 -1
- package/src/__tests__/plugin-external-api.test.ts +68 -0
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +69 -0
- package/src/__tests__/published-app-updater.test.ts +138 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +5 -7
- package/src/__tests__/title-generate-hook.test.ts +2 -0
- package/src/__tests__/web-fetch.test.ts +45 -0
- package/src/acp/__tests__/helpers/acp-config-stub.ts +0 -2
- package/src/acp/resolve-agent.test.ts +0 -56
- package/src/acp/resolve-agent.ts +10 -38
- package/src/agent/loop.ts +13 -27
- package/src/api/responses/memory-v3-selection-log.ts +19 -10
- package/src/cli/commands/__tests__/memory-v3.test.ts +191 -210
- package/src/cli/commands/memory-v3.ts +57 -199
- package/src/cli/lib/__tests__/install-from-github.test.ts +232 -29
- package/src/cli/lib/__tests__/plugin-details.test.ts +28 -19
- package/src/cli/lib/__tests__/plugin-marketplace.test.ts +57 -7
- package/src/cli/lib/__tests__/search-plugins.test.ts +17 -10
- package/src/cli/lib/install-from-github.ts +258 -41
- package/src/cli/lib/plugin-details.ts +20 -13
- package/src/cli/lib/plugin-marketplace.ts +23 -5
- package/src/cli/lib/search-plugins.ts +14 -8
- package/src/config/acp-defaults.ts +3 -3
- package/src/config/acp-schema.ts +1 -7
- package/src/config/bundled-skills/acp/SKILL.md +4 -17
- package/src/config/bundled-skills/acp/TOOLS.json +2 -2
- package/src/config/call-site-defaults.ts +0 -1
- package/src/config/feature-flag-registry.json +3 -18
- package/src/config/llm-resolver.ts +39 -7
- package/src/config/schemas/__tests__/memory-v3.test.ts +25 -9
- package/src/config/schemas/call-site-catalog.ts +0 -7
- package/src/config/schemas/llm.ts +17 -1
- package/src/config/schemas/memory-v3.ts +58 -8
- package/src/config/seed-inference-profiles.ts +18 -0
- package/src/context/post-turn-tool-result-truncation.ts +39 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +8 -1
- package/src/daemon/conversation-agent-loop.ts +87 -95
- package/src/daemon/conversation-error.ts +31 -4
- package/src/daemon/conversation-history.ts +1 -1
- package/src/daemon/conversation-media-retry.ts +19 -6
- package/src/daemon/conversation-messaging.ts +17 -0
- package/src/daemon/conversation-process.ts +14 -5
- package/src/daemon/conversation-queue-manager.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +37 -1
- package/src/daemon/conversation-surfaces.ts +141 -3
- package/src/daemon/conversation.ts +48 -13
- package/src/daemon/external-plugins-bootstrap.ts +8 -3
- package/src/daemon/persist-unsendable-image.ts +62 -25
- package/src/daemon/process-message.ts +1 -1
- package/src/daemon/tool-side-effects.ts +15 -0
- package/src/memory/__tests__/activation-session-store.test.ts +41 -0
- package/src/memory/__tests__/onboarding-events-store.test.ts +80 -0
- package/src/memory/activation-session-store.ts +43 -0
- package/src/memory/db-init.ts +4 -0
- package/src/memory/migrations/273-onboarding-events-funnel-columns.ts +46 -0
- package/src/memory/migrations/274-create-activation-sessions.ts +15 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/onboarding-events-store.ts +66 -18
- package/src/memory/schema/infrastructure.ts +13 -0
- package/src/memory/v2/__tests__/consolidation-job.test.ts +4 -97
- package/src/memory/v2/__tests__/page-store.test.ts +22 -0
- package/src/memory/v2/consolidation-job.ts +2 -72
- package/src/memory/v2/types.ts +5 -0
- package/src/messaging/providers/telegram-bot/api.ts +14 -5
- package/src/notifications/adapters/telegram.ts +7 -1
- package/src/plugin-api/constants.ts +2 -2
- package/src/plugin-api/index.ts +2 -2
- package/src/plugin-api/types.ts +19 -5
- package/src/plugins/defaults/compaction/compact.ts +24 -15
- package/src/plugins/defaults/compaction/context-overflow-reducer.ts +4 -4
- package/src/plugins/defaults/compaction/manager-store.ts +1 -1
- package/src/{context → plugins/defaults/compaction}/window-manager.ts +68 -12
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +12 -18
- package/src/plugins/defaults/memory-v3-shadow/__tests__/capabilities.test.ts +19 -66
- package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +181 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/edge.test.ts +247 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +139 -129
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +332 -164
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +516 -293
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +306 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +51 -21
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +402 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-needle.test.ts +135 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/sections.test.ts +125 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +66 -11
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +446 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +271 -110
- package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +0 -22
- package/src/plugins/defaults/memory-v3-shadow/capabilities.ts +26 -62
- package/src/plugins/defaults/memory-v3-shadow/dense.ts +97 -0
- package/src/plugins/defaults/memory-v3-shadow/edge.ts +252 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +3 -2
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +415 -181
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +196 -56
- package/src/plugins/defaults/memory-v3-shadow/page-content.ts +39 -2
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +204 -0
- package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +15 -7
- package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +236 -0
- package/src/plugins/defaults/memory-v3-shadow/section-needle.ts +200 -0
- package/src/plugins/defaults/memory-v3-shadow/sections.ts +115 -0
- package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +75 -3
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +111 -78
- package/src/plugins/defaults/memory-v3-shadow/types.ts +52 -19
- package/src/plugins/defaults/memory-v3-shadow/working-set.ts +4 -1
- package/src/plugins/external-api.ts +114 -0
- package/src/prompts/system-prompt.ts +61 -10
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +37 -2
- package/src/providers/anthropic/client.ts +9 -10
- package/src/providers/inference/kimi-cjk-token-ids.ts +493 -0
- package/src/providers/inference/logit-bias.ts +55 -0
- package/src/providers/openai/__tests__/vision-not-supported.test.ts +75 -0
- package/src/providers/openai/chat-completions-provider.ts +44 -1
- package/src/providers/retry.ts +22 -0
- package/src/providers/types.ts +6 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +112 -0
- package/src/runtime/routes/acp-routes.test.ts +3 -20
- package/src/runtime/routes/app-management-routes.ts +3 -0
- package/src/runtime/routes/conversation-routes.ts +2 -0
- package/src/runtime/routes/memory-v3-routes.ts +64 -314
- package/src/runtime/routes/playground/__tests__/force-compact.test.ts +1 -1
- package/src/runtime/routes/stt-routes.ts +45 -12
- package/src/runtime/routes/workspace-routes.ts +50 -15
- package/src/services/published-app-updater.ts +30 -8
- package/src/telemetry/__tests__/activation-funnel.test.ts +95 -0
- package/src/telemetry/activation-funnel.ts +167 -0
- package/src/telemetry/types.ts +13 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +154 -0
- package/src/telemetry/usage-telemetry-reporter.ts +26 -1
- package/src/tools/acp/list-agents.test.ts +2 -18
- package/src/tools/acp/list-agents.ts +3 -15
- package/src/tools/acp/spawn.test.ts +0 -10
- package/src/tools/browser/browser-execution.ts +12 -2
- package/src/tools/network/web-fetch.ts +65 -24
- package/src/tools/skills/load.ts +1 -1
- package/src/tools/ui-surface/definitions.ts +7 -0
- package/src/acp/feature-gate.test.ts +0 -48
- package/src/acp/feature-gate.ts +0 -34
- package/src/plugins/defaults/memory-v3-shadow/__tests__/assign.test.ts +0 -242
- package/src/plugins/defaults/memory-v3-shadow/__tests__/core.test.ts +0 -39
- package/src/plugins/defaults/memory-v3-shadow/__tests__/health.test.ts +0 -219
- package/src/plugins/defaults/memory-v3-shadow/__tests__/needle.test.ts +0 -107
- package/src/plugins/defaults/memory-v3-shadow/__tests__/provider-blocks.test.ts +0 -13
- package/src/plugins/defaults/memory-v3-shadow/__tests__/reconcile.test.ts +0 -274
- package/src/plugins/defaults/memory-v3-shadow/__tests__/router.test.ts +0 -337
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selector.test.ts +0 -470
- package/src/plugins/defaults/memory-v3-shadow/__tests__/snapshot.test.ts +0 -168
- package/src/plugins/defaults/memory-v3-shadow/__tests__/tree.test.ts +0 -192
- package/src/plugins/defaults/memory-v3-shadow/assign.ts +0 -272
- package/src/plugins/defaults/memory-v3-shadow/core.ts +0 -26
- package/src/plugins/defaults/memory-v3-shadow/health.ts +0 -0
- package/src/plugins/defaults/memory-v3-shadow/needle.ts +0 -115
- package/src/plugins/defaults/memory-v3-shadow/provider-blocks.ts +0 -26
- package/src/plugins/defaults/memory-v3-shadow/reconcile.ts +0 -527
- package/src/plugins/defaults/memory-v3-shadow/router.ts +0 -190
- package/src/plugins/defaults/memory-v3-shadow/selector.ts +0 -226
- package/src/plugins/defaults/memory-v3-shadow/snapshot.ts +0 -209
- package/src/plugins/defaults/memory-v3-shadow/tree.ts +0 -174
- package/src/util/map-limit.ts +0 -27
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for the `pre-model-call` and `
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
70
|
+
...(hooks.postModelCall
|
|
71
71
|
? {
|
|
72
|
-
"
|
|
73
|
-
hooks.
|
|
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("
|
|
86
|
+
test("post-model-call transforms the persisted message content", async () => {
|
|
87
87
|
registerOutputHookPlugin({
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
//
|
|
329
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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("../
|
|
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
|
|
7
|
-
*
|
|
8
|
-
* conversational options verbatim. The orchestrator
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
74
|
-
return
|
|
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
|
|
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
|
-
|
|
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
|
|
122
|
+
const observed = registerManager("conv-options", makeResult());
|
|
109
123
|
|
|
110
124
|
// WHEN defaultCompact runs with force/profile/trust options
|
|
111
125
|
await defaultCompact({
|
|
112
|
-
|
|
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
|
|
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("../
|
|
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 "../
|
|
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 "../
|
|
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("../
|
|
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("../
|
|
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("../
|
|
252
|
+
mock.module("../plugins/defaults/compaction/window-manager.js", () => ({
|
|
253
253
|
ContextWindowManager: class {
|
|
254
254
|
constructor() {}
|
|
255
255
|
updateConfig() {}
|