@vellumai/assistant 0.8.11 → 0.8.12-staging.1
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/ARCHITECTURE.md +15 -17
- package/README.md +0 -6
- package/bun.lock +6 -122
- package/node_modules/@vellumai/gateway-client/bun.lock +1 -0
- package/node_modules/@vellumai/gateway-client/package.json +3 -1
- package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -1
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +87 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +3 -5
- package/openapi.yaml +126 -4
- package/package.json +1 -3
- package/src/__tests__/adaptive-thinking-repair.test.ts +185 -0
- package/src/__tests__/agent-loop-compaction-events.test.ts +7 -6
- package/src/__tests__/anthropic-provider.test.ts +129 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +4 -1
- package/src/__tests__/btw-routes.test.ts +7 -34
- package/src/__tests__/checker.test.ts +6 -12
- package/src/__tests__/config-loader-backfill.test.ts +4 -2
- package/src/__tests__/config-loader-quarantine-notice.test.ts +167 -0
- package/src/__tests__/config-watcher.test.ts +2 -2
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
- package/src/__tests__/conversation-error.test.ts +2 -6
- package/src/__tests__/conversation-history-web-search.test.ts +8 -0
- package/src/__tests__/conversation-title-service.test.ts +2 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -1
- package/src/__tests__/disk-pressure-tools.test.ts +1 -1
- package/src/__tests__/exploration-drift-hook.test.ts +692 -0
- package/src/__tests__/filing-service.test.ts +8 -3
- package/src/__tests__/guardian-action-store.test.ts +0 -167
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +1 -1
- package/src/__tests__/heartbeat-disk-pressure.test.ts +4 -1
- package/src/__tests__/heartbeat-service.test.ts +5 -2
- package/src/__tests__/identity-intro-cache.test.ts +12 -5
- package/src/__tests__/identity-routes.test.ts +16 -57
- package/src/__tests__/injector-chain.test.ts +8 -3
- package/src/__tests__/injector-config-quarantine-notice.test.ts +115 -0
- package/src/__tests__/llm-usage-store.test.ts +11 -0
- package/src/__tests__/memory-v2-static-injector.test.ts +22 -0
- package/src/__tests__/model-intents.test.ts +1 -1
- package/src/__tests__/oauth-cli.test.ts +19 -8
- package/src/__tests__/openai-provider.test.ts +34 -0
- package/src/__tests__/prechat-onboarding-contract.test.ts +0 -1
- package/src/__tests__/recurrence-engine.test.ts +45 -0
- package/src/__tests__/schedule-routes.test.ts +34 -0
- package/src/__tests__/scheduler-disk-pressure.test.ts +1 -1
- package/src/__tests__/script-proxy-conversation-manager.test.ts +10 -5
- package/src/__tests__/skill-tool-factory.test.ts +49 -0
- package/src/__tests__/subagent-role-registry.test.ts +24 -1
- package/src/__tests__/subagent-tools.test.ts +1 -0
- package/src/__tests__/system-prompt.test.ts +109 -11
- package/src/__tests__/tool-error-hook.test.ts +1 -0
- package/src/__tests__/tool-result-spool.test.ts +337 -0
- package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
- package/src/__tests__/validate-input.test.ts +95 -1
- package/src/__tests__/workspace-migration-098-remove-stale-updates-bulletin-file.test.ts +65 -0
- package/src/__tests__/workspace-migration-099-disable-cache-one-shot-callsites.test.ts +139 -0
- package/src/__tests__/workspace-release-notes-feature-flag-guard.test.ts +45 -95
- package/src/agent/loop.ts +81 -24
- package/src/api/events/usage-progress.ts +28 -0
- package/src/api/index.ts +6 -0
- package/src/background-wake/wake-intent-hooks.test.ts +2 -0
- package/src/bundler/app-bundler.ts +25 -42
- package/src/calls/call-controller.ts +1 -1
- package/src/cli/commands/plugins.ts +248 -15
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +318 -0
- package/src/cli/lib/__tests__/install-from-github.test.ts +16 -9
- package/src/cli/lib/__tests__/plugin-artifact.test.ts +183 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +158 -0
- package/src/cli/lib/__tests__/plugin-fingerprint.test.ts +245 -0
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +301 -0
- package/src/cli/lib/inspect-plugin.ts +252 -0
- package/src/cli/lib/install-from-github.ts +214 -21
- package/src/cli/lib/list-installed-plugins.ts +17 -6
- package/src/cli/lib/plugin-artifact.ts +103 -0
- package/src/cli/lib/plugin-details.ts +18 -1
- package/src/cli/lib/plugin-fingerprint.ts +197 -0
- package/src/cli/lib/upgrade-plugin.ts +219 -0
- package/src/config/bundled-skills/subagent/SKILL.md +2 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +8 -2
- package/src/config/call-site-defaults.ts +13 -2
- package/src/config/feature-flag-registry.json +8 -16
- package/src/config/loader.ts +52 -59
- package/src/config/schema.ts +0 -2
- package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
- package/src/config/schemas/__tests__/memory-v3.test.ts +10 -0
- package/src/config/schemas/llm.ts +10 -0
- package/src/config/schemas/memory-v2.ts +13 -0
- package/src/config/schemas/memory-v3.ts +92 -0
- package/src/context/post-turn-tool-result-truncation.ts +32 -18
- package/src/context/tool-result-spool.ts +104 -0
- package/src/credential-execution/feature-gates.ts +0 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +41 -16
- package/src/daemon/conversation-error.ts +6 -15
- package/src/daemon/conversation.ts +9 -0
- package/src/daemon/disk-pressure-policy.ts +0 -1
- package/src/daemon/lifecycle.ts +1 -20
- package/src/daemon/message-types/conversations.ts +2 -15
- package/src/daemon/trust-context.ts +1 -1
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +1 -1
- package/src/home/__tests__/home-content-refresh.test.ts +114 -0
- package/src/home/__tests__/suggested-prompts.test.ts +86 -5
- package/src/home/home-content-refresh.ts +43 -31
- package/src/home/home-greeting-cache.ts +8 -1
- package/src/home/home-greeting.ts +13 -9
- package/src/home/suggested-prompts.ts +77 -24
- package/src/ipc/routes/trust-rules.test.ts +66 -72
- package/src/media/image-credentials.ts +2 -2
- package/src/memory/__tests__/compaction-log-store-clickhouse.test.ts +432 -0
- package/src/memory/{compaction-log-writer-clickhouse.ts → compaction-log-store-clickhouse.ts} +264 -55
- package/src/memory/conversation-attention-store.ts +1 -0
- package/src/memory/conversation-bootstrap.ts +18 -9
- package/src/memory/conversation-crud.ts +12 -2
- package/src/memory/conversation-title-service.ts +53 -9
- package/src/memory/delivery-channels.ts +0 -69
- package/src/memory/graph/extraction-job.ts +0 -15
- package/src/memory/guardian-action-store.ts +1 -376
- package/src/memory/llm-usage-store.ts +5 -1
- package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +2 -2
- package/src/memory/v2/__tests__/consolidation-job.test.ts +183 -2
- package/src/memory/v2/__tests__/injection.test.ts +70 -0
- package/src/memory/v2/__tests__/static-context.test.ts +12 -0
- package/src/memory/v2/consolidation-job.ts +93 -9
- package/src/memory/v2/injection.ts +53 -0
- package/src/memory/v2/prompts/consolidation.ts +1 -0
- package/src/memory/v2/static-context.ts +13 -1
- package/src/memory/v2/sweep-job.ts +1 -1
- package/src/memory/v2/types.ts +5 -0
- package/src/plugin-api/types.ts +7 -0
- package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +300 -0
- package/src/plugins/defaults/exploration-drift/package.json +15 -0
- package/src/plugins/defaults/index.ts +25 -0
- package/src/plugins/defaults/memory-retrieval/injectors.ts +132 -4
- package/src/plugins/defaults/memory-v3-shadow/__tests__/card.test.ts +92 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +2 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/fresh-set.test.ts +52 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +2 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +136 -5
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +17 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +6 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +5 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +68 -4
- package/src/plugins/defaults/memory-v3-shadow/card.ts +49 -5
- package/src/plugins/defaults/memory-v3-shadow/fresh-set.ts +59 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +4 -2
- package/src/plugins/defaults/memory-v3-shadow/learned-edges.test.ts +169 -0
- package/src/plugins/defaults/memory-v3-shadow/learned-edges.ts +178 -0
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +115 -26
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +13 -9
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +144 -22
- package/src/plugins/defaults/memory-v3-shadow/types.ts +24 -6
- package/src/plugins/defaults/title-generate/hooks/stop.ts +13 -0
- package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +16 -0
- package/src/prompts/cache-boundary.ts +17 -0
- package/src/prompts/sections.ts +50 -17
- package/src/prompts/system-prompt.ts +12 -4
- package/src/prompts/templates/system-sections.ts +22 -0
- package/src/providers/anthropic/client.ts +74 -28
- package/src/providers/gemini/client.ts +5 -1
- package/src/providers/minimax/client.ts +9 -0
- package/src/providers/model-intents.ts +2 -2
- package/src/providers/openai/chat-completions-provider.ts +2 -1
- package/src/providers/openai/responses-provider.ts +5 -1
- package/src/providers/retry.ts +8 -0
- package/src/providers/types.ts +11 -0
- package/src/runtime/AGENTS.md +6 -0
- package/src/runtime/__tests__/agent-wake.test.ts +2 -2
- package/src/runtime/agent-wake.ts +5 -5
- package/src/runtime/background-job-runner.ts +2 -2
- package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +150 -3
- package/src/runtime/migrations/vbundle-import-analyzer.ts +29 -6
- package/src/runtime/migrations/vbundle-import-policy.ts +23 -0
- package/src/runtime/migrations/vbundle-importer.ts +9 -4
- package/src/runtime/migrations/vbundle-streaming-importer.ts +8 -3
- package/src/runtime/pre-first-message-gate.ts +1 -1
- package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +241 -0
- package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +97 -185
- package/src/runtime/routes/__tests__/home-feed-routes.test.ts +17 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +1 -0
- package/src/runtime/routes/__tests__/task-routes.test.ts +3 -3
- package/src/runtime/routes/btw-routes.ts +0 -14
- package/src/runtime/routes/conversation-compaction-routes.ts +86 -19
- package/src/runtime/routes/conversation-list-routes.ts +77 -5
- package/src/runtime/routes/conversation-management-routes.ts +54 -0
- package/src/runtime/routes/gateway-log-routes.ts +14 -64
- package/src/runtime/routes/home-feed-routes.ts +10 -0
- package/src/runtime/routes/identity-intro-cache.ts +1 -1
- package/src/runtime/routes/identity-routes.ts +76 -20
- package/src/runtime/routes/inbound-message-handler.ts +0 -36
- package/src/runtime/routes/plugins-routes.ts +21 -0
- package/src/runtime/routes/schedule-routes.ts +19 -2
- package/src/runtime/routes/trust-rules-routes.ts +14 -67
- package/src/schedule/recurrence-engine.ts +34 -0
- package/src/schedule/scheduler.ts +1 -0
- package/src/skills/validate-input.ts +41 -1
- package/src/subagent/types.ts +26 -1
- package/src/telemetry/types.ts +15 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +6 -1
- package/src/telemetry/usage-telemetry-reporter.ts +1 -0
- package/src/tools/apps/executors.ts +1 -1
- package/src/tools/skills/skill-tool-factory.ts +19 -8
- package/src/usage/types.ts +8 -1
- package/src/util/platform.ts +16 -0
- package/src/watcher/engine.ts +1 -0
- package/src/workspace/adaptive-thinking-repair.ts +113 -0
- package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +70 -67
- package/src/workspace/migrations/098-remove-stale-updates-bulletin-file.ts +31 -0
- package/src/workspace/migrations/099-disable-cache-one-shot-callsites.ts +81 -0
- package/src/workspace/migrations/registry.ts +4 -0
- package/src/__tests__/config-loader-quarantine-bulletin.test.ts +0 -202
- package/src/__tests__/conversation-starters-cadence.test.ts +0 -161
- package/src/__tests__/guardian-action-followup-executor.test.ts +0 -322
- package/src/__tests__/guardian-action-followup-store.test.ts +0 -373
- package/src/__tests__/guardian-action-late-reply.test.ts +0 -1083
- package/src/__tests__/update-bulletin-job.test.ts +0 -292
- package/src/config/schemas/updates.ts +0 -14
- package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +0 -227
- package/src/memory/conversation-starters-cadence.ts +0 -78
- package/src/prompts/update-bulletin-job.ts +0 -180
- package/src/runtime/guardian-action-followup-executor.ts +0 -306
|
@@ -1,123 +1,117 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Unit tests for the trust rule IPC proxy routes.
|
|
3
|
-
*
|
|
4
|
-
* Covers:
|
|
5
|
-
* - trust_rules_list: no params, tool filter, include_all, origin filter
|
|
6
|
-
* - error path: non-OK gateway response surfaces body .error message
|
|
7
3
|
*/
|
|
8
4
|
|
|
9
5
|
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
10
6
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
mock.module("../../config/env.js", () => ({
|
|
16
|
-
getGatewayInternalBaseUrl: () => "http://localhost:7822",
|
|
17
|
-
}));
|
|
18
|
-
|
|
19
|
-
type MockResponse = {
|
|
20
|
-
ok: boolean;
|
|
21
|
-
status: number;
|
|
22
|
-
json: () => Promise<unknown>;
|
|
7
|
+
type IpcCall = {
|
|
8
|
+
method: string;
|
|
9
|
+
params?: Record<string, unknown>;
|
|
23
10
|
};
|
|
24
11
|
|
|
25
|
-
let
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
json: async () => ({ rules: [] }),
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
let capturedFetchCalls: Array<{ url: string; init?: RequestInit }> = [];
|
|
12
|
+
let ipcCalls: IpcCall[] = [];
|
|
13
|
+
let ipcResult: unknown = { rules: [] };
|
|
14
|
+
let ipcError: Error | undefined;
|
|
32
15
|
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
16
|
+
const ipcCallPersistentMock = mock(
|
|
17
|
+
async (method: string, params?: Record<string, unknown>) => {
|
|
18
|
+
ipcCalls.push({ method, params });
|
|
19
|
+
if (ipcError) throw ipcError;
|
|
20
|
+
return ipcResult;
|
|
21
|
+
},
|
|
22
|
+
);
|
|
37
23
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// Import module under test AFTER mocks are set up
|
|
42
|
-
// ---------------------------------------------------------------------------
|
|
24
|
+
mock.module("../gateway-client.js", () => ({
|
|
25
|
+
ipcCallPersistent: ipcCallPersistentMock,
|
|
26
|
+
}));
|
|
43
27
|
|
|
44
28
|
import { ROUTES as trustRuleRoutes } from "../../runtime/routes/trust-rules-routes.js";
|
|
45
29
|
|
|
46
|
-
// ---------------------------------------------------------------------------
|
|
47
|
-
// Helpers
|
|
48
|
-
// ---------------------------------------------------------------------------
|
|
49
|
-
|
|
50
30
|
function findRoute(method: string) {
|
|
51
31
|
const route = trustRuleRoutes.find((r) => r.operationId === method);
|
|
52
32
|
if (!route) throw new Error(`Route not found: ${method}`);
|
|
53
33
|
return route;
|
|
54
34
|
}
|
|
55
35
|
|
|
56
|
-
// ---------------------------------------------------------------------------
|
|
57
|
-
// Tests
|
|
58
|
-
// ---------------------------------------------------------------------------
|
|
59
|
-
|
|
60
36
|
describe("trustRuleRoutes", () => {
|
|
61
37
|
beforeEach(() => {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
json: async () => ({ rules: [] }),
|
|
67
|
-
};
|
|
68
|
-
mockFetch.mockClear();
|
|
38
|
+
ipcCalls = [];
|
|
39
|
+
ipcResult = { rules: [] };
|
|
40
|
+
ipcError = undefined;
|
|
41
|
+
ipcCallPersistentMock.mockClear();
|
|
69
42
|
});
|
|
70
43
|
|
|
71
44
|
describe("trust_rules_list", () => {
|
|
72
|
-
test("no params
|
|
45
|
+
test("no params calls trust_rules_list with empty params", async () => {
|
|
73
46
|
const route = findRoute("trust_rules_list");
|
|
74
47
|
await route.handler({ body: {} });
|
|
75
48
|
|
|
76
|
-
expect(
|
|
77
|
-
expect(capturedFetchCalls[0].url).toBe(
|
|
78
|
-
"http://localhost:7822/v1/trust-rules",
|
|
79
|
-
);
|
|
80
|
-
expect(capturedFetchCalls[0].init).toBeUndefined();
|
|
49
|
+
expect(ipcCalls).toEqual([{ method: "trust_rules_list", params: {} }]);
|
|
81
50
|
});
|
|
82
51
|
|
|
83
|
-
test("{ tool: 'bash' }
|
|
52
|
+
test("{ tool: 'bash' } is forwarded", async () => {
|
|
84
53
|
const route = findRoute("trust_rules_list");
|
|
85
54
|
await route.handler({ body: { tool: "bash" } });
|
|
86
55
|
|
|
87
|
-
expect(
|
|
88
|
-
"
|
|
89
|
-
);
|
|
56
|
+
expect(ipcCalls).toEqual([
|
|
57
|
+
{ method: "trust_rules_list", params: { tool: "bash" } },
|
|
58
|
+
]);
|
|
90
59
|
});
|
|
91
60
|
|
|
92
|
-
test("{ include_all: true }
|
|
61
|
+
test("{ include_all: true } is forwarded", async () => {
|
|
93
62
|
const route = findRoute("trust_rules_list");
|
|
94
63
|
await route.handler({ body: { include_all: true } });
|
|
95
64
|
|
|
96
|
-
expect(
|
|
97
|
-
"
|
|
98
|
-
);
|
|
65
|
+
expect(ipcCalls).toEqual([
|
|
66
|
+
{ method: "trust_rules_list", params: { include_all: true } },
|
|
67
|
+
]);
|
|
99
68
|
});
|
|
100
69
|
|
|
101
|
-
test("{ origin: 'user_defined' }
|
|
70
|
+
test("{ origin: 'user_defined' } is forwarded", async () => {
|
|
102
71
|
const route = findRoute("trust_rules_list");
|
|
103
72
|
await route.handler({ body: { origin: "user_defined" } });
|
|
104
73
|
|
|
105
|
-
expect(
|
|
106
|
-
|
|
107
|
-
|
|
74
|
+
expect(ipcCalls).toEqual([
|
|
75
|
+
{
|
|
76
|
+
method: "trust_rules_list",
|
|
77
|
+
params: { origin: "user_defined" },
|
|
78
|
+
},
|
|
79
|
+
]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("returns the parsed gateway IPC response", async () => {
|
|
83
|
+
ipcResult = {
|
|
84
|
+
rules: [
|
|
85
|
+
{
|
|
86
|
+
id: "rule-123",
|
|
87
|
+
tool: "bash",
|
|
88
|
+
pattern: "echo hello",
|
|
89
|
+
risk: "low",
|
|
90
|
+
description: "Allow echo hello",
|
|
91
|
+
origin: "user_defined",
|
|
92
|
+
userModified: false,
|
|
93
|
+
deleted: false,
|
|
94
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
95
|
+
updatedAt: "2026-01-01T00:00:00.000Z",
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const route = findRoute("trust_rules_list");
|
|
101
|
+
const result = await route.handler({ body: {} });
|
|
102
|
+
|
|
103
|
+
expect(result).toEqual(ipcResult);
|
|
108
104
|
});
|
|
109
105
|
});
|
|
110
106
|
|
|
111
107
|
describe("error path", () => {
|
|
112
|
-
test("
|
|
113
|
-
|
|
114
|
-
ok: false,
|
|
115
|
-
status: 404,
|
|
116
|
-
json: async () => ({ error: "Not found" }),
|
|
117
|
-
};
|
|
108
|
+
test("gateway IPC error is propagated", async () => {
|
|
109
|
+
ipcError = new Error("Gateway IPC call timed out");
|
|
118
110
|
|
|
119
111
|
const route = findRoute("trust_rules_list");
|
|
120
|
-
await expect(route.handler({ body: {} })).rejects.toThrow(
|
|
112
|
+
await expect(route.handler({ body: {} })).rejects.toThrow(
|
|
113
|
+
"Gateway IPC call timed out",
|
|
114
|
+
);
|
|
121
115
|
});
|
|
122
116
|
});
|
|
123
117
|
});
|
|
@@ -66,8 +66,8 @@ export async function resolveImageGenCredentials(opts: {
|
|
|
66
66
|
function providerKeyHint(provider: ImageGenProvider): string {
|
|
67
67
|
switch (provider) {
|
|
68
68
|
case "gemini":
|
|
69
|
-
return "No Gemini API key configured. Please set your Gemini API key in Settings
|
|
69
|
+
return "No Gemini API key configured. Please set your Gemini API key in Settings → Models & Services.";
|
|
70
70
|
case "openai":
|
|
71
|
-
return "No OpenAI API key configured. Please set your OpenAI API key in Settings
|
|
71
|
+
return "No OpenAI API key configured. Please set your OpenAI API key in Settings → Models & Services.";
|
|
72
72
|
}
|
|
73
73
|
}
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the ClickHouse compaction log store.
|
|
3
|
+
*
|
|
4
|
+
* The store is exercised with an injected `fetchImpl` so the tests pin the
|
|
5
|
+
* wire shape: lazy CREATE TABLE on first write, INSERT statements via the
|
|
6
|
+
* `query` URL param with JSONEachRow bodies, the start/end row mappings
|
|
7
|
+
* (sentinels on the start row, full `ContextWindowResult` projection on the
|
|
8
|
+
* end row), and the read path that pairs rows back into per-attempt events.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, expect, test } from "bun:test";
|
|
11
|
+
|
|
12
|
+
import type { ContextWindowResult } from "../../plugins/defaults/compaction/window-manager.js";
|
|
13
|
+
import {
|
|
14
|
+
ClickHouseCompactionLogStore,
|
|
15
|
+
type CompactionEndEvent,
|
|
16
|
+
type CompactionStartEvent,
|
|
17
|
+
} from "../compaction-log-store-clickhouse.js";
|
|
18
|
+
|
|
19
|
+
interface CapturedRequest {
|
|
20
|
+
url: URL;
|
|
21
|
+
body: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function createWriter(overrides?: {
|
|
25
|
+
fetchImpl?: (url: string, init?: RequestInit) => Promise<Response>;
|
|
26
|
+
}): { writer: ClickHouseCompactionLogStore; requests: CapturedRequest[] } {
|
|
27
|
+
const requests: CapturedRequest[] = [];
|
|
28
|
+
const fetchImpl = (overrides?.fetchImpl ??
|
|
29
|
+
(async (url: string, init?: RequestInit) => {
|
|
30
|
+
requests.push({ url: new URL(url), body: String(init?.body ?? "") });
|
|
31
|
+
return new Response("", { status: 200 });
|
|
32
|
+
})) as typeof fetch;
|
|
33
|
+
const writer = new ClickHouseCompactionLogStore(
|
|
34
|
+
{ database: "analytics", table: "compaction_logs", user: "writer" },
|
|
35
|
+
{
|
|
36
|
+
resolveUrl: async () => "http://clickhouse.example.com:8123",
|
|
37
|
+
resolvePassword: async () => "secret",
|
|
38
|
+
resolveAssistantId: async () => "assistant-123",
|
|
39
|
+
fetchImpl,
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
return { writer, requests };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const startEvent: CompactionStartEvent = {
|
|
46
|
+
type: "context_compacting",
|
|
47
|
+
compactionId: "comp-1",
|
|
48
|
+
requestId: "req-1",
|
|
49
|
+
trigger: "budget",
|
|
50
|
+
startedAt: 1000,
|
|
51
|
+
messages: [
|
|
52
|
+
{ role: "user", content: [{ type: "text", text: "hi" }] },
|
|
53
|
+
{ role: "assistant", content: [{ type: "text", text: "hello" }] },
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const result: ContextWindowResult = {
|
|
58
|
+
messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
59
|
+
compacted: true,
|
|
60
|
+
previousEstimatedInputTokens: 900,
|
|
61
|
+
estimatedInputTokens: 300,
|
|
62
|
+
maxInputTokens: 1000,
|
|
63
|
+
thresholdTokens: 850,
|
|
64
|
+
compactedMessages: 10,
|
|
65
|
+
compactedPersistedMessages: 8,
|
|
66
|
+
preservedTailMessages: 2,
|
|
67
|
+
summaryCalls: 1,
|
|
68
|
+
summaryInputTokens: 880,
|
|
69
|
+
summaryOutputTokens: 120,
|
|
70
|
+
summaryModel: "test-model",
|
|
71
|
+
summaryText: "summary text",
|
|
72
|
+
reason: "auto",
|
|
73
|
+
exhausted: false,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const endEvent: CompactionEndEvent = {
|
|
77
|
+
type: "compaction_completed",
|
|
78
|
+
compactionId: "comp-1",
|
|
79
|
+
requestId: "req-1",
|
|
80
|
+
trigger: "budget",
|
|
81
|
+
startedAt: 1000,
|
|
82
|
+
finishedAt: 1500,
|
|
83
|
+
...result,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
describe("ClickHouseCompactionLogStore", () => {
|
|
87
|
+
test("first write creates the table then inserts", async () => {
|
|
88
|
+
const { writer, requests } = createWriter();
|
|
89
|
+
|
|
90
|
+
await writer.writeStart("conv-1", startEvent);
|
|
91
|
+
|
|
92
|
+
expect(requests.length).toBe(2);
|
|
93
|
+
expect(requests[0]!.body).toContain("CREATE TABLE IF NOT EXISTS");
|
|
94
|
+
expect(requests[0]!.body).toContain("`compaction_logs`");
|
|
95
|
+
expect(requests[0]!.url.searchParams.get("database")).toBe("analytics");
|
|
96
|
+
|
|
97
|
+
const insert = requests[1]!;
|
|
98
|
+
expect(insert.url.searchParams.get("query")).toBe(
|
|
99
|
+
"INSERT INTO `compaction_logs` FORMAT JSONEachRow",
|
|
100
|
+
);
|
|
101
|
+
const row = JSON.parse(insert.body) as Record<string, unknown>;
|
|
102
|
+
expect(row.assistant_id).toBe("assistant-123");
|
|
103
|
+
expect(row.conversation_id).toBe("conv-1");
|
|
104
|
+
expect(row.compaction_id).toBe("comp-1");
|
|
105
|
+
expect(row.request_id).toBe("req-1");
|
|
106
|
+
expect(row.phase).toBe("start");
|
|
107
|
+
expect(row.trigger).toBe("budget");
|
|
108
|
+
expect(row.started_at).toBe(1000);
|
|
109
|
+
expect(row.pre_message_count).toBe(2);
|
|
110
|
+
// End-only fields stay at their sentinels on the start row.
|
|
111
|
+
expect(row.finished_at).toBe(0);
|
|
112
|
+
expect(row.duration_ms).toBe(-1);
|
|
113
|
+
expect(row.estimated_input_tokens).toBe(-1);
|
|
114
|
+
expect(row.summary_model).toBe("");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("table creation happens once across writes", async () => {
|
|
118
|
+
const { writer, requests } = createWriter();
|
|
119
|
+
|
|
120
|
+
await writer.writeStart("conv-1", startEvent);
|
|
121
|
+
await writer.writeEnd("conv-1", endEvent);
|
|
122
|
+
|
|
123
|
+
const ddl = requests.filter((r) => r.body.includes("CREATE TABLE"));
|
|
124
|
+
expect(ddl.length).toBe(1);
|
|
125
|
+
expect(requests.length).toBe(3);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("end row projects the full ContextWindowResult", async () => {
|
|
129
|
+
const { writer, requests } = createWriter();
|
|
130
|
+
|
|
131
|
+
await writer.writeEnd("conv-1", endEvent);
|
|
132
|
+
|
|
133
|
+
const row = JSON.parse(requests[1]!.body) as Record<string, unknown>;
|
|
134
|
+
expect(row.phase).toBe("end");
|
|
135
|
+
expect(row.finished_at).toBe(1500);
|
|
136
|
+
expect(row.duration_ms).toBe(500);
|
|
137
|
+
expect(row.result_message_count).toBe(1);
|
|
138
|
+
expect(row.compacted).toBe(1);
|
|
139
|
+
expect(row.previous_estimated_input_tokens).toBe(900);
|
|
140
|
+
expect(row.estimated_input_tokens).toBe(300);
|
|
141
|
+
expect(row.max_input_tokens).toBe(1000);
|
|
142
|
+
expect(row.threshold_tokens).toBe(850);
|
|
143
|
+
expect(row.compacted_messages).toBe(10);
|
|
144
|
+
expect(row.compacted_persisted_messages).toBe(8);
|
|
145
|
+
expect(row.preserved_tail_messages).toBe(2);
|
|
146
|
+
expect(row.summary_calls).toBe(1);
|
|
147
|
+
expect(row.summary_input_tokens).toBe(880);
|
|
148
|
+
expect(row.summary_output_tokens).toBe(120);
|
|
149
|
+
expect(row.summary_model).toBe("test-model");
|
|
150
|
+
expect(row.summary_failed).toBe(-1);
|
|
151
|
+
expect(row.reason).toBe("auto");
|
|
152
|
+
expect(row.exhausted).toBe(0);
|
|
153
|
+
expect(row.summary_text).toBe("summary text");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("end row maps explicit summaryFailed and omitted optionals", async () => {
|
|
157
|
+
const { writer, requests } = createWriter();
|
|
158
|
+
|
|
159
|
+
await writer.writeEnd("conv-1", {
|
|
160
|
+
...endEvent,
|
|
161
|
+
compacted: false,
|
|
162
|
+
summaryFailed: true,
|
|
163
|
+
preservedTailMessages: undefined,
|
|
164
|
+
reason: undefined,
|
|
165
|
+
exhausted: true,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const row = JSON.parse(requests[1]!.body) as Record<string, unknown>;
|
|
169
|
+
expect(row.compacted).toBe(0);
|
|
170
|
+
expect(row.summary_failed).toBe(1);
|
|
171
|
+
expect(row.preserved_tail_messages).toBe(-1);
|
|
172
|
+
expect(row.reason).toBe("");
|
|
173
|
+
expect(row.exhausted).toBe(1);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("failed table creation retries on the next write", async () => {
|
|
177
|
+
let calls = 0;
|
|
178
|
+
const requests: CapturedRequest[] = [];
|
|
179
|
+
const { writer } = createWriter({
|
|
180
|
+
fetchImpl: async (url: string, init?: RequestInit) => {
|
|
181
|
+
calls++;
|
|
182
|
+
requests.push({ url: new URL(url), body: String(init?.body ?? "") });
|
|
183
|
+
if (calls === 1) return new Response("boom", { status: 500 });
|
|
184
|
+
return new Response("", { status: 200 });
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
await expect(writer.writeStart("conv-1", startEvent)).rejects.toThrow(
|
|
189
|
+
"ClickHouse compaction-log request failed",
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
await writer.writeStart("conv-1", startEvent);
|
|
193
|
+
const ddl = requests.filter((r) => r.body.includes("CREATE TABLE"));
|
|
194
|
+
expect(ddl.length).toBe(2);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("write failures surface the HTTP status and body", async () => {
|
|
198
|
+
let calls = 0;
|
|
199
|
+
const { writer } = createWriter({
|
|
200
|
+
fetchImpl: async () => {
|
|
201
|
+
calls++;
|
|
202
|
+
if (calls === 1) return new Response("", { status: 200 });
|
|
203
|
+
return new Response("table is read-only", { status: 403 });
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
await expect(writer.writeEnd("conv-1", endEvent)).rejects.toThrow(
|
|
208
|
+
/HTTP 403.*table is read-only/,
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("long summary text is truncated", async () => {
|
|
213
|
+
const { writer, requests } = createWriter();
|
|
214
|
+
|
|
215
|
+
await writer.writeEnd("conv-1", {
|
|
216
|
+
...endEvent,
|
|
217
|
+
summaryText: "x".repeat(10_000),
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const row = JSON.parse(requests[1]!.body) as Record<string, unknown>;
|
|
221
|
+
expect((row.summary_text as string).length).toBe(4000);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// ---------------------------------------------------------------------
|
|
226
|
+
// Read path — getEventsBetween
|
|
227
|
+
// ---------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
function startRow(overrides: Record<string, unknown> = {}) {
|
|
230
|
+
return {
|
|
231
|
+
compaction_id: "comp-1",
|
|
232
|
+
request_id: "req-1",
|
|
233
|
+
phase: "start",
|
|
234
|
+
trigger: "budget",
|
|
235
|
+
started_at: 1000,
|
|
236
|
+
finished_at: 0,
|
|
237
|
+
duration_ms: -1,
|
|
238
|
+
pre_message_count: 12,
|
|
239
|
+
result_message_count: -1,
|
|
240
|
+
compacted: 0,
|
|
241
|
+
previous_estimated_input_tokens: -1,
|
|
242
|
+
estimated_input_tokens: -1,
|
|
243
|
+
max_input_tokens: -1,
|
|
244
|
+
threshold_tokens: -1,
|
|
245
|
+
compacted_messages: -1,
|
|
246
|
+
compacted_persisted_messages: -1,
|
|
247
|
+
preserved_tail_messages: -1,
|
|
248
|
+
summary_calls: -1,
|
|
249
|
+
summary_input_tokens: -1,
|
|
250
|
+
summary_output_tokens: -1,
|
|
251
|
+
summary_model: "",
|
|
252
|
+
summary_failed: -1,
|
|
253
|
+
reason: "",
|
|
254
|
+
exhausted: 0,
|
|
255
|
+
injection_mode: "",
|
|
256
|
+
auto_compress_applied: 0,
|
|
257
|
+
summary_text: "",
|
|
258
|
+
...overrides,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function endRow(overrides: Record<string, unknown> = {}) {
|
|
263
|
+
return startRow({
|
|
264
|
+
phase: "end",
|
|
265
|
+
finished_at: 1500,
|
|
266
|
+
duration_ms: 500,
|
|
267
|
+
pre_message_count: -1,
|
|
268
|
+
result_message_count: 4,
|
|
269
|
+
compacted: 1,
|
|
270
|
+
previous_estimated_input_tokens: 900,
|
|
271
|
+
estimated_input_tokens: 300,
|
|
272
|
+
max_input_tokens: 1000,
|
|
273
|
+
threshold_tokens: 850,
|
|
274
|
+
compacted_messages: 10,
|
|
275
|
+
compacted_persisted_messages: 8,
|
|
276
|
+
preserved_tail_messages: 2,
|
|
277
|
+
summary_calls: 1,
|
|
278
|
+
summary_input_tokens: 880,
|
|
279
|
+
summary_output_tokens: 120,
|
|
280
|
+
summary_model: "test-model",
|
|
281
|
+
summary_failed: 0,
|
|
282
|
+
reason: "auto",
|
|
283
|
+
summary_text: "summary text",
|
|
284
|
+
...overrides,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function createReader(rows: Record<string, unknown>[]): {
|
|
289
|
+
writer: ClickHouseCompactionLogStore;
|
|
290
|
+
requests: CapturedRequest[];
|
|
291
|
+
} {
|
|
292
|
+
const requests: CapturedRequest[] = [];
|
|
293
|
+
const { writer } = createWriter({
|
|
294
|
+
fetchImpl: async (url: string, init?: RequestInit) => {
|
|
295
|
+
requests.push({ url: new URL(url), body: String(init?.body ?? "") });
|
|
296
|
+
const body =
|
|
297
|
+
rows.length === 0
|
|
298
|
+
? ""
|
|
299
|
+
: rows.map((r) => JSON.stringify(r)).join("\n") + "\n";
|
|
300
|
+
return new Response(body, { status: 200 });
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
return { writer, requests };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
describe("ClickHouseCompactionLogStore — getEventsBetween", () => {
|
|
307
|
+
test("binds the window via param_ URL slots, never string interpolation", async () => {
|
|
308
|
+
const { writer, requests } = createReader([]);
|
|
309
|
+
|
|
310
|
+
await writer.getEventsBetween("conv-1", 1999, 9001);
|
|
311
|
+
|
|
312
|
+
expect(requests.length).toBe(1);
|
|
313
|
+
const req = requests[0]!;
|
|
314
|
+
expect(req.url.searchParams.get("database")).toBe("analytics");
|
|
315
|
+
expect(req.url.searchParams.get("param_assistant_id")).toBe(
|
|
316
|
+
"assistant-123",
|
|
317
|
+
);
|
|
318
|
+
expect(req.url.searchParams.get("param_conversation_id")).toBe("conv-1");
|
|
319
|
+
expect(req.url.searchParams.get("param_after_started_at")).toBe("1999");
|
|
320
|
+
expect(req.url.searchParams.get("param_before_started_at")).toBe("9001");
|
|
321
|
+
expect(req.body).toContain("{assistant_id:String}");
|
|
322
|
+
expect(req.body).toContain(
|
|
323
|
+
"started_at > fromUnixTimestamp64Milli({after_started_at:Int64})",
|
|
324
|
+
);
|
|
325
|
+
expect(req.body).toContain("LIMIT 1 BY compaction_id, phase");
|
|
326
|
+
expect(req.body).toContain("FORMAT JSONEachRow");
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test("omits the floor predicate when afterStartedAt is null", async () => {
|
|
330
|
+
const { writer, requests } = createReader([]);
|
|
331
|
+
|
|
332
|
+
await writer.getEventsBetween("conv-1", null, 9001);
|
|
333
|
+
|
|
334
|
+
const req = requests[0]!;
|
|
335
|
+
expect(req.url.searchParams.get("param_after_started_at")).toBeNull();
|
|
336
|
+
expect(req.body).not.toContain("after_started_at");
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("pairs start and end rows into one completed event", async () => {
|
|
340
|
+
const { writer } = createReader([startRow(), endRow()]);
|
|
341
|
+
|
|
342
|
+
const events = await writer.getEventsBetween("conv-1", null, 9001);
|
|
343
|
+
|
|
344
|
+
expect(events).toHaveLength(1);
|
|
345
|
+
const event = events[0]!;
|
|
346
|
+
expect(event.compactionId).toBe("comp-1");
|
|
347
|
+
expect(event.completed).toBe(true);
|
|
348
|
+
expect(event.trigger).toBe("budget");
|
|
349
|
+
expect(event.startedAt).toBe(1000);
|
|
350
|
+
expect(event.finishedAt).toBe(1500);
|
|
351
|
+
expect(event.durationMs).toBe(500);
|
|
352
|
+
// Start-phase field comes from the start row even though the end
|
|
353
|
+
// row carries the -1 sentinel for it.
|
|
354
|
+
expect(event.preMessageCount).toBe(12);
|
|
355
|
+
expect(event.resultMessageCount).toBe(4);
|
|
356
|
+
expect(event.compacted).toBe(true);
|
|
357
|
+
expect(event.summaryInputTokens).toBe(880);
|
|
358
|
+
expect(event.summaryOutputTokens).toBe(120);
|
|
359
|
+
expect(event.summaryModel).toBe("test-model");
|
|
360
|
+
expect(event.summaryFailed).toBe(false);
|
|
361
|
+
expect(event.reason).toBe("auto");
|
|
362
|
+
expect(event.exhausted).toBe(false);
|
|
363
|
+
expect(event.summaryText).toBe("summary text");
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test("start-only rows surface as incomplete events with null end fields", async () => {
|
|
367
|
+
const { writer } = createReader([startRow()]);
|
|
368
|
+
|
|
369
|
+
const events = await writer.getEventsBetween("conv-1", null, 9001);
|
|
370
|
+
|
|
371
|
+
expect(events).toHaveLength(1);
|
|
372
|
+
const event = events[0]!;
|
|
373
|
+
expect(event.completed).toBe(false);
|
|
374
|
+
expect(event.preMessageCount).toBe(12);
|
|
375
|
+
expect(event.finishedAt).toBeNull();
|
|
376
|
+
expect(event.durationMs).toBeNull();
|
|
377
|
+
expect(event.compacted).toBeNull();
|
|
378
|
+
expect(event.summaryModel).toBeNull();
|
|
379
|
+
expect(event.summaryFailed).toBeNull();
|
|
380
|
+
expect(event.summaryText).toBeNull();
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test("maps sentinels back to null and coerces quoted Int64 values", async () => {
|
|
384
|
+
// ClickHouse may serialize Int64 columns as JSON strings.
|
|
385
|
+
const { writer } = createReader([
|
|
386
|
+
endRow({
|
|
387
|
+
compaction_id: "comp-2",
|
|
388
|
+
started_at: "2000",
|
|
389
|
+
duration_ms: "700",
|
|
390
|
+
preserved_tail_messages: -1,
|
|
391
|
+
summary_failed: -1,
|
|
392
|
+
reason: "",
|
|
393
|
+
summary_text: "",
|
|
394
|
+
}),
|
|
395
|
+
]);
|
|
396
|
+
|
|
397
|
+
const events = await writer.getEventsBetween("conv-1", null, 9001);
|
|
398
|
+
|
|
399
|
+
const event = events[0]!;
|
|
400
|
+
expect(event.startedAt).toBe(2000);
|
|
401
|
+
expect(event.durationMs).toBe(700);
|
|
402
|
+
expect(event.preservedTailMessages).toBeNull();
|
|
403
|
+
expect(event.summaryFailed).toBeNull();
|
|
404
|
+
expect(event.reason).toBeNull();
|
|
405
|
+
expect(event.summaryText).toBeNull();
|
|
406
|
+
// End-only pair: start-phase field is unknown.
|
|
407
|
+
expect(event.preMessageCount).toBeNull();
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test("sorts paired events by startedAt ascending", async () => {
|
|
411
|
+
const { writer } = createReader([
|
|
412
|
+
startRow({ compaction_id: "comp-late", started_at: 5000 }),
|
|
413
|
+
startRow({ compaction_id: "comp-early", started_at: 1000 }),
|
|
414
|
+
endRow({ compaction_id: "comp-late", started_at: 5000 }),
|
|
415
|
+
]);
|
|
416
|
+
|
|
417
|
+
const events = await writer.getEventsBetween("conv-1", null, 9001);
|
|
418
|
+
|
|
419
|
+
expect(events.map((e) => e.compactionId)).toEqual([
|
|
420
|
+
"comp-early",
|
|
421
|
+
"comp-late",
|
|
422
|
+
]);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test("empty response body yields an empty event list", async () => {
|
|
426
|
+
const { writer } = createReader([]);
|
|
427
|
+
|
|
428
|
+
const events = await writer.getEventsBetween("conv-1", null, 9001);
|
|
429
|
+
|
|
430
|
+
expect(events).toEqual([]);
|
|
431
|
+
});
|
|
432
|
+
});
|