@vellumai/assistant 0.8.11 → 0.8.12-staging.2

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 (244) 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 +633 -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-catalog-parity.test.ts +16 -0
  37. package/src/__tests__/llm-usage-store.test.ts +11 -0
  38. package/src/__tests__/log-export-workspace.test.ts +468 -3
  39. package/src/__tests__/memory-v2-static-injector.test.ts +22 -0
  40. package/src/__tests__/model-intents.test.ts +1 -1
  41. package/src/__tests__/oauth-cli.test.ts +19 -8
  42. package/src/__tests__/openai-provider.test.ts +34 -0
  43. package/src/__tests__/prechat-onboarding-contract.test.ts +0 -1
  44. package/src/__tests__/recurrence-engine.test.ts +45 -0
  45. package/src/__tests__/schedule-routes.test.ts +34 -0
  46. package/src/__tests__/scheduler-disk-pressure.test.ts +1 -1
  47. package/src/__tests__/script-proxy-conversation-manager.test.ts +10 -5
  48. package/src/__tests__/secret-fixtures.ts +20 -0
  49. package/src/__tests__/skill-tool-factory.test.ts +49 -0
  50. package/src/__tests__/subagent-role-registry.test.ts +24 -1
  51. package/src/__tests__/subagent-tools.test.ts +1 -0
  52. package/src/__tests__/system-prompt.test.ts +109 -11
  53. package/src/__tests__/tool-approval-handler.test.ts +85 -0
  54. package/src/__tests__/tool-audit-listener.test.ts +86 -0
  55. package/src/__tests__/tool-error-hook.test.ts +1 -0
  56. package/src/__tests__/tool-result-spool.test.ts +337 -0
  57. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
  58. package/src/__tests__/validate-input.test.ts +95 -1
  59. package/src/__tests__/workspace-migration-098-remove-stale-updates-bulletin-file.test.ts +65 -0
  60. package/src/__tests__/workspace-migration-099-disable-cache-one-shot-callsites.test.ts +139 -0
  61. package/src/__tests__/workspace-migration-100-upgrade-quality-profile-to-fable-5.test.ts +174 -0
  62. package/src/__tests__/workspace-migration-101-upgrade-balanced-economy-to-minimax-m3.test.ts +162 -0
  63. package/src/__tests__/workspace-release-notes-feature-flag-guard.test.ts +45 -95
  64. package/src/acp/__tests__/agent-process.test.ts +315 -2
  65. package/src/acp/__tests__/prepare-agent-env.test.ts +79 -5
  66. package/src/acp/agent-process.ts +163 -34
  67. package/src/acp/prepare-agent-env.ts +55 -15
  68. package/src/agent/loop.ts +81 -24
  69. package/src/api/events/usage-progress.ts +28 -0
  70. package/src/api/index.ts +6 -0
  71. package/src/background-wake/wake-intent-hooks.test.ts +2 -0
  72. package/src/bundler/app-bundler.ts +25 -42
  73. package/src/bundler/app-compiler.ts +8 -0
  74. package/src/calls/call-controller.ts +1 -1
  75. package/src/cli/commands/plugins.ts +248 -15
  76. package/src/cli/lib/__tests__/inspect-plugin.test.ts +318 -0
  77. package/src/cli/lib/__tests__/install-from-github.test.ts +16 -9
  78. package/src/cli/lib/__tests__/plugin-artifact.test.ts +183 -0
  79. package/src/cli/lib/__tests__/plugin-details.test.ts +158 -0
  80. package/src/cli/lib/__tests__/plugin-fingerprint.test.ts +245 -0
  81. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +307 -0
  82. package/src/cli/lib/inspect-plugin.ts +252 -0
  83. package/src/cli/lib/install-from-github.ts +214 -21
  84. package/src/cli/lib/list-installed-plugins.ts +17 -6
  85. package/src/cli/lib/plugin-artifact.ts +103 -0
  86. package/src/cli/lib/plugin-details.ts +18 -1
  87. package/src/cli/lib/plugin-fingerprint.ts +197 -0
  88. package/src/cli/lib/upgrade-plugin.ts +225 -0
  89. package/src/config/bundled-skills/subagent/SKILL.md +2 -0
  90. package/src/config/bundled-skills/subagent/TOOLS.json +8 -2
  91. package/src/config/call-site-defaults.ts +13 -2
  92. package/src/config/feature-flag-registry.json +8 -16
  93. package/src/config/loader.ts +52 -59
  94. package/src/config/schema.ts +0 -2
  95. package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
  96. package/src/config/schemas/__tests__/memory-v3.test.ts +10 -0
  97. package/src/config/schemas/llm.ts +10 -0
  98. package/src/config/schemas/memory-v2.ts +13 -0
  99. package/src/config/schemas/memory-v3.ts +92 -0
  100. package/src/config/seed-inference-profiles.ts +4 -8
  101. package/src/context/post-turn-tool-result-truncation.ts +32 -18
  102. package/src/context/tool-result-spool.ts +104 -0
  103. package/src/credential-execution/feature-gates.ts +0 -1
  104. package/src/daemon/conversation-agent-loop-handlers.ts +41 -16
  105. package/src/daemon/conversation-error.ts +6 -15
  106. package/src/daemon/conversation.ts +9 -0
  107. package/src/daemon/disk-pressure-policy.ts +0 -1
  108. package/src/daemon/lifecycle.ts +1 -20
  109. package/src/daemon/message-types/conversations.ts +2 -15
  110. package/src/daemon/trust-context.ts +1 -1
  111. package/src/events/tool-audit-listener.ts +40 -9
  112. package/src/heartbeat/__tests__/heartbeat-service.test.ts +1 -1
  113. package/src/home/__tests__/home-content-refresh.test.ts +114 -0
  114. package/src/home/__tests__/suggested-prompts.test.ts +86 -5
  115. package/src/home/home-content-refresh.ts +43 -31
  116. package/src/home/home-greeting-cache.ts +8 -1
  117. package/src/home/home-greeting.ts +13 -9
  118. package/src/home/suggested-prompts.ts +77 -24
  119. package/src/ipc/routes/trust-rules.test.ts +66 -72
  120. package/src/media/image-credentials.ts +2 -2
  121. package/src/memory/__tests__/compaction-log-store-clickhouse.test.ts +432 -0
  122. package/src/memory/{compaction-log-writer-clickhouse.ts → compaction-log-store-clickhouse.ts} +264 -55
  123. package/src/memory/conversation-attention-store.ts +1 -0
  124. package/src/memory/conversation-bootstrap.ts +18 -9
  125. package/src/memory/conversation-crud.ts +12 -2
  126. package/src/memory/conversation-title-service.ts +53 -9
  127. package/src/memory/delivery-channels.ts +0 -69
  128. package/src/memory/graph/extraction-job.ts +0 -15
  129. package/src/memory/guardian-action-store.ts +1 -376
  130. package/src/memory/llm-usage-store.ts +5 -1
  131. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +2 -2
  132. package/src/memory/v2/__tests__/consolidation-job.test.ts +183 -2
  133. package/src/memory/v2/__tests__/injection.test.ts +70 -0
  134. package/src/memory/v2/__tests__/static-context.test.ts +12 -0
  135. package/src/memory/v2/consolidation-job.ts +93 -9
  136. package/src/memory/v2/injection.ts +53 -0
  137. package/src/memory/v2/prompts/consolidation.ts +1 -0
  138. package/src/memory/v2/static-context.ts +13 -1
  139. package/src/memory/v2/sweep-job.ts +1 -1
  140. package/src/memory/v2/types.ts +5 -0
  141. package/src/plugin-api/types.ts +7 -0
  142. package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +300 -0
  143. package/src/plugins/defaults/exploration-drift/package.json +15 -0
  144. package/src/plugins/defaults/index.ts +25 -0
  145. package/src/plugins/defaults/memory-retrieval/injectors.ts +132 -4
  146. package/src/plugins/defaults/memory-v3-shadow/__tests__/card.test.ts +92 -0
  147. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +2 -1
  148. package/src/plugins/defaults/memory-v3-shadow/__tests__/fresh-set.test.ts +52 -0
  149. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
  150. package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +2 -1
  151. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +136 -5
  152. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +17 -0
  153. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +6 -0
  154. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +5 -1
  155. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +68 -4
  156. package/src/plugins/defaults/memory-v3-shadow/card.ts +49 -5
  157. package/src/plugins/defaults/memory-v3-shadow/fresh-set.ts +59 -0
  158. package/src/plugins/defaults/memory-v3-shadow/injector.ts +4 -2
  159. package/src/plugins/defaults/memory-v3-shadow/learned-edges.test.ts +169 -0
  160. package/src/plugins/defaults/memory-v3-shadow/learned-edges.ts +178 -0
  161. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +115 -26
  162. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +13 -9
  163. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +144 -22
  164. package/src/plugins/defaults/memory-v3-shadow/types.ts +24 -6
  165. package/src/plugins/defaults/title-generate/hooks/stop.ts +13 -0
  166. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +16 -0
  167. package/src/prompts/cache-boundary.ts +17 -0
  168. package/src/prompts/sections.ts +50 -17
  169. package/src/prompts/system-prompt.ts +12 -4
  170. package/src/prompts/templates/system-sections.ts +22 -0
  171. package/src/providers/__tests__/unparseable-tool-args.test.ts +53 -0
  172. package/src/providers/anthropic/client.ts +74 -28
  173. package/src/providers/gemini/client.ts +5 -1
  174. package/src/providers/minimax/client.ts +9 -0
  175. package/src/providers/model-catalog.ts +28 -0
  176. package/src/providers/model-intents.ts +3 -3
  177. package/src/providers/openai/chat-completions-provider.ts +4 -2
  178. package/src/providers/openai/responses-provider.ts +7 -2
  179. package/src/providers/retry.ts +8 -0
  180. package/src/providers/types.ts +11 -0
  181. package/src/providers/unparseable-tool-args.ts +56 -0
  182. package/src/runtime/AGENTS.md +6 -0
  183. package/src/runtime/__tests__/agent-wake.test.ts +2 -2
  184. package/src/runtime/agent-wake.ts +5 -5
  185. package/src/runtime/background-job-runner.ts +2 -2
  186. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +150 -3
  187. package/src/runtime/migrations/vbundle-import-analyzer.ts +29 -6
  188. package/src/runtime/migrations/vbundle-import-policy.ts +23 -0
  189. package/src/runtime/migrations/vbundle-importer.ts +9 -4
  190. package/src/runtime/migrations/vbundle-streaming-importer.ts +8 -3
  191. package/src/runtime/pre-first-message-gate.ts +1 -1
  192. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +241 -0
  193. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +132 -0
  194. package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +97 -185
  195. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +17 -0
  196. package/src/runtime/routes/__tests__/plugins-routes.test.ts +348 -0
  197. package/src/runtime/routes/__tests__/task-routes.test.ts +3 -3
  198. package/src/runtime/routes/btw-routes.ts +0 -14
  199. package/src/runtime/routes/conversation-compaction-routes.ts +86 -19
  200. package/src/runtime/routes/conversation-list-routes.ts +77 -5
  201. package/src/runtime/routes/conversation-management-routes.ts +54 -0
  202. package/src/runtime/routes/conversation-query-routes.ts +79 -4
  203. package/src/runtime/routes/gateway-log-routes.ts +14 -64
  204. package/src/runtime/routes/home-feed-routes.ts +10 -0
  205. package/src/runtime/routes/identity-intro-cache.ts +1 -1
  206. package/src/runtime/routes/identity-routes.ts +76 -20
  207. package/src/runtime/routes/inbound-message-handler.ts +0 -36
  208. package/src/runtime/routes/log-export-routes.ts +143 -96
  209. package/src/runtime/routes/plugins-routes.ts +380 -0
  210. package/src/runtime/routes/redact-staged-export.ts +259 -0
  211. package/src/runtime/routes/schedule-routes.ts +19 -2
  212. package/src/runtime/routes/trust-rules-routes.ts +14 -67
  213. package/src/schedule/recurrence-engine.ts +34 -0
  214. package/src/schedule/scheduler.ts +1 -0
  215. package/src/security/redact-json.ts +61 -0
  216. package/src/skills/validate-input.ts +41 -1
  217. package/src/subagent/types.ts +26 -1
  218. package/src/telemetry/types.ts +15 -1
  219. package/src/telemetry/usage-telemetry-reporter.test.ts +6 -1
  220. package/src/telemetry/usage-telemetry-reporter.ts +1 -0
  221. package/src/tools/apps/executors.ts +1 -1
  222. package/src/tools/skills/skill-tool-factory.ts +19 -8
  223. package/src/tools/tool-approval-handler.ts +31 -0
  224. package/src/usage/types.ts +8 -1
  225. package/src/util/platform.ts +16 -0
  226. package/src/watcher/engine.ts +1 -0
  227. package/src/workspace/adaptive-thinking-repair.ts +113 -0
  228. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +70 -67
  229. package/src/workspace/migrations/098-remove-stale-updates-bulletin-file.ts +31 -0
  230. package/src/workspace/migrations/099-disable-cache-one-shot-callsites.ts +81 -0
  231. package/src/workspace/migrations/100-upgrade-quality-profile-to-fable-5.ts +86 -0
  232. package/src/workspace/migrations/101-upgrade-balanced-economy-to-minimax-m3.ts +70 -0
  233. package/src/workspace/migrations/registry.ts +8 -0
  234. package/src/__tests__/config-loader-quarantine-bulletin.test.ts +0 -202
  235. package/src/__tests__/conversation-starters-cadence.test.ts +0 -161
  236. package/src/__tests__/guardian-action-followup-executor.test.ts +0 -322
  237. package/src/__tests__/guardian-action-followup-store.test.ts +0 -373
  238. package/src/__tests__/guardian-action-late-reply.test.ts +0 -1083
  239. package/src/__tests__/update-bulletin-job.test.ts +0 -292
  240. package/src/config/schemas/updates.ts +0 -14
  241. package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +0 -227
  242. package/src/memory/conversation-starters-cadence.ts +0 -78
  243. package/src/prompts/update-bulletin-job.ts +0 -180
  244. package/src/runtime/guardian-action-followup-executor.ts +0 -306
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.8.11",
3
+ "version": "0.8.12-staging.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -50,7 +50,6 @@
50
50
  "@vellumai/skill-host-contracts": "file:../packages/skill-host-contracts",
