@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.
Files changed (219) hide show
  1. package/ARCHITECTURE.md +15 -17
  2. package/README.md +0 -6
  3. package/bun.lock +6 -122
  4. package/node_modules/@vellumai/gateway-client/bun.lock +1 -0
  5. package/node_modules/@vellumai/gateway-client/package.json +3 -1
  6. package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -1
  7. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +87 -0
  8. package/node_modules/@vellumai/gateway-client/src/index.ts +3 -5
  9. package/openapi.yaml +126 -4
  10. package/package.json +1 -3
  11. package/src/__tests__/adaptive-thinking-repair.test.ts +185 -0
  12. package/src/__tests__/agent-loop-compaction-events.test.ts +7 -6
  13. package/src/__tests__/anthropic-provider.test.ts +129 -0
  14. package/src/__tests__/background-workers-disk-pressure.test.ts +4 -1
  15. package/src/__tests__/btw-routes.test.ts +7 -34
  16. package/src/__tests__/checker.test.ts +6 -12
  17. package/src/__tests__/config-loader-backfill.test.ts +4 -2
  18. package/src/__tests__/config-loader-quarantine-notice.test.ts +167 -0
  19. package/src/__tests__/config-watcher.test.ts +2 -2
  20. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
  21. package/src/__tests__/conversation-error.test.ts +2 -6
  22. package/src/__tests__/conversation-history-web-search.test.ts +8 -0
  23. package/src/__tests__/conversation-title-service.test.ts +2 -1
  24. package/src/__tests__/credential-security-invariants.test.ts +1 -1
  25. package/src/__tests__/disk-pressure-tools.test.ts +1 -1
  26. package/src/__tests__/exploration-drift-hook.test.ts +692 -0
  27. package/src/__tests__/filing-service.test.ts +8 -3
  28. package/src/__tests__/guardian-action-store.test.ts +0 -167
  29. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +1 -1
  30. package/src/__tests__/heartbeat-disk-pressure.test.ts +4 -1
  31. package/src/__tests__/heartbeat-service.test.ts +5 -2
  32. package/src/__tests__/identity-intro-cache.test.ts +12 -5
  33. package/src/__tests__/identity-routes.test.ts +16 -57
  34. package/src/__tests__/injector-chain.test.ts +8 -3
  35. package/src/__tests__/injector-config-quarantine-notice.test.ts +115 -0
  36. package/src/__tests__/llm-usage-store.test.ts +11 -0
  37. package/src/__tests__/memory-v2-static-injector.test.ts +22 -0
  38. package/src/__tests__/model-intents.test.ts +1 -1
  39. package/src/__tests__/oauth-cli.test.ts +19 -8
  40. package/src/__tests__/openai-provider.test.ts +34 -0
  41. package/src/__tests__/prechat-onboarding-contract.test.ts +0 -1
  42. package/src/__tests__/recurrence-engine.test.ts +45 -0
  43. package/src/__tests__/schedule-routes.test.ts +34 -0
  44. package/src/__tests__/scheduler-disk-pressure.test.ts +1 -1
  45. package/src/__tests__/script-proxy-conversation-manager.test.ts +10 -5
  46. package/src/__tests__/skill-tool-factory.test.ts +49 -0
  47. package/src/__tests__/subagent-role-registry.test.ts +24 -1
  48. package/src/__tests__/subagent-tools.test.ts +1 -0
  49. package/src/__tests__/system-prompt.test.ts +109 -11
  50. package/src/__tests__/tool-error-hook.test.ts +1 -0
  51. package/src/__tests__/tool-result-spool.test.ts +337 -0
  52. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
  53. package/src/__tests__/validate-input.test.ts +95 -1
  54. package/src/__tests__/workspace-migration-098-remove-stale-updates-bulletin-file.test.ts +65 -0
  55. package/src/__tests__/workspace-migration-099-disable-cache-one-shot-callsites.test.ts +139 -0
  56. package/src/__tests__/workspace-release-notes-feature-flag-guard.test.ts +45 -95
  57. package/src/agent/loop.ts +81 -24
  58. package/src/api/events/usage-progress.ts +28 -0
  59. package/src/api/index.ts +6 -0
  60. package/src/background-wake/wake-intent-hooks.test.ts +2 -0
  61. package/src/bundler/app-bundler.ts +25 -42
  62. package/src/calls/call-controller.ts +1 -1
  63. package/src/cli/commands/plugins.ts +248 -15
  64. package/src/cli/lib/__tests__/inspect-plugin.test.ts +318 -0
  65. package/src/cli/lib/__tests__/install-from-github.test.ts +16 -9
  66. package/src/cli/lib/__tests__/plugin-artifact.test.ts +183 -0
  67. package/src/cli/lib/__tests__/plugin-details.test.ts +158 -0
  68. package/src/cli/lib/__tests__/plugin-fingerprint.test.ts +245 -0
  69. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +301 -0
  70. package/src/cli/lib/inspect-plugin.ts +252 -0
  71. package/src/cli/lib/install-from-github.ts +214 -21
  72. package/src/cli/lib/list-installed-plugins.ts +17 -6
  73. package/src/cli/lib/plugin-artifact.ts +103 -0
  74. package/src/cli/lib/plugin-details.ts +18 -1
  75. package/src/cli/lib/plugin-fingerprint.ts +197 -0
  76. package/src/cli/lib/upgrade-plugin.ts +219 -0
  77. package/src/config/bundled-skills/subagent/SKILL.md +2 -0
  78. package/src/config/bundled-skills/subagent/TOOLS.json +8 -2
  79. package/src/config/call-site-defaults.ts +13 -2
  80. package/src/config/feature-flag-registry.json +8 -16
  81. package/src/config/loader.ts +52 -59
  82. package/src/config/schema.ts +0 -2
  83. package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
  84. package/src/config/schemas/__tests__/memory-v3.test.ts +10 -0
  85. package/src/config/schemas/llm.ts +10 -0
  86. package/src/config/schemas/memory-v2.ts +13 -0
  87. package/src/config/schemas/memory-v3.ts +92 -0
  88. package/src/context/post-turn-tool-result-truncation.ts +32 -18
  89. package/src/context/tool-result-spool.ts +104 -0
  90. package/src/credential-execution/feature-gates.ts +0 -1
  91. package/src/daemon/conversation-agent-loop-handlers.ts +41 -16
  92. package/src/daemon/conversation-error.ts +6 -15
  93. package/src/daemon/conversation.ts +9 -0
  94. package/src/daemon/disk-pressure-policy.ts +0 -1
  95. package/src/daemon/lifecycle.ts +1 -20
  96. package/src/daemon/message-types/conversations.ts +2 -15
  97. package/src/daemon/trust-context.ts +1 -1
  98. package/src/heartbeat/__tests__/heartbeat-service.test.ts +1 -1
  99. package/src/home/__tests__/home-content-refresh.test.ts +114 -0
  100. package/src/home/__tests__/suggested-prompts.test.ts +86 -5
  101. package/src/home/home-content-refresh.ts +43 -31
  102. package/src/home/home-greeting-cache.ts +8 -1
  103. package/src/home/home-greeting.ts +13 -9
  104. package/src/home/suggested-prompts.ts +77 -24
  105. package/src/ipc/routes/trust-rules.test.ts +66 -72
  106. package/src/media/image-credentials.ts +2 -2
  107. package/src/memory/__tests__/compaction-log-store-clickhouse.test.ts +432 -0
  108. package/src/memory/{compaction-log-writer-clickhouse.ts → compaction-log-store-clickhouse.ts} +264 -55
  109. package/src/memory/conversation-attention-store.ts +1 -0
  110. package/src/memory/conversation-bootstrap.ts +18 -9
  111. package/src/memory/conversation-crud.ts +12 -2
  112. package/src/memory/conversation-title-service.ts +53 -9
  113. package/src/memory/delivery-channels.ts +0 -69
  114. package/src/memory/graph/extraction-job.ts +0 -15
  115. package/src/memory/guardian-action-store.ts +1 -376
  116. package/src/memory/llm-usage-store.ts +5 -1
  117. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +2 -2
  118. package/src/memory/v2/__tests__/consolidation-job.test.ts +183 -2
  119. package/src/memory/v2/__tests__/injection.test.ts +70 -0
  120. package/src/memory/v2/__tests__/static-context.test.ts +12 -0
  121. package/src/memory/v2/consolidation-job.ts +93 -9
  122. package/src/memory/v2/injection.ts +53 -0
  123. package/src/memory/v2/prompts/consolidation.ts +1 -0
  124. package/src/memory/v2/static-context.ts +13 -1
  125. package/src/memory/v2/sweep-job.ts +1 -1
  126. package/src/memory/v2/types.ts +5 -0
  127. package/src/plugin-api/types.ts +7 -0
  128. package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +300 -0
  129. package/src/plugins/defaults/exploration-drift/package.json +15 -0
  130. package/src/plugins/defaults/index.ts +25 -0
  131. package/src/plugins/defaults/memory-retrieval/injectors.ts +132 -4
  132. package/src/plugins/defaults/memory-v3-shadow/__tests__/card.test.ts +92 -0
  133. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +2 -1
  134. package/src/plugins/defaults/memory-v3-shadow/__tests__/fresh-set.test.ts +52 -0
  135. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
  136. package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +2 -1
  137. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +136 -5
  138. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +17 -0
  139. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +6 -0
  140. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +5 -1
  141. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +68 -4
  142. package/src/plugins/defaults/memory-v3-shadow/card.ts +49 -5
  143. package/src/plugins/defaults/memory-v3-shadow/fresh-set.ts +59 -0
  144. package/src/plugins/defaults/memory-v3-shadow/injector.ts +4 -2
  145. package/src/plugins/defaults/memory-v3-shadow/learned-edges.test.ts +169 -0
  146. package/src/plugins/defaults/memory-v3-shadow/learned-edges.ts +178 -0
  147. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +115 -26
  148. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +13 -9
  149. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +144 -22
  150. package/src/plugins/defaults/memory-v3-shadow/types.ts +24 -6
  151. package/src/plugins/defaults/title-generate/hooks/stop.ts +13 -0
  152. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +16 -0
  153. package/src/prompts/cache-boundary.ts +17 -0
  154. package/src/prompts/sections.ts +50 -17
  155. package/src/prompts/system-prompt.ts +12 -4
  156. package/src/prompts/templates/system-sections.ts +22 -0
  157. package/src/providers/anthropic/client.ts +74 -28
  158. package/src/providers/gemini/client.ts +5 -1
  159. package/src/providers/minimax/client.ts +9 -0
  160. package/src/providers/model-intents.ts +2 -2
  161. package/src/providers/openai/chat-completions-provider.ts +2 -1
  162. package/src/providers/openai/responses-provider.ts +5 -1
  163. package/src/providers/retry.ts +8 -0
  164. package/src/providers/types.ts +11 -0
  165. package/src/runtime/AGENTS.md +6 -0
  166. package/src/runtime/__tests__/agent-wake.test.ts +2 -2
  167. package/src/runtime/agent-wake.ts +5 -5
  168. package/src/runtime/background-job-runner.ts +2 -2
  169. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +150 -3
  170. package/src/runtime/migrations/vbundle-import-analyzer.ts +29 -6
  171. package/src/runtime/migrations/vbundle-import-policy.ts +23 -0
  172. package/src/runtime/migrations/vbundle-importer.ts +9 -4
  173. package/src/runtime/migrations/vbundle-streaming-importer.ts +8 -3
  174. package/src/runtime/pre-first-message-gate.ts +1 -1
  175. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +241 -0
  176. package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +97 -185
  177. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +17 -0
  178. package/src/runtime/routes/__tests__/plugins-routes.test.ts +1 -0
  179. package/src/runtime/routes/__tests__/task-routes.test.ts +3 -3
  180. package/src/runtime/routes/btw-routes.ts +0 -14
  181. package/src/runtime/routes/conversation-compaction-routes.ts +86 -19
  182. package/src/runtime/routes/conversation-list-routes.ts +77 -5
  183. package/src/runtime/routes/conversation-management-routes.ts +54 -0
  184. package/src/runtime/routes/gateway-log-routes.ts +14 -64
  185. package/src/runtime/routes/home-feed-routes.ts +10 -0
  186. package/src/runtime/routes/identity-intro-cache.ts +1 -1
  187. package/src/runtime/routes/identity-routes.ts +76 -20
  188. package/src/runtime/routes/inbound-message-handler.ts +0 -36
  189. package/src/runtime/routes/plugins-routes.ts +21 -0
  190. package/src/runtime/routes/schedule-routes.ts +19 -2
  191. package/src/runtime/routes/trust-rules-routes.ts +14 -67
  192. package/src/schedule/recurrence-engine.ts +34 -0
  193. package/src/schedule/scheduler.ts +1 -0
  194. package/src/skills/validate-input.ts +41 -1
  195. package/src/subagent/types.ts +26 -1
  196. package/src/telemetry/types.ts +15 -1
  197. package/src/telemetry/usage-telemetry-reporter.test.ts +6 -1
  198. package/src/telemetry/usage-telemetry-reporter.ts +1 -0
  199. package/src/tools/apps/executors.ts +1 -1
  200. package/src/tools/skills/skill-tool-factory.ts +19 -8
  201. package/src/usage/types.ts +8 -1
  202. package/src/util/platform.ts +16 -0
  203. package/src/watcher/engine.ts +1 -0
  204. package/src/workspace/adaptive-thinking-repair.ts +113 -0
  205. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +70 -67
  206. package/src/workspace/migrations/098-remove-stale-updates-bulletin-file.ts +31 -0
  207. package/src/workspace/migrations/099-disable-cache-one-shot-callsites.ts +81 -0
  208. package/src/workspace/migrations/registry.ts +4 -0
  209. package/src/__tests__/config-loader-quarantine-bulletin.test.ts +0 -202
  210. package/src/__tests__/conversation-starters-cadence.test.ts +0 -161
  211. package/src/__tests__/guardian-action-followup-executor.test.ts +0 -322
  212. package/src/__tests__/guardian-action-followup-store.test.ts +0 -373
  213. package/src/__tests__/guardian-action-late-reply.test.ts +0 -1083
  214. package/src/__tests__/update-bulletin-job.test.ts +0 -292
  215. package/src/config/schemas/updates.ts +0 -14
  216. package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +0 -227
  217. package/src/memory/conversation-starters-cadence.ts +0 -78
  218. package/src/prompts/update-bulletin-job.ts +0 -180
  219. 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
- _appendQuarantineBulletin: () => {},
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 { beforeEach, describe, expect, mock, test } from "bun:test";
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
- (_key: string): { conversationId: string } | null => ({
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 return static fallback when the dynamic greetings flag is off", async () => {
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 UPDATES.md is auto-allowed", async () => {
432
- const updatesPath = join(checkerTestDir, "UPDATES.md");
433
- const result = await check("file_read", { path: updatesPath }, "/tmp");
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 UPDATES.md is auto-allowed", async () => {
444
- const updatesPath = join(checkerTestDir, "UPDATES.md");
445
- const result = await check("file_edit", { path: updatesPath }, "/tmp");
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
- const updatesPath = join(WORKSPACE_DIR, "UPDATES.md");
329
- if (existsSync(updatesPath)) rmSync(updatesPath, { force: true });
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("UPDATES.md is not subscribed (only the registered handler set is)", () => {
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, "UPDATES.md"))).toBeUndefined();
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, update bulletins) runs on the workspace
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
- 1,
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-writer-clickhouse.ts", // ClickHouse compaction log writer — lazy lookup of clickhouse:url + clickhouse:password + vellum:platform_assistant_id for self-scoped event writes
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
- _appendQuarantineBulletin: () => undefined,
44
+ _writeQuarantineNotice: () => undefined,
45
45
  }));
46
46
 
47
47
  mock.module("../daemon/conversation-skill-tools.js", () => ({