@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
@@ -22,87 +22,90 @@ import type { WorkspaceMigration } from "./types.js";
22
22
  // - Already have thinking enabled (idempotent)
23
23
  // - Are source: "user" (user-created profiles are untouched)
24
24
  // - Have a non-managed, non-absent source (unknown origin)
25
- // - Use a non-Anthropic provider (adaptive thinking is Anthropic-specific)
25
+ // - Resolve to a non-Anthropic provider (adaptive thinking is
26
+ // Anthropic-specific). A profile with no explicit provider inherits
27
+ // llm.default.provider, so the check falls back to that — with a
28
+ // completely absent llm.default.provider treated as Anthropic, matching
29
+ // migration 052's own `?? "anthropic"` default. This keeps the legacy
30
+ // non-Anthropic empty `{}` shells seeded by migration 052 off the repair.
31
+ //
32
+ // Note: the live startup path re-runs the same repair after the platform
33
+ // overlay merge — see assistant/src/workspace/adaptive-thinking-repair.ts.
34
+ // This file keeps its own frozen copy of the logic because migration modules
35
+ // are self-contained snapshots and must never be imported by other code.
26
36
 
27
37
  const ADAPTIVE_THINKING = { enabled: true, streamThinking: true } as const;
28
38
  const TARGET_PROFILES = ["balanced", "quality-optimized"] as const;
29
39
 