51
51
  "@vellumai/slack-text": "file:../packages/slack-text",
52
52
  "@vellumai/twilio-client": "file:../packages/twilio-client",
53
- "archiver": "7.0.1",
54
53
  "commander": "13.1.0",
55
54
  "croner": "10.0.1",
56
55
  "dotenv": "17.3.1",
@@ -97,7 +96,6 @@
97
96
  },
98
97
  "devDependencies": {
99
98
  "@microsoft/api-extractor": "7.58.8",
100
- "@types/archiver": "7.0.0",
101
99
  "@types/bun": "1.3.10",
102
100
  "@types/node": "25.5.0",
103
101
  "@types/semver": "7.5.8",
@@ -0,0 +1,185 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ rmSync,
6
+ writeFileSync,
7
+ } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
11
+
12
+ import { repairAdaptiveThinkingOnManagedProfiles } from "../workspace/adaptive-thinking-repair.js";
13
+ import { enableAdaptiveThinkingManagedProfilesMigration } from "../workspace/migrations/097-enable-adaptive-thinking-managed-profiles.js";
14
+
15
+ const ADAPTIVE = { enabled: true, streamThinking: true };
16
+
17
+ let workspaceDir: string;
18
+
19
+ beforeEach(() => {
20
+ workspaceDir = join(
21
+ tmpdir(),
22
+ `vellum-adaptive-thinking-repair-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
23
+ );
24
+ mkdirSync(workspaceDir, { recursive: true });
25
+ });
26
+
27
+ afterEach(() => {
28
+ if (existsSync(workspaceDir)) {
29
+ rmSync(workspaceDir, { recursive: true, force: true });
30
+ }
31
+ });
32
+
33
+ function writeConfig(data: Record<string, unknown>): void {
34
+ writeFileSync(
35
+ join(workspaceDir, "config.json"),
36
+ JSON.stringify(data, null, 2) + "\n",
37
+ );
38
+ }
39
+
40
+ function readConfig(): Record<string, unknown> {
41
+ return JSON.parse(readFileSync(join(workspaceDir, "config.json"), "utf-8"));
42
+ }
43
+
44
+ function profile(name: string): Record<string, unknown> {
45
+ const config = readConfig();
46
+ const llm = config.llm as Record<string, unknown>;
47
+ const profiles = llm.profiles as Record<string, unknown>;
48
+ return profiles[name] as Record<string, unknown>;
49
+ }
50
+
51
+ // The startup repair and migration 097 keep frozen copies of the same logic
52
+ // (migration modules must stay self-contained). Run every scenario against
53
+ // both so the copies can't drift.
54
+ const implementations: Array<[string, (dir: string) => void]> = [
55
+ ["startup repair", repairAdaptiveThinkingOnManagedProfiles],
56
+ [
57
+ "migration 097",
58
+ (dir) => enableAdaptiveThinkingManagedProfilesMigration.run(dir),
59
+ ],
60
+ ];
61
+
62
+ describe.each(implementations)("%s", (_label, run) => {
63
+ test("patches managed Anthropic profiles missing thinking", () => {
64
+ writeConfig({
65
+ llm: {
66
+ default: { provider: "anthropic", model: "claude-sonnet-4-6" },
67
+ profiles: {
68
+ balanced: { source: "managed", provider: "anthropic" },
69
+ "quality-optimized": {
70
+ source: "managed",
71
+ provider: "anthropic",
72
+ thinking: { enabled: false },
73
+ },
74
+ },
75
+ },
76
+ });
77
+
78
+ run(workspaceDir);
79
+
80
+ expect(profile("balanced").thinking).toEqual(ADAPTIVE);
81
+ expect(profile("quality-optimized").thinking).toEqual(ADAPTIVE);
82
+ });
83
+
84
+ test("skips source-less empty shells when llm.default.provider is non-Anthropic", () => {
85
+ // Migration 052 seeds empty {} shells (no source, no provider) on legacy
86
+ // non-Anthropic workspaces. They inherit the non-Anthropic provider from
87
+ // llm.default and must stay off the Anthropic-specific repair.
88
+ writeConfig({
89
+ llm: {
90
+ default: { provider: "gemini", model: "gemini-2.5-pro" },
91
+ profiles: {
92
+ balanced: {},
93
+ "quality-optimized": {},
94
+ },
95
+ },
96
+ });
97
+
98
+ run(workspaceDir);
99
+
100
+ expect(profile("balanced")).toEqual({});
101
+ expect(profile("quality-optimized")).toEqual({});
102
+ });
103
+
104
+ test("treats absent llm.default.provider as Anthropic for source-less profiles", () => {
105
+ writeConfig({
106
+ llm: {
107
+ default: { model: "claude-sonnet-4-6" },
108
+ profiles: {
109
+ balanced: {},
110
+ },
111
+ },
112
+ });
113
+
114
+ run(workspaceDir);
115
+
116
+ expect(profile("balanced").thinking).toEqual(ADAPTIVE);
117
+ });
118
+
119
+ test("explicit Anthropic provider on the profile wins over a non-Anthropic default", () => {
120
+ writeConfig({
121
+ llm: {
122
+ default: { provider: "gemini" },
123
+ profiles: {
124
+ balanced: { provider: "anthropic" },
125
+ },
126
+ },
127
+ });
128
+
129
+ run(workspaceDir);
130
+
131
+ expect(profile("balanced").thinking).toEqual(ADAPTIVE);
132
+ });
133
+
134
+ test("skips profiles with an explicit non-Anthropic provider", () => {
135
+ writeConfig({
136
+ llm: {
137
+ default: { provider: "anthropic" },
138
+ profiles: {
139
+ balanced: { provider: "gemini" },
140
+ },
141
+ },
142
+ });
143
+
144
+ run(workspaceDir);
145
+
146
+ expect(profile("balanced").thinking).toBeUndefined();
147
+ });
148
+
149
+ test("skips source: user profiles", () => {
150
+ writeConfig({
151
+ llm: {
152
+ default: { provider: "anthropic" },
153
+ profiles: {
154
+ balanced: { source: "user", provider: "anthropic" },
155
+ },
156
+ },
157
+ });
158
+
159
+ run(workspaceDir);
160
+
161
+ expect(profile("balanced").thinking).toBeUndefined();
162
+ });
163
+
164
+ test("is idempotent when thinking is already enabled", () => {
165
+ const config = {
166
+ llm: {
167
+ default: { provider: "anthropic" },
168
+ profiles: {
169
+ balanced: {
170
+ source: "managed",
171
+ thinking: { enabled: true, streamThinking: false },
172
+ },
173
+ },
174
+ },
175
+ };
176
+ writeConfig(config);
177
+
178
+ run(workspaceDir);
179
+
180
+ expect(profile("balanced").thinking).toEqual({
181
+ enabled: true,
182
+ streamThinking: false,
183
+ });
184
+ });
185
+ });
@@ -4,8 +4,9 @@
4
4
  * The compaction pipeline between `context_compacting` (start) and
5
5
  * `compaction_completed` (end) is plugin-owned, so consumers reconstruct a
6
6
  * compaction attempt purely from the pair — correlated by `compactionId`.
7
- * These tests pin the pairing contract: shared id, trigger, timestamps, and
8
- * pre-compaction state on the start event.
7
+ * These tests pin the pairing contract: shared id, trigger, timestamps,
8
+ * pre-compaction state on the start event, and the unnested
9
+ * `ContextWindowResult` fields on the end event.
9
10
  */
10
11
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
11
12
 
@@ -185,11 +186,11 @@ describe("AgentLoop compaction start/end event pair", () => {
185
186
  expect(end!.finishedAt).toBeGreaterThanOrEqual(end!.startedAt);
186
187
  expect(end!.finishedAt).toBeLessThanOrEqual(after);
187
188
 
188
- // Start carries the pre-compaction history; end carries the result and
189
- // the stripped history the summary was built from.
189
+ // Start carries the pre-compaction history; end carries the
190
+ // ContextWindowResult fields unnested on the event.
190
191
  expect(start!.messages.length).toBeGreaterThan(0);
191
- expect(end!.result.compacted).toBe(true);
192
- expect(end!.messages.length).toBeGreaterThan(0);
192
+ expect(end!.compacted).toBe(true);
193
+ expect(end!.exhausted).toBe(false);
193
194
 
194
195
  // Start precedes end in the event stream.
195
196
  expect(events.indexOf(start!)).toBeLessThan(events.indexOf(end!));
@@ -106,6 +106,7 @@ mock.module("@anthropic-ai/sdk", () => ({
106
106
  }));
107
107
 
108
108
  // Import after mocking
109
+ import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "../prompts/cache-boundary.js";
109
110
  import { AnthropicProvider } from "../providers/anthropic/client.js";
110
111
  import {
111
112
  isPlaceholderSentinelText,
@@ -308,6 +309,91 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
308
309
  });
309
310
  });
310
311
 
312
+ test("splits a boundary-carrying system prompt into two cached blocks and drops the tool breakpoint", async () => {
313
+ /**
314
+ * Tests that the SYSTEM_PROMPT_CACHE_BOUNDARY marker yields two
315
+ * independently cached system blocks, and that the explicit
316
+ * last-tool breakpoint is suppressed (the first system breakpoint
317
+ * already covers tools) so the total stays within Anthropic's
318
+ * 4-breakpoint budget.
319
+ */
320
+ // GIVEN a system prompt carrying a cache boundary, plus tools and a
321
+ // tool-use loop (so turn-start and tail breakpoints also apply)
322
+ const prompt = `stable sections${SYSTEM_PROMPT_CACHE_BOUNDARY}volatile sections`;
323
+ const messages: Message[] = [
324
+ userMsg("Do something"),
325
+ toolUseMsg("tu_1", "bash"),
326
+ toolResultMsg("tu_1", "output"),
327
+ ];
328
+
329
+ // WHEN the message is sent
330
+ await provider.sendMessage(messages, {
331
+ tools: sampleTools,
332
+ systemPrompt: prompt,
333
+ });
334
+
335
+ // THEN the system prompt is split into two blocks, each 1h-cached
336
+ const system = lastStreamParams!.system as Array<{
337
+ type: string;
338
+ text: string;
339
+ cache_control?: { type: string; ttl?: string };
340
+ }>;
341
+ expect(system).toHaveLength(2);
342
+ expect(system[0].text).toBe("stable sections");
343
+ expect(system[1].text).toBe("volatile sections");
344
+ expect(system[0].cache_control).toEqual({ type: "ephemeral", ttl: "1h" });
345
+ expect(system[1].cache_control).toEqual({ type: "ephemeral", ttl: "1h" });
346
+
347
+ // AND no tool carries a breakpoint (system block 1 covers them)
348
+ const tools = lastStreamParams!.tools as Array<{
349
+ cache_control?: { type: string; ttl?: string };
350
+ }>;
351
+ for (const tool of tools) {
352
+ expect(tool.cache_control).toBeUndefined();
353
+ }
354
+
355
+ // AND the turn-start and tail message breakpoints are unchanged,
356
+ // keeping the total at the 4-breakpoint cap
357
+ const sent = lastStreamParams!.messages as Array<{
358
+ role: string;
359
+ content: Array<{
360
+ type: string;
361
+ cache_control?: { type: string; ttl?: string };
362
+ }>;
363
+ }>;
364
+ const turnStart = sent[0];
365
+ expect(
366
+ turnStart.content[turnStart.content.length - 1].cache_control,
367
+ ).toEqual({ type: "ephemeral", ttl: "1h" });
368
+ const lastMsg = sent[sent.length - 1];
369
+ expect(lastMsg.content[lastMsg.content.length - 1].cache_control).toEqual({
370
+ type: "ephemeral",
371
+ ttl: "5m",
372
+ });
373
+ });
374
+
375
+ test("keeps the last-tool breakpoint when the system prompt has no boundary", async () => {
376
+ /**
377
+ * Tests that a boundary-free system prompt preserves the explicit
378
+ * tool breakpoint (single system block leaves budget for it).
379
+ */
380
+ // GIVEN a system prompt without a cache boundary and tools
381
+ // WHEN the message is sent
382
+ await provider.sendMessage([userMsg("Hi")], {
383
+ tools: sampleTools,
384
+ systemPrompt: "You are helpful.",
385
+ });
386
+
387
+ // THEN the last tool keeps its breakpoint
388
+ const tools = lastStreamParams!.tools as Array<{
389
+ cache_control?: { type: string; ttl?: string };
390
+ }>;
391
+ expect(tools[tools.length - 1].cache_control).toEqual({
392
+ type: "ephemeral",
393
+ ttl: "1h",
394
+ });
395
+ });
396
+
311
397
  // -----------------------------------------------------------------------
312
398
  // Tool cache control
313
399
  // -----------------------------------------------------------------------
@@ -389,6 +475,49 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
389
475
  ).not.toContain("extended-cache-ttl-2025-04-11");
390
476
  });
391
477
 
478
+ test("disableCache sends no cache_control anywhere and strips caller-stamped block markers", async () => {
479
+ // A one-shot call site that opted out of prompt caching: no breakpoint
480
+ // on system, tools, turn-start, or tail — and a caller-stamped block
481
+ // marker is removed rather than forwarded.
482
+ const messages: Message[] = [
483
+ {
484
+ role: "user",
485
+ content: [
486
+ cachedPrefixBlock("stable pages block"),
487
+ { type: "text", text: "Do something" },
488
+ ],
489
+ },
490
+ toolUseMsg("tu_1", "bash"),
491
+ toolResultMsg("tu_1", "output"),
492
+ ];
493
+ await provider.sendMessage(messages, {
494
+ tools: sampleTools,
495
+ systemPrompt: "You are a helpful assistant.",
496
+ config: { disableCache: true },
497
+ });
498
+
499
+ const system = lastStreamParams!.system as Array<{
500
+ cache_control?: unknown;
501
+ }>;
502
+ expect(system[0].cache_control).toBeUndefined();
503
+
504
+ const tools = lastStreamParams!.tools as Array<{
505
+ cache_control?: unknown;
506
+ }>;
507
+ for (const tool of tools) {
508
+ expect(tool.cache_control).toBeUndefined();
509
+ }
510
+
511
+ const sent = lastStreamParams!.messages as Array<{
512
+ content: Array<{ cache_control?: unknown }>;
513
+ }>;
514
+ for (const message of sent) {
515
+ for (const block of message.content) {
516
+ expect(block.cache_control).toBeUndefined();
517
+ }
518
+ }
519
+ });
520
+
392
521
  test("v3-shape call (system + tools + cached prefix block) stays within the 4-breakpoint cap", async () => {
393
522
  // Mirrors a v3 L2 selector call: a system prompt, a forced tool, and a user
394
523
  // message whose stable <pages> block carries a cache_control breakpoint
@@ -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;