@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
|
@@ -52,7 +52,7 @@ mock.module("../config/loader.js", () => ({
|
|
|
52
52
|
getNestedValue: () => undefined,
|
|
53
53
|
setNestedValue: () => {},
|
|
54
54
|
API_KEY_PROVIDERS: [],
|
|
55
|
-
|
|
55
|
+
_writeQuarantineNotice: () => {},
|
|
56
56
|
invalidateConfigCache: () => {},
|
|
57
57
|
}));
|
|
58
58
|
|
|
@@ -158,6 +158,9 @@ mock.module("../memory/conversation-crud.js", () => ({
|
|
|
158
158
|
|
|
159
159
|
mock.module("../memory/conversation-title-service.js", () => ({
|
|
160
160
|
GENERATING_TITLE: "Generating title...",
|
|
161
|
+
AUTO_TITLE_DETERMINISTIC: 2,
|
|
162
|
+
deriveDeterministicTitle: (context: { systemHint?: string }) =>
|
|
163
|
+
context.systemHint ?? "Untitled Conversation",
|
|
161
164
|
isReplaceableTitle: () => true,
|
|
162
165
|
queueGenerateConversationTitle: () => {},
|
|
163
166
|
queueRegenerateConversationTitle: () => {},
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { rmSync, writeFileSync } from "node:fs";
|
|
10
10
|
import { join } from "node:path";
|
|
11
|
-
import {
|
|
11
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
12
12
|
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
14
14
|
// Mocks — must be defined before importing the module under test
|
|
@@ -22,7 +22,11 @@ mock.module("../util/logger.js", () => ({
|
|
|
22
22
|
}));
|
|
23
23
|
|
|
24
24
|
const mockGetConversationByKey = mock(
|
|
25
|
-
(
|
|
25
|
+
(
|
|
26
|
+
_key: string,
|
|
27
|
+
): {
|
|
28
|
+
conversationId: string;
|
|
29
|
+
} | null => ({
|
|
26
30
|
conversationId: "conv-test-123",
|
|
27
31
|
}),
|
|
28
32
|
);
|
|
@@ -80,13 +84,6 @@ mock.module("../runtime/routes/identity-intro-cache.js", () => ({
|
|
|
80
84
|
setCachedIntro: () => {},
|
|
81
85
|
}));
|
|
82
86
|
|
|
83
|
-
const assistantFeatureFlags: Record<string, boolean> = {};
|
|
84
|
-
|
|
85
|
-
mock.module("../config/assistant-feature-flags.js", () => ({
|
|
86
|
-
isAssistantFeatureFlagEnabled: (key: string) =>
|
|
87
|
-
assistantFeatureFlags[key] ?? false,
|
|
88
|
-
}));
|
|
89
|
-
|
|
90
87
|
// Mock getOrCreateConversation from conversation-store so the handler
|
|
91
88
|
// never touches DaemonServer.
|
|
92
89
|
const mockGetOrCreateConversation = mock(async (_id: string) =>
|
|
@@ -185,12 +182,6 @@ function makeMockSession(
|
|
|
185
182
|
const route = ROUTES.find((r) => r.endpoint === "btw" && r.method === "POST");
|
|
186
183
|
if (!route) throw new Error("btw route not found in ROUTES");
|
|
187
184
|
|
|
188
|
-
beforeEach(() => {
|
|
189
|
-
for (const key of Object.keys(assistantFeatureFlags)) {
|
|
190
|
-
delete assistantFeatureFlags[key];
|
|
191
|
-
}
|
|
192
|
-
});
|
|
193
|
-
|
|
194
185
|
async function callHandler(
|
|
195
186
|
body: Record<string, unknown>,
|
|
196
187
|
): Promise<{ result: unknown; error?: unknown }> {
|
|
@@ -338,25 +329,7 @@ describe("POST /v1/btw", () => {
|
|
|
338
329
|
expect(options!.config!.modelIntent).toBeUndefined();
|
|
339
330
|
});
|
|
340
331
|
|
|
341
|
-
test("greeting requests
|
|
342
|
-
mockGetOrCreateConversation.mockClear();
|
|
343
|
-
|
|
344
|
-
const { result } = await callHandler({
|
|
345
|
-
conversationKey: "greeting",
|
|
346
|
-
content: "Generate a greeting",
|
|
347
|
-
});
|
|
348
|
-
const text = await readStream(result as ReadableStream<Uint8Array>);
|
|
349
|
-
|
|
350
|
-
expect(text).toContain(
|
|
351
|
-
`event: btw_text_delta\ndata: {"text":"What are we working on?"}`,
|
|
352
|
-
);
|
|
353
|
-
expect(text).toContain("event: btw_complete\ndata: {}");
|
|
354
|
-
expect(mockGetOrCreateConversation).not.toHaveBeenCalled();
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
test("greeting requests pass callSite: 'emptyStateGreeting' when the dynamic greetings flag is on", async () => {
|
|
358
|
-
assistantFeatureFlags["empty-state-dynamic-greetings"] = true;
|
|
359
|
-
|
|
332
|
+
test("greeting requests pass callSite: 'emptyStateGreeting'", async () => {
|
|
360
333
|
const provider = makeMockProvider();
|
|
361
334
|
const session = makeMockSession(provider);
|
|
362
335
|
mockGetOrCreateConversation.mockImplementationOnce(async () => session);
|
|
@@ -428,21 +428,15 @@ describe("Permission Checker", () => {
|
|
|
428
428
|
expect(result.decision).toBe("allow");
|
|
429
429
|
});
|
|
430
430
|
|
|
431
|
-
test("file_read of workspace
|
|
432
|
-
const
|
|
433
|
-
const result = await check("file_read", { path:
|
|
434
|
-
expect(result.decision).toBe("allow");
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
test("file_write of workspace UPDATES.md is auto-allowed", async () => {
|
|
438
|
-
const updatesPath = join(checkerTestDir, "UPDATES.md");
|
|
439
|
-
const result = await check("file_write", { path: updatesPath }, "/tmp");
|
|
431
|
+
test("file_read of workspace BOOTSTRAP.md is auto-allowed", async () => {
|
|
432
|
+
const bootstrapPath = join(checkerTestDir, "BOOTSTRAP.md");
|
|
433
|
+
const result = await check("file_read", { path: bootstrapPath }, "/tmp");
|
|
440
434
|
expect(result.decision).toBe("allow");
|
|
441
435
|
});
|
|
442
436
|
|
|
443
|
-
test("file_edit of workspace
|
|
444
|
-
const
|
|
445
|
-
const result = await check("file_edit", { path:
|
|
437
|
+
test("file_edit of workspace BOOTSTRAP.md is auto-allowed", async () => {
|
|
438
|
+
const bootstrapPath = join(checkerTestDir, "BOOTSTRAP.md");
|
|
439
|
+
const result = await check("file_edit", { path: bootstrapPath }, "/tmp");
|
|
446
440
|
expect(result.decision).toBe("allow");
|
|
447
441
|
});
|
|
448
442
|
|
|
@@ -82,6 +82,7 @@ import { migrateProviderConnectionStatusLabel } from "../memory/migrations/244-p
|
|
|
82
82
|
import { migrateProviderConnectionBaseUrlAndModels } from "../memory/migrations/250-provider-connection-base-url-and-models.js";
|
|
83
83
|
import * as schema from "../memory/schema.js";
|
|
84
84
|
import { getConnection } from "../providers/inference/connections.js";
|
|
85
|
+
import { getConfigQuarantineNoticePath } from "../util/platform.js";
|
|
85
86
|
import { setStorePathForTesting } from "./encrypted-store-test-helpers.js";
|
|
86
87
|
|
|
87
88
|
// ---------------------------------------------------------------------------
|
|
@@ -325,8 +326,9 @@ describe("loadConfig startup behavior", () => {
|
|
|
325
326
|
}
|
|
326
327
|
}
|
|
327
328
|
}
|
|
328
|
-
|
|
329
|
-
|
|
329
|
+
// Clear any leftover config-quarantine notice sentinel from prior runs.
|
|
330
|
+
const noticePath = getConfigQuarantineNoticePath();
|
|
331
|
+
if (existsSync(noticePath)) rmSync(noticePath, { force: true });
|
|
330
332
|
ensureTestDir();
|
|
331
333
|
setStorePathForTesting(join(WORKSPACE_DIR, "keys.enc"));
|
|
332
334
|
delete process.env.VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* When loadConfig()/loadRawConfig() quarantines a corrupt config.json, it
|
|
3
|
+
* writes a small JSON sentinel to <workspace>/data/config-quarantine-notice.json
|
|
4
|
+
* recording the event. The per-turn `config-quarantine-notice` injector reads
|
|
5
|
+
* that sentinel and surfaces the reset to the agent — it's agent-visible
|
|
6
|
+
* context, not a push notification.
|
|
7
|
+
*
|
|
8
|
+
* The sentinel is overwritten on each quarantine (idempotent per event) and
|
|
9
|
+
* resolves from a workspace-derived path so it is safe to write during the
|
|
10
|
+
* very early config load, before the SQLite DB or getConfig().dataDir exist.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
existsSync,
|
|
15
|
+
mkdirSync,
|
|
16
|
+
readdirSync,
|
|
17
|
+
readFileSync,
|
|
18
|
+
rmSync,
|
|
19
|
+
writeFileSync,
|
|
20
|
+
} from "node:fs";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
_writeQuarantineNotice,
|
|
26
|
+
invalidateConfigCache,
|
|
27
|
+
loadConfig,
|
|
28
|
+
} from "../config/loader.js";
|
|
29
|
+
import { getConfigQuarantineNoticePath } from "../util/platform.js";
|
|
30
|
+
import { setStorePathForTesting } from "./encrypted-store-test-helpers.js";
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Helpers
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
const WORKSPACE_DIR = process.env.VELLUM_WORKSPACE_DIR!;
|
|
37
|
+
const CONFIG_PATH = join(WORKSPACE_DIR, "config.json");
|
|
38
|
+
const NOTICE_PATH = getConfigQuarantineNoticePath();
|
|
39
|
+
|
|
40
|
+
function ensureTestDir(): void {
|
|
41
|
+
const dirs = [
|
|
42
|
+
WORKSPACE_DIR,
|
|
43
|
+
join(WORKSPACE_DIR, "data"),
|
|
44
|
+
join(WORKSPACE_DIR, "data", "memory"),
|
|
45
|
+
join(WORKSPACE_DIR, "data", "memory", "knowledge"),
|
|
46
|
+
join(WORKSPACE_DIR, "data", "logs"),
|
|
47
|
+
];
|
|
48
|
+
for (const dir of dirs) {
|
|
49
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resetWorkspace(): void {
|
|
54
|
+
for (const name of readdirSync(WORKSPACE_DIR)) {
|
|
55
|
+
rmSync(join(WORKSPACE_DIR, name), { recursive: true, force: true });
|
|
56
|
+
}
|
|
57
|
+
ensureTestDir();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function listQuarantinedFiles(): string[] {
|
|
61
|
+
return readdirSync(WORKSPACE_DIR).filter((name) =>
|
|
62
|
+
/^config\.json\.corrupt-.+\.json$/.test(name),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readNotice(): {
|
|
67
|
+
quarantinedAt: string;
|
|
68
|
+
quarantinePath: string;
|
|
69
|
+
originalPath: string;
|
|
70
|
+
} {
|
|
71
|
+
return JSON.parse(readFileSync(NOTICE_PATH, "utf-8"));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Tests
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
describe("config-quarantine notice sentinel", () => {
|
|
79
|
+
beforeEach(() => {
|
|
80
|
+
resetWorkspace();
|
|
81
|
+
setStorePathForTesting(join(WORKSPACE_DIR, "keys.enc"));
|
|
82
|
+
invalidateConfigCache();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
afterEach(() => {
|
|
86
|
+
setStorePathForTesting(null);
|
|
87
|
+
invalidateConfigCache();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("writes a sentinel recording the quarantine when config.json is corrupt", () => {
|
|
91
|
+
writeFileSync(CONFIG_PATH, '{"provider": "anthropic", "mo');
|
|
92
|
+
|
|
93
|
+
loadConfig();
|
|
94
|
+
|
|
95
|
+
const [quarantinedName] = listQuarantinedFiles();
|
|
96
|
+
expect(quarantinedName).toBeDefined();
|
|
97
|
+
const quarantinedPath = join(WORKSPACE_DIR, quarantinedName);
|
|
98
|
+
|
|
99
|
+
expect(existsSync(NOTICE_PATH)).toBe(true);
|
|
100
|
+
const notice = readNotice();
|
|
101
|
+
expect(notice.quarantinePath).toBe(quarantinedPath);
|
|
102
|
+
expect(notice.originalPath).toBe(CONFIG_PATH);
|
|
103
|
+
// quarantinedAt is a parseable ISO timestamp.
|
|
104
|
+
expect(Number.isNaN(Date.parse(notice.quarantinedAt))).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("a second quarantine overwrites the sentinel with the latest event", () => {
|
|
108
|
+
// First corruption round.
|
|
109
|
+
writeFileSync(CONFIG_PATH, '{"partial": ');
|
|
110
|
+
loadConfig();
|
|
111
|
+
invalidateConfigCache();
|
|
112
|
+
|
|
113
|
+
const firstNotice = readNotice();
|
|
114
|
+
expect(listQuarantinedFiles()).toHaveLength(1);
|
|
115
|
+
|
|
116
|
+
// Loader wrote a fresh default config.json after quarantine; corrupt it
|
|
117
|
+
// again. Spin briefly so the second quarantine gets a distinct
|
|
118
|
+
// millisecond-precision filename (and a later timestamp).
|
|
119
|
+
const untilDifferentMs = Date.now() + 5;
|
|
120
|
+
while (Date.now() < untilDifferentMs) {
|
|
121
|
+
/* spin */
|
|
122
|
+
}
|
|
123
|
+
writeFileSync(CONFIG_PATH, "still not json");
|
|
124
|
+
loadConfig();
|
|
125
|
+
|
|
126
|
+
const quarantined = listQuarantinedFiles().sort();
|
|
127
|
+
expect(quarantined).toHaveLength(2);
|
|
128
|
+
|
|
129
|
+
// Single sentinel, pointing at the most recent quarantine.
|
|
130
|
+
const secondNotice = readNotice();
|
|
131
|
+
expect(secondNotice.quarantinePath).not.toBe(firstNotice.quarantinePath);
|
|
132
|
+
expect(secondNotice.quarantinePath).toBe(
|
|
133
|
+
join(WORKSPACE_DIR, quarantined[quarantined.length - 1]),
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("valid config.json does not create a sentinel (regression guard)", () => {
|
|
138
|
+
writeFileSync(
|
|
139
|
+
CONFIG_PATH,
|
|
140
|
+
JSON.stringify({ provider: "anthropic", model: "claude-opus-4-7" }),
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
loadConfig();
|
|
144
|
+
|
|
145
|
+
expect(listQuarantinedFiles()).toHaveLength(0);
|
|
146
|
+
expect(existsSync(NOTICE_PATH)).toBe(false);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("sentinel write failure is swallowed (startup never blocks)", () => {
|
|
150
|
+
// Make the notice directory unwritable by replacing it with a file so the
|
|
151
|
+
// write/mkdir path throws. The helper must not propagate the error.
|
|
152
|
+
const noticeDir = join(WORKSPACE_DIR, "data");
|
|
153
|
+
rmSync(noticeDir, { recursive: true, force: true });
|
|
154
|
+
// A regular file where the `data` directory is expected makes mkdir/write
|
|
155
|
+
// fail with ENOTDIR/EEXIST.
|
|
156
|
+
writeFileSync(noticeDir, "not a directory", "utf-8");
|
|
157
|
+
|
|
158
|
+
const quarantinePath = join(
|
|
159
|
+
WORKSPACE_DIR,
|
|
160
|
+
"config.json.corrupt-2026-04-20T12-00-00.000Z.json",
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
expect(() =>
|
|
164
|
+
_writeQuarantineNotice(CONFIG_PATH, quarantinePath),
|
|
165
|
+
).not.toThrow();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -266,12 +266,12 @@ describe("ConfigWatcher workspace file handlers", () => {
|
|
|
266
266
|
expect(identityCallCount).toBe(1);
|
|
267
267
|
});
|
|
268
268
|
|
|
269
|
-
test("
|
|
269
|
+
test("unregistered workspace files are not subscribed (only the registered handler set is)", () => {
|
|
270
270
|
watcher.start(onConversationEvict);
|
|
271
271
|
// Per-file watching only registers config.json, SOUL.md, IDENTITY.md.
|
|
272
272
|
// The whole workspace dir must not be watched either — that was the
|
|
273
273
|
// ENXIO-on-Unix-sockets bug.
|
|
274
|
-
expect(findFileWatch(join(WORKSPACE_DIR, "
|
|
274
|
+
expect(findFileWatch(join(WORKSPACE_DIR, "OTHER.md"))).toBeUndefined();
|
|
275
275
|
expect(findWatcher(WORKSPACE_DIR)).toBeUndefined();
|
|
276
276
|
});
|
|
277
277
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* `inferenceProfile` column at turn start and threads it through to
|
|
4
4
|
* `AgentLoop.run()` as the per-turn `overrideProfile`. Background
|
|
5
5
|
* conversations intentionally skip the column so background fan-out
|
|
6
|
-
* (subagents, scheduled tasks
|
|
6
|
+
* (subagents, scheduled tasks) runs on the workspace
|
|
7
7
|
* defaults rather than inheriting an interactive override.
|
|
8
8
|
*
|
|
9
9
|
* This is the "conversation-agent-loop integration" half of the
|
|
@@ -461,7 +461,7 @@ describe("classifyConversationError", () => {
|
|
|
461
461
|
const result = classifyConversationError(err, baseCtx);
|
|
462
462
|
expect(result.code).toBe("PROVIDER_NOT_CONFIGURED");
|
|
463
463
|
expect(result.userMessage).toBe(
|
|
464
|
-
"No API key configured for inference. Add one in Settings to start chatting.",
|
|
464
|
+
"No API key configured for inference. Add one in Settings → Models & Services to start chatting.",
|
|
465
465
|
);
|
|
466
466
|
expect(result.retryable).toBe(true);
|
|
467
467
|
expect(result.errorCategory).toBe("provider_not_configured");
|
|
@@ -636,11 +636,7 @@ describe("classifyConversationError", () => {
|
|
|
636
636
|
});
|
|
637
637
|
|
|
638
638
|
it("classifies ProviderError 403 with 'invalid api key' message as PROVIDER_INVALID_KEY", () => {
|
|
639
|
-
const err = new ProviderError(
|
|
640
|
-
"OpenAI: Invalid API key",
|
|
641
|
-
"openai",
|
|
642
|
-
403,
|
|
643
|
-
);
|
|
639
|
+
const err = new ProviderError("OpenAI: Invalid API key", "openai", 403);
|
|
644
640
|
const result = classifyConversationError(err, baseCtx);
|
|
645
641
|
expect(result.code).toBe("PROVIDER_INVALID_KEY");
|
|
646
642
|
expect(result.errorCategory).toBe("provider_invalid_key");
|
|
@@ -755,6 +755,7 @@ describe("web_search_tool_result structural guard", () => {
|
|
|
755
755
|
// (array of web_search_result objects) and is not truncated this way.
|
|
756
756
|
"context/tool-result-truncation.ts",
|
|
757
757
|
"context/post-turn-tool-result-truncation.ts",
|
|
758
|
+
"context/tool-result-spool.ts",
|
|
758
759
|
|
|
759
760
|
// Anthropic provider type guards define API-specific discriminants.
|
|
760
761
|
// It has a separate isWebSearchToolResultBlock for the other type.
|
|
@@ -788,6 +789,13 @@ describe("web_search_tool_result structural guard", () => {
|
|
|
788
789
|
// tool_result is relevant. Same reasoning as agent/loop.ts above.
|
|
789
790
|
"plugins/defaults/tool-error/hooks/post-tool-use.ts",
|
|
790
791
|
|
|
792
|
+
// Counts unbroken runs of locally-executed exploration tools (bash,
|
|
793
|
+
// file_read, file_list) to detect inline drift. Server-side web search
|
|
794
|
+
// results (web_search_tool_result) are not produced by the tool executor;
|
|
795
|
+
// any non-exploration block simply bounds the trailing run, which is the
|
|
796
|
+
// conservative direction (fewer nudges). Same reasoning as agent/loop.ts.
|
|
797
|
+
"plugins/defaults/exploration-drift/hooks/post-tool-use.ts",
|
|
798
|
+
|
|
791
799
|
// Reconciles synthesized cancellation tool_results for locally-executed
|
|
792
800
|
// tools only. Same reasoning as agent/loop.ts above.
|
|
793
801
|
"daemon/conversation-agent-loop.ts",
|
|
@@ -59,6 +59,7 @@ mock.module("../runtime/sync/resource-sync-events.js", () => ({
|
|
|
59
59
|
}));
|
|
60
60
|
|
|
61
61
|
import {
|
|
62
|
+
AUTO_TITLE_DETERMINISTIC,
|
|
62
63
|
generateAndPersistConversationTitle,
|
|
63
64
|
queueGenerateConversationTitle,
|
|
64
65
|
regenerateConversationTitle,
|
|
@@ -260,7 +261,7 @@ describe("conversation-title-service", () => {
|
|
|
260
261
|
expect(mockUpdateConversationTitle).toHaveBeenCalledWith(
|
|
261
262
|
"conv-1",
|
|
262
263
|
"Untitled Conversation",
|
|
263
|
-
|
|
264
|
+
AUTO_TITLE_DETERMINISTIC,
|
|
264
265
|
);
|
|
265
266
|
});
|
|
266
267
|
|
|
@@ -216,7 +216,7 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
|
|
|
216
216
|
"media/image-credentials.ts", // shared image-gen credential resolver (provider API key lookup)
|
|
217
217
|
"memory/embedding-backend.ts", // embedding backend API key lookup
|
|
218
218
|
"memory/llm-request-log-source-clickhouse.ts", // ClickHouse read source — lazy lookup of clickhouse:url + clickhouse:password + vellum:platform_assistant_id for self-scoped mirror reads
|
|
219
|
-
"memory/compaction-log-
|
|
219
|
+
"memory/compaction-log-store-clickhouse.ts", // ClickHouse compaction log writer — lazy lookup of clickhouse:url + clickhouse:password + vellum:platform_assistant_id for self-scoped event writes
|
|
220
220
|
"daemon/providers-setup.ts", // provider initialization API key lookup
|
|
221
221
|
"workspace/migrations/006-services-config.ts", // services config migration reads provider API keys
|
|
222
222
|
"workspace/migrations/018-rekey-compound-credential-keys.ts", // re-key compound credential storage keys
|
|
@@ -41,7 +41,7 @@ mock.module("../config/loader.js", () => ({
|
|
|
41
41
|
setNestedValue: () => undefined,
|
|
42
42
|
mergeDefaultWorkspaceConfig: (config: unknown) => config,
|
|
43
43
|
API_KEY_PROVIDERS: [] as const,
|
|
44
|
-
|
|
44
|
+
_writeQuarantineNotice: () => undefined,
|
|
45
45
|
}));
|
|
46
46
|
|
|
47
47
|
mock.module("../daemon/conversation-skill-tools.js", () => ({
|