30
- /**
31
- * Patch managed Anthropic profiles that are missing adaptive thinking.
32
- *
33
- * Exported so lifecycle.ts can re-run the repair after mergeDefaultWorkspaceConfig()
34
- * and seedInferenceProfiles(). On-platform instances with a config overlay have their
35
- * profiles overwritten by the overlay merge (which runs after workspace migrations),
36
- * so the migration alone is insufficient — the post-overlay call ensures the repair
37
- * sticks even when the overlay supplies profiles without thinking enabled.
38
- *
39
- * The function is idempotent: profiles that already have thinking enabled are skipped.
40
- */
41
- export function repairAdaptiveThinkingOnManagedProfiles(
42
- workspaceDir: string,
43
- ): void {
44
- const configPath = join(workspaceDir, "config.json");
45
- if (!existsSync(configPath)) return;
40
+ export const enableAdaptiveThinkingManagedProfilesMigration: WorkspaceMigration =
41
+ {
42
+ id: "097-enable-adaptive-thinking-managed-profiles",
43
+ description:
44
+ "Enable adaptive thinking on managed balanced and quality-optimized profiles",
45
+ run(workspaceDir: string): void {
46
+ const configPath = join(workspaceDir, "config.json");
47
+ if (!existsSync(configPath)) return;
46
48
 
47
- let config: Record<string, unknown>;
48
- try {
49
- const raw = JSON.parse(readFileSync(configPath, "utf-8"));
50
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
51
- config = raw as Record<string, unknown>;
52
- } catch {
53
- return;
54
- }
49
+ let config: Record<string, unknown>;
50
+ try {
51
+ const raw = JSON.parse(readFileSync(configPath, "utf-8"));
52
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
53
+ config = raw as Record<string, unknown>;
54
+ } catch {
55
+ return;
56
+ }
55
57
 
56
- const llm = readObject(config.llm);
57
- if (llm === null) return;
58
+ const llm = readObject(config.llm);
59
+ if (llm === null) return;
58
60
 
59
- const profiles = readObject(llm.profiles);
60
- if (profiles === null) return;
61
+ const profiles = readObject(llm.profiles);
62
+ if (profiles === null) return;
61
63
 
62
- let changed = false;
64
+ // Profiles without an explicit provider inherit llm.default.provider at
65
+ // resolution time; an absent llm.default.provider resolves to Anthropic.
66
+ const defaultBlock = readObject(llm.default);
67
+ const defaultProvider =
68
+ typeof defaultBlock?.provider === "string"
69
+ ? defaultBlock.provider
70
+ : "anthropic";
63
71
 
64
- for (const name of TARGET_PROFILES) {
65
- const profile = readObject(profiles[name]);
66
- if (profile === null) continue;
72
+ let changed = false;
67
73
 
68
- // Only patch managed Anthropic profiles.
69
- // Legacy profiles created before the `source` metadata field was introduced
70
- // have source=undefined. Treat these as managed when the profile name is one
71
- // of the canonical managed names (which TARGET_PROFILES already guarantees)
72
- // and the provider is Anthropic (or absent). Explicit `source: "user"`
73
- // profiles are always skipped.
74
- if (profile.source === "user") continue;
75
- if (profile.source !== undefined && profile.source !== "managed") continue;
76
- if (
77
- typeof profile.provider === "string" &&
78
- profile.provider !== "anthropic"
79
- ) {
80
- continue;
81
- }
74
+ for (const name of TARGET_PROFILES) {
75
+ const profile = readObject(profiles[name]);
76
+ if (profile === null) continue;
82
77
 
83
- // Skip if thinking is already enabled.
84
- const thinking = readObject(profile.thinking);
85
- if (thinking !== null && thinking.enabled === true) continue;
78
+ // Only patch managed Anthropic profiles.
79
+ // Legacy profiles created before the `source` metadata field was
80
+ // introduced have source=undefined. Treat these as managed when the
81
+ // profile name is one of the canonical managed names (which
82
+ // TARGET_PROFILES already guarantees) and the effective provider —
83
+ // explicit, or inherited from llm.default — is Anthropic. Explicit
84
+ // `source: "user"` profiles are always skipped.
85
+ if (profile.source === "user") continue;
86
+ if (profile.source !== undefined && profile.source !== "managed") {
87
+ continue;
88
+ }
89
+ const effectiveProvider =
90
+ typeof profile.provider === "string"
91
+ ? profile.provider
92
+ : defaultProvider;
93
+ if (effectiveProvider !== "anthropic") continue;
86
94
 
87
- profile.thinking = { ...ADAPTIVE_THINKING };
88
- profiles[name] = profile;
89
- changed = true;
90
- }
95
+ // Skip if thinking is already enabled.
96
+ const thinking = readObject(profile.thinking);
97
+ if (thinking !== null && thinking.enabled === true) continue;
91
98
 
92
- if (changed) {
93
- llm.profiles = profiles;
94
- config.llm = llm;
95
- writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
96
- }
97
- }
99
+ profile.thinking = { ...ADAPTIVE_THINKING };
100
+ profiles[name] = profile;
101
+ changed = true;
102
+ }
98
103
 
99
- export const enableAdaptiveThinkingManagedProfilesMigration: WorkspaceMigration =
100
- {
101
- id: "097-enable-adaptive-thinking-managed-profiles",
102
- description:
103
- "Enable adaptive thinking on managed balanced and quality-optimized profiles",
104
- run(workspaceDir: string): void {
105
- repairAdaptiveThinkingOnManagedProfiles(workspaceDir);
104
+ if (changed) {
105
+ llm.profiles = profiles;
106
+ config.llm = llm;
107
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
108
+ }
106
109
  },
107
110
  down(_workspaceDir: string): void {
108
111
  // Forward-only.
@@ -0,0 +1,31 @@
1
+ import { rmSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ import type { WorkspaceMigration } from "./types.js";
5
+
6
+ const MIGRATION_ID = "098-remove-stale-updates-bulletin-file";
7
+
8
+ /**
9
+ * Delete `<workspace>/UPDATES.md`.
10
+ *
11
+ * Release notes used to be appended to the file by workspace migrations and
12
+ * processed by a background conversation dispatched at daemon startup. That
13
+ * feature has been removed — nothing consumes the file's contents anymore,
14
+ * and it was system-written bulletin material, so it is deleted outright.
15
+ *
16
+ * Idempotent: `rmSync` with `force` is a no-op when the file is absent.
17
+ */
18
+ export const removeStaleUpdatesBulletinFileMigration: WorkspaceMigration = {
19
+ id: MIGRATION_ID,
20
+ description:
21
+ "Delete leftover UPDATES.md release-note bulletins (processing job removed)",
22
+
23
+ run(workspaceDir: string): void {
24
+ rmSync(join(workspaceDir, "UPDATES.md"), { force: true });
25
+ },
26
+
27
+ down(_workspaceDir: string): void {
28
+ // Forward-only: the removed content was pending release-note bulletins
29
+ // for a feature that no longer exists.
30
+ },
31
+ };
@@ -0,0 +1,81 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ import type { WorkspaceMigration } from "./types.js";
5
+
6
+ /**
7
+ * Add `disableCache: true` to existing on-disk entries for the one-shot LLM
8
+ * call sites whose shipped defaults now opt out of prompt caching.
9
+ *
10
+ * The resolver prefers an explicit `llm.callSites.<id>` entry over
11
+ * CALL_SITE_DEFAULTS, and earlier migrations (054 recall, 072 replySuggestion,
12
+ * and platform overlays for the others) seeded such entries. Without this
13
+ * migration, upgraded workspaces would keep paying prompt-cache writes on
14
+ * call sites whose prompts never produce a cache read — only fresh installs
15
+ * would pick up the new shipped default.
16
+ *
17
+ * Idempotent and customization-preserving: an entry that already carries a
18
+ * `disableCache` key (either value — the user may have opted back in) is
19
+ * left untouched, and only the one key is added to entries we patch. Entries
20
+ * that don't exist on disk are skipped — those workspaces already resolve
21
+ * the shipped default.
22
+ */
23
+ const ONE_SHOT_CALL_SITES = [
24
+ "recall",
25
+ "replySuggestion",
26
+ "homeGreeting",
27
+ "homeSuggestedPrompts",
28
+ "conversationTitle",
29
+ "memoryConsolidation",
30
+ ] as const;
31
+
32
+ export const disableCacheOneShotCallsitesMigration: WorkspaceMigration = {
33
+ id: "099-disable-cache-one-shot-callsites",
34
+ description:
35
+ "Add disableCache: true to existing one-shot call-site entries so upgraded workspaces match the new shipped defaults",
36
+ run(workspaceDir: string): void {
37
+ if (process.env.VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH) return;
38
+
39
+ const configPath = join(workspaceDir, "config.json");
40
+ if (!existsSync(configPath)) return;
41
+
42
+ let config: Record<string, unknown> = {};
43
+ try {
44
+ const raw = JSON.parse(readFileSync(configPath, "utf-8"));
45
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
46
+ config = raw as Record<string, unknown>;
47
+ } catch {
48
+ return;
49
+ }
50
+
51
+ const llm = readObject(config.llm);
52
+ if (llm === null) return;
53
+ const callSites = readObject(llm.callSites);
54
+ if (callSites === null) return;
55
+
56
+ let changed = false;
57
+ for (const callSite of ONE_SHOT_CALL_SITES) {
58
+ const entry = readObject(callSites[callSite]);
59
+ if (entry === null) continue;
60
+ if ("disableCache" in entry) continue;
61
+ entry.disableCache = true;
62
+ callSites[callSite] = entry;
63
+ changed = true;
64
+ }
65
+ if (!changed) return;
66
+
67
+ llm.callSites = callSites;
68
+ config.llm = llm;
69
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
70
+ },
71
+ down(_workspaceDir: string): void {
72
+ // Forward-only.
73
+ },
74
+ };
75
+
76
+ function readObject(value: unknown): Record<string, unknown> | null {
77
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
78
+ return null;
79
+ }
80
+ return value as Record<string, unknown>;
81
+ }
@@ -95,6 +95,8 @@ import { seedAvatarManifestMigration } from "./094-seed-avatar-manifest.js";
95
95
  import { bumpHeartbeatInterval30mTo60mMigration } from "./095-bump-heartbeat-interval-30m-to-60m.js";
96
96
  import { reduceQualityProfileEffortMigration } from "./096-reduce-quality-profile-effort.js";
97
97
  import { enableAdaptiveThinkingManagedProfilesMigration } from "./097-enable-adaptive-thinking-managed-profiles.js";
98
+ import { removeStaleUpdatesBulletinFileMigration } from "./098-remove-stale-updates-bulletin-file.js";
99
+ import { disableCacheOneShotCallsitesMigration } from "./099-disable-cache-one-shot-callsites.js";
98
100
  import { migrateToWorkspaceVolumeMigration } from "./migrate-to-workspace-volume.js";
99
101
  import type { WorkspaceMigration } from "./types.js";
100
102
 
@@ -201,4 +203,6 @@ export const WORKSPACE_MIGRATIONS: WorkspaceMigration[] = [
201
203
  bumpHeartbeatInterval30mTo60mMigration,
202
204
  reduceQualityProfileEffortMigration,
203
205
  enableAdaptiveThinkingManagedProfilesMigration,
206
+ removeStaleUpdatesBulletinFileMigration,
207
+ disableCacheOneShotCallsitesMigration,
204
208
  ];
@@ -1,202 +0,0 @@
1
- /**
2
- * When loadConfig()/loadRawConfig() quarantines a corrupt config.json, it
3
- * appends a bulletin to <workspace>/UPDATES.md so the background update-
4
- * bulletin job picks up the event inside a background-only conversation.
5
- * The agent decides whether to surface the quarantine to the user — it's
6
- * agent-visible context, not a push notification.
7
- *
8
- * The bulletin is keyed on the quarantine filename via an HTML marker so
9
- * repeated appends for the same quarantine are idempotent — per the
10
- * Release-Update-Hygiene rule in the root AGENTS.md, idempotency at both the
11
- * runner level AND an in-file marker is required to close the crash-mid-append
12
- * window.
13
- */
14
-
15
- import {
16
- existsSync,
17
- mkdirSync,
18
- readdirSync,
19
- readFileSync,
20
- rmSync,
21
- writeFileSync,
22
- } from "node:fs";
23
- import { basename, join } from "node:path";
24
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
25
-
26
- import {
27
- _appendQuarantineBulletin,
28
- invalidateConfigCache,
29
- loadConfig,
30
- } from "../config/loader.js";
31
- import { setStorePathForTesting } from "./encrypted-store-test-helpers.js";
32
-
33
- // ---------------------------------------------------------------------------
34
- // Helpers
35
- // ---------------------------------------------------------------------------
36
-
37
- const WORKSPACE_DIR = process.env.VELLUM_WORKSPACE_DIR!;
38
- const CONFIG_PATH = join(WORKSPACE_DIR, "config.json");
39
- const UPDATES_PATH = join(WORKSPACE_DIR, "UPDATES.md");
40
-
41
- function ensureTestDir(): void {
42
- const dirs = [
43
- WORKSPACE_DIR,
44
- join(WORKSPACE_DIR, "data"),
45
- join(WORKSPACE_DIR, "data", "memory"),
46
- join(WORKSPACE_DIR, "data", "memory", "knowledge"),
47
- join(WORKSPACE_DIR, "data", "logs"),
48
- ];
49
- for (const dir of dirs) {
50
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
51
- }
52
- }
53
-
54
- function resetWorkspace(): void {
55
- for (const name of readdirSync(WORKSPACE_DIR)) {
56
- rmSync(join(WORKSPACE_DIR, name), { recursive: true, force: true });
57
- }
58
- ensureTestDir();
59
- }
60
-
61
- function listQuarantinedFiles(): string[] {
62
- return readdirSync(WORKSPACE_DIR).filter((name) =>
63
- /^config\.json\.corrupt-.+\.json$/.test(name),
64
- );
65
- }
66
-
67
- // ---------------------------------------------------------------------------
68
- // Tests
69
- // ---------------------------------------------------------------------------
70
-
71
- describe("config-quarantine UPDATES.md bulletin", () => {
72
- beforeEach(() => {
73
- resetWorkspace();
74
- setStorePathForTesting(join(WORKSPACE_DIR, "keys.enc"));
75
- invalidateConfigCache();
76
- });
77
-
78
- afterEach(() => {
79
- setStorePathForTesting(null);
80
- invalidateConfigCache();
81
- });
82
-
83
- test("writes a bulletin with the quarantine marker when config.json is corrupt", () => {
84
- writeFileSync(CONFIG_PATH, '{"provider": "anthropic", "mo');
85
-
86
- loadConfig();
87
-
88
- const [quarantinedName] = listQuarantinedFiles();
89
- expect(quarantinedName).toBeDefined();
90
- const quarantinedPath = join(WORKSPACE_DIR, quarantinedName);
91
-
92
- expect(existsSync(UPDATES_PATH)).toBe(true);
93
- const body = readFileSync(UPDATES_PATH, "utf-8");
94
-
95
- // Heading must match the wording the background update-bulletin job
96
- // relays to the user.
97
- expect(body).toContain("## Config was reset to defaults");
98
- // Quarantine file path is what the user cats to recover.
99
- expect(body).toContain(quarantinedPath);
100
- // Idempotency marker is shape-exact, basename-keyed (not full path).
101
- expect(body).toContain(`<!-- config-quarantine:${quarantinedName} -->`);
102
- expect(body).toMatch(
103
- /<!-- config-quarantine:config\.json\.corrupt-.+\.json -->/,
104
- );
105
- });
106
-
107
- test("two successive quarantines append two distinct bulletins (not overwrite)", () => {
108
- // First corruption round.
109
- writeFileSync(CONFIG_PATH, '{"partial": ');
110
- loadConfig();
111
- invalidateConfigCache();
112
-
113
- const firstQuarantined = listQuarantinedFiles();
114
- expect(firstQuarantined).toHaveLength(1);
115
- const firstBody = readFileSync(UPDATES_PATH, "utf-8");
116
- const firstMarker = `<!-- config-quarantine:${firstQuarantined[0]} -->`;
117
- expect(firstBody).toContain(firstMarker);
118
-
119
- // Loader wrote a fresh default config.json after quarantine; corrupt it
120
- // again. Sleep briefly to guarantee a different ISO-timestamp millisecond
121
- // component and therefore a distinct quarantine filename.
122
- const untilDifferentMs = Date.now() + 5;
123
- while (Date.now() < untilDifferentMs) {
124
- /* spin */
125
- }
126
- writeFileSync(CONFIG_PATH, "still not json");
127
- loadConfig();
128
-
129
- const quarantined = listQuarantinedFiles().sort();
130
- expect(quarantined).toHaveLength(2);
131
-
132
- const body = readFileSync(UPDATES_PATH, "utf-8");
133
- for (const name of quarantined) {
134
- expect(body).toContain(`<!-- config-quarantine:${name} -->`);
135
- }
136
- // Two "Config was reset to defaults" sections — appended, not overwritten.
137
- const headingMatches = body.match(/## Config was reset to defaults/g) ?? [];
138
- expect(headingMatches).toHaveLength(2);
139
- // The earlier marker is still present (append semantics).
140
- expect(body).toContain(firstMarker);
141
- });
142
-
143
- test("idempotent: pre-existing marker for the same quarantine filename skips the append", () => {
144
- // Simulate a crash-mid-append: UPDATES.md already contains a marker for
145
- // a specific quarantine filename. A follow-up call referencing the same
146
- // filename must leave the file untouched.
147
- const quarantineName = "config.json.corrupt-2026-04-20T12-00-00.000Z.json";
148
- const quarantinePath = join(WORKSPACE_DIR, quarantineName);
149
- const preexisting =
150
- `## Config was reset to defaults\n\n` +
151
- `Pre-existing bulletin for ${quarantinePath}.\n\n` +
152
- `<!-- config-quarantine:${quarantineName} -->\n`;
153
- writeFileSync(UPDATES_PATH, preexisting, "utf-8");
154
- // Also create the quarantine file on disk so the helper sees an
155
- // environment consistent with a prior successful rename.
156
- writeFileSync(quarantinePath, "{ not json", "utf-8");
157
-
158
- _appendQuarantineBulletin(CONFIG_PATH, quarantinePath);
159
-
160
- const after = readFileSync(UPDATES_PATH, "utf-8");
161
- expect(after).toBe(preexisting);
162
- // Exactly one marker present — no duplicate was appended.
163
- expect(
164
- after.match(
165
- new RegExp(`<!-- config-quarantine:${quarantineName} -->`, "g"),
166
- )?.length,
167
- ).toBe(1);
168
- expect(basename(quarantinePath)).toBe(quarantineName);
169
- });
170
-
171
- test("valid config.json does not create UPDATES.md (regression guard)", () => {
172
- writeFileSync(
173
- CONFIG_PATH,
174
- JSON.stringify({ provider: "anthropic", model: "claude-opus-4-7" }),
175
- );
176
-
177
- loadConfig();
178
-
179
- expect(listQuarantinedFiles()).toHaveLength(0);
180
- expect(existsSync(UPDATES_PATH)).toBe(false);
181
- });
182
-
183
- test("appends (does not overwrite) when UPDATES.md already has unrelated content", () => {
184
- const priorContent =
185
- `## Some earlier bulletin\n\n` +
186
- `Unrelated prior content from a previous migration.\n\n` +
187
- `<!-- release-note-id:unrelated-note -->\n`;
188
- writeFileSync(UPDATES_PATH, priorContent, "utf-8");
189
-
190
- writeFileSync(CONFIG_PATH, "{oops");
191
- loadConfig();
192
-
193
- const body = readFileSync(UPDATES_PATH, "utf-8");
194
- // Prior content preserved verbatim at the start.
195
- expect(body.startsWith(priorContent)).toBe(true);
196
- // New bulletin appended.
197
- expect(body).toContain("## Config was reset to defaults");
198
- expect(body).toMatch(
199
- /<!-- config-quarantine:config\.json\.corrupt-.+\.json -->/,
200
- );
201
- });
202
- });
@@ -1,161 +0,0 @@
1
- import { beforeEach, describe, expect, mock, test } from "bun:test";
2
-
3
- import { v4 as uuid } from "uuid";
4
-
5
- mock.module("../util/logger.js", () => ({
6
- getLogger: () =>
7
- new Proxy({} as Record<string, unknown>, {
8
- get: () => () => {},
9
- }),
10
- }));
11
-
12
- import { maybeEnqueueConversationStartersJob } from "../memory/conversation-starters-cadence.js";
13
- import { getSqlite } from "../memory/db-connection.js";
14
- import { initializeDb } from "../memory/db-init.js";
15
- initializeDb();
16
-
17
- function clearTables() {
18
- getSqlite().run("DELETE FROM memory_graph_nodes");
19
- getSqlite().run("DELETE FROM memory_jobs");
20
- getSqlite().run("DELETE FROM memory_checkpoints");
21
- }
22
-
23
- function insertMemoryNode(scopeId = "default") {
24
- const now = Date.now();
25
- getSqlite().run(
26
- `INSERT INTO memory_graph_nodes (
27
- id, content, type, created, last_accessed, last_consolidated,
28
- emotional_charge, fidelity, confidence, significance,
29
- stability, reinforcement_count, last_reinforced,
30
- source_conversations, source_type, scope_id
31
- ) VALUES (?, ?, ?, ?, ?, ?, ?, 'vivid', 0.8, 0.5, 14, 0, ?, '[]', 'inferred', ?)`,
32
- [
33
- uuid(),
34
- "test statement",
35
- "semantic",
36
- now,
37
- now,
38
- now,
39
- '{"valence":0,"intensity":0.1,"decayCurve":"linear","decayRate":0.05,"originalIntensity":0.1}',
40
- now,
41
- scopeId,
42
- ],
43
- );
44
- }
45
-
46
- function setCheckpoint(key: string, value: string) {
47
- getSqlite().run(
48
- `INSERT OR REPLACE INTO memory_checkpoints (key, value, updated_at) VALUES (?, ?, ?)`,
49
- [key, value, Date.now()],
50
- );
51
- }
52
-
53
- function getPendingJobs(): Array<{ type: string }> {
54
- return getSqlite()
55
- .prepare(
56
- `SELECT type FROM memory_jobs WHERE type = 'generate_conversation_starters' AND status = 'pending'`,
57
- )
58
- .all() as Array<{ type: string }>;
59
- }
60
-
61
- beforeEach(() => {
62
- clearTables();
63
- });
64
-
65
- describe("maybeEnqueueConversationStartersJob", () => {
66
- test("no-op when zero memory nodes", () => {
67
- maybeEnqueueConversationStartersJob("default");
68
- expect(getPendingJobs()).toHaveLength(0);
69
- });
70
-
71
- test("enqueues when threshold exceeded (<=10 nodes, threshold=1)", () => {
72
- insertMemoryNode();
73
- insertMemoryNode();
74
-
75
- maybeEnqueueConversationStartersJob("default");
76
- expect(getPendingJobs()).toHaveLength(1);
77
- });
78
-
79
- test("no-op when delta below threshold", () => {
80
- for (let i = 0; i < 5; i++) insertMemoryNode();
81
-
82
- // Set checkpoint to current count — no new items since last gen
83
- setCheckpoint("conversation_starters:item_count_at_last_gen:default", "5");
84
-
85
- maybeEnqueueConversationStartersJob("default");
86
- expect(getPendingJobs()).toHaveLength(0);
87
- });
88
-
89
- test("uses higher threshold for larger memory counts (>50 nodes, threshold=10)", () => {
90
- for (let i = 0; i < 55; i++) insertMemoryNode();
91
-
92
- // Set checkpoint so delta is only 4 (below threshold of 10)
93
- setCheckpoint(
94
- "conversation_starters:item_count_at_last_gen:default",
95
- "51",
96
- );
97
-
98
- maybeEnqueueConversationStartersJob("default");
99
- expect(getPendingJobs()).toHaveLength(0);
100
-
101
- // Add more to exceed threshold
102
- for (let i = 0; i < 6; i++) insertMemoryNode();
103
-
104
- maybeEnqueueConversationStartersJob("default");
105
- expect(getPendingJobs()).toHaveLength(1);
106
- });
107
-
108
- test("dedup prevents double-enqueue", () => {
109
- insertMemoryNode();
110
- insertMemoryNode();
111
-
112
- maybeEnqueueConversationStartersJob("default");
113
- expect(getPendingJobs()).toHaveLength(1);
114
-
115
- // Call again — should not create a second job
116
- maybeEnqueueConversationStartersJob("default");
117
- expect(getPendingJobs()).toHaveLength(1);
118
- });
119
-
120
- test("enqueues when active memory count drops below the last-generation checkpoint", () => {
121
- // Start with 5 nodes and set checkpoint to 5
122
- for (let i = 0; i < 5; i++) insertMemoryNode();
123
- setCheckpoint("conversation_starters:item_count_at_last_gen:default", "5");
124
-
125
- // Simulate pruning: mark 3 nodes as gone (reducing totalActive to 2)
126
- const ids = (
127
- getSqlite()
128
- .prepare(`SELECT id FROM memory_graph_nodes LIMIT 3`)
129
- .all() as Array<{ id: string }>
130
- ).map((r) => r.id);
131
- for (const id of ids) {
132
- getSqlite().run(
133
- `UPDATE memory_graph_nodes SET fidelity = 'gone' WHERE id = ?`,
134
- [id],
135
- );
136
- }
137
-
138
- // The checkpoint is now ahead of the active memory count. This should
139
- // enqueue a refresh immediately so stale starters can recover.
140
- maybeEnqueueConversationStartersJob("default");
141
- expect(getPendingJobs()).toHaveLength(1);
142
- });
143
-
144
- test("scopes are independent", () => {
145
- insertMemoryNode("scope-a");
146
- insertMemoryNode("scope-b");
147
-
148
- maybeEnqueueConversationStartersJob("scope-a");
149
- maybeEnqueueConversationStartersJob("scope-b");
150
-
151
- const jobs = getSqlite()
152
- .prepare(
153
- `SELECT payload FROM memory_jobs WHERE type = 'generate_conversation_starters' AND status = 'pending'`,
154
- )
155
- .all() as Array<{ payload: string }>;
156
-
157
- expect(jobs).toHaveLength(2);
158
- const payloads = jobs.map((j) => JSON.parse(j.payload).scopeId).sort();
159
- expect(payloads).toEqual(["scope-a", "scope-b"]);
160
- });
161
- });