@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
@@ -1,5 +1,6 @@
1
1
  import Anthropic from "@anthropic-ai/sdk";
2
2
 
3
+ import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "../../prompts/cache-boundary.js";
3
4
  import { isAbortReason } from "../../util/abort-reasons.js";
4
5
  import { ProviderError } from "../../util/errors.js";
5
6
  import { getLogger } from "../../util/logger.js";
@@ -809,6 +810,12 @@ export class AnthropicProvider implements Provider {
809
810
  const mutableLatestUserMessage =
810
811
  (config as Record<string, unknown> | undefined)
811
812
  ?.mutableLatestUserMessage === true;
813
+ // Full prompt-caching opt-out: send no cache breakpoints at all and strip
814
+ // caller-stamped block-level markers. Resolved per call site (see
815
+ // `disableCache` in the LLM config schema) for one-shot prompts where
816
+ // every breakpoint is a paid cache write with no future read.
817
+ const disableCache =
818
+ (config as Record<string, unknown> | undefined)?.disableCache === true;
812
819
  let sentMessages: Anthropic.MessageParam[] | undefined;
813
820
  const startedAt = Date.now();
814
821
  // Hoisted so the catch block can distinguish our inner stream timeout
@@ -998,6 +1005,7 @@ export class AnthropicProvider implements Provider {
998
1005
  cacheTtl: _cacheTtl,
999
1006
  disableTurnStartCache: _disableTurnStartCache,
1000
1007
  mutableLatestUserMessage: _mutableLatestUserMessage,
1008
+ disableCache: _disableCache,
1001
1009
  max_tokens: callerMaxTokens,
1002
1010
  usageAttributionHeaders,
1003
1011
  ...restConfig
@@ -1054,19 +1062,34 @@ export class AnthropicProvider implements Provider {
1054
1062
  };
1055
1063
 
1056
1064
  if (systemPrompt) {
1057
- // The whole system prompt is rendered as a single cached
1058
- // block. A 1-hour cache TTL is used (when supported by the
1059
- // model) so the breakpoint survives turn gaps that exceed the
1060
- // default 5-minute window.
1061
- params.system = [
1062
- {
1065
+ // The system prompt may carry a cache boundary (placed by the
1066
+ // section pipeline — see `prompts/sections.ts`) splitting it into
1067
+ // a stable-prefix block and a volatile-suffix block, each with
1068
+ // its own breakpoint so a volatile-section change doesn't
1069
+ // re-create the stable prefix. A 1-hour cache TTL is used (when
1070
+ // supported by the model) so the breakpoints survive turn gaps
1071
+ // that exceed the default 5-minute window.
1072
+ params.system = systemPrompt
1073
+ .split(SYSTEM_PROMPT_CACHE_BOUNDARY)
1074
+ .filter((text) => text.length > 0)
1075
+ .map((text) => ({
1063
1076
  type: "text" as const,
1064
- text: systemPrompt,
1065
- cache_control: cacheControl,
1066
- },
1067
- ];
1077
+ text,
1078
+ ...(disableCache ? {} : { cache_control: cacheControl }),
1079
+ }));
1080
+ if (params.system.length === 0) delete params.system;
1068
1081
  }
1069
1082
 
1083
+ // Tools precede the system blocks in the cached prefix, so the first
1084
+ // system breakpoint already covers the tool definitions. When the
1085
+ // system prompt is split into two blocks, skip the explicit last-tool
1086
+ // breakpoint to stay within Anthropic's 4-breakpoint budget; with a
1087
+ // single (or no) system block the tool breakpoint is kept.
1088
+ const systemBlockCount = Array.isArray(params.system)
1089
+ ? params.system.length
1090
+ : 0;
1091
+ const applyToolCacheControl = systemBlockCount < 2;
1092
+
1070
1093
  if (tools && tools.length > 0) {
1071
1094
  if (
1072
1095
  this.useNativeWebSearch &&
@@ -1077,7 +1100,9 @@ export class AnthropicProvider implements Provider {
1077
1100
  name: t.name,
1078
1101
  description: t.description,
1079
1102
  input_schema: t.input_schema as Anthropic.Tool["input_schema"],
1080
- ...(i === otherTools.length - 1
1103
+ ...(applyToolCacheControl &&
1104
+ !disableCache &&
1105
+ i === otherTools.length - 1
1081
1106
  ? { cache_control: cacheControl }
1082
1107
  : {}),
1083
1108
  }));
@@ -1092,7 +1117,9 @@ export class AnthropicProvider implements Provider {
1092
1117
  name: t.name,
1093
1118
  description: t.description,
1094
1119
  input_schema: t.input_schema as Anthropic.Tool["input_schema"],
1095
- ...(i === tools.length - 1 ? { cache_control: cacheControl } : {}),
1120
+ ...(applyToolCacheControl && !disableCache && i === tools.length - 1
1121
+ ? { cache_control: cacheControl }
1122
+ : {}),
1096
1123
  }));
1097
1124
  }
1098
1125
  }
@@ -1140,6 +1167,7 @@ export class AnthropicProvider implements Provider {
1140
1167
  mutableLatestUserMessage && turnStartIdx === msgs.length - 1;
1141
1168
  if (
1142
1169
  turnStartIdx >= 0 &&
1170
+ !disableCache &&
1143
1171
  !disableTurnStartCache &&
1144
1172
  !skipVolatileTurnStartAnchor
1145
1173
  ) {
@@ -1156,7 +1184,11 @@ export class AnthropicProvider implements Provider {
1156
1184
  // cache_creation tokens per new turn). Skipped during tool-use loops
1157
1185
  // where the current turn-start already covers the same prefix and a
1158
1186
  // second anchor would blow the 4-breakpoint budget.
1159
- if (turnStartIdx === msgs.length - 1 && turnStartIdx > 0) {
1187
+ if (
1188
+ !disableCache &&
1189
+ turnStartIdx === msgs.length - 1 &&
1190
+ turnStartIdx > 0
1191
+ ) {
1160
1192
  const prevTurnAnchorIdx = findUserTextMsgIdx(turnStartIdx - 1);
1161
1193
  if (prevTurnAnchorIdx >= 0)
1162
1194
  applyCacheControlToLastBlock(prevTurnAnchorIdx);
@@ -1168,7 +1200,11 @@ export class AnthropicProvider implements Provider {
1168
1200
  // cheaply without conflicting with the 1h breakpoints above.
1169
1201
  // Skip thinking/redacted_thinking blocks — Anthropic doesn't allow
1170
1202
  // cache_control on those types.
1171
- if (turnStartIdx >= 0 && turnStartIdx < sentMessages.length - 1) {
1203
+ if (
1204
+ !disableCache &&
1205
+ turnStartIdx >= 0 &&
1206
+ turnStartIdx < sentMessages.length - 1
1207
+ ) {
1172
1208
  const lastMsg = sentMessages[sentMessages.length - 1];
1173
1209
  if (Array.isArray(lastMsg.content) && lastMsg.content.length > 0) {
1174
1210
  const NON_CACHEABLE_TYPES = new Set([
@@ -1193,11 +1229,12 @@ export class AnthropicProvider implements Provider {
1193
1229
  }
1194
1230
  }
1195
1231
 
1196
- // Cache-breakpoint accounting: system(1) + tools(1) + turn-start(1) +
1197
- // (tail OR prev-turn-anchor)(1) = 4 — exactly Anthropic's per-request
1198
- // cap. Tail and prev-turn-anchor are mutually exclusive (the latter
1199
- // only fires when turn-start is the last message, which suppresses
1200
- // the tail), so the total can't drift past 4.
1232
+ // Cache-breakpoint accounting: system(≤2) + tools(1, only when the
1233
+ // system is a single block or absent) + turn-start(1) +
1234
+ // (tail OR prev-turn-anchor)(1) ≤ 4 — Anthropic's per-request cap.
1235
+ // Tail and prev-turn-anchor are mutually exclusive (the latter only
1236
+ // fires when turn-start is the last message, which suppresses the
1237
+ // tail), so the total can't drift past 4.
1201
1238
 
1202
1239
  // Strip orphaned UTF-16 surrogates so the Anthropic JSON parser never
1203
1240
  // sees invalid strings produced by upstream surrogate-splitting `.slice()` calls.
@@ -1208,19 +1245,28 @@ export class AnthropicProvider implements Provider {
1208
1245
  sentMessages = params.messages;
1209
1246
  }
1210
1247
 
1211
- // Haiku does not support the extended-cache-ttl beta, so it must never
1212
- // receive a `ttl` on any cache_control. The client's own breakpoints
1213
- // already omit it for Haiku, but a caller can stamp a `ttl` on message
1214
- // blocks before the provider sees them — strip it here so the request
1215
- // stays valid on Haiku models.
1216
- if (isHaiku) {
1248
+ // Callers can stamp `cache_control` on message blocks before the
1249
+ // provider sees them. Two repairs apply:
1250
+ // - `disableCache`: strip the marker entirely — this call opted out of
1251
+ // prompt caching, and a leftover block-level marker would still incur
1252
+ // a cache write.
1253
+ // - Haiku: strip only the `ttl` field — Haiku does not support the
1254
+ // extended-cache-ttl beta, so a `ttl` would make the request invalid.
1255
+ // The client's own breakpoints already omit it for Haiku.
1256
+ if (disableCache || isHaiku) {
1217
1257
  for (const msg of sentMessages) {
1218
1258
  if (!Array.isArray(msg.content)) continue;
1219
1259
  for (const block of msg.content) {
1220
1260
  if (typeof block === "string") continue;
1221
- const cc = (block as { cache_control?: { ttl?: unknown } })
1222
- .cache_control;
1223
- if (cc && "ttl" in cc) delete cc.ttl;
1261
+ const blockRecord = block as {
1262
+ cache_control?: { ttl?: unknown };
1263
+ };
1264
+ if (!blockRecord.cache_control) continue;
1265
+ if (disableCache) {
1266
+ delete blockRecord.cache_control;
1267
+ } else if ("ttl" in blockRecord.cache_control) {
1268
+ delete blockRecord.cache_control.ttl;
1269
+ }
1224
1270
  }
1225
1271
  }
1226
1272
  }
@@ -5,6 +5,7 @@ import {
5
5
  THINKING_LEVELS,
6
6
  type ThinkingLevel as ThinkingLevelName,
7
7
  } from "../../config/schemas/llm.js";
8
+ import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "../../prompts/cache-boundary.js";
8
9
  import { isAbortReason } from "../../util/abort-reasons.js";
9
10
  import { ProviderError } from "../../util/errors.js";
10
11
  import { getLogger } from "../../util/logger.js";
@@ -314,7 +315,10 @@ export class GeminiProvider implements Provider {
314
315
  const geminiConfig: genai.GenerateContentConfig = {};
315
316
 
316
317
  if (systemPrompt) {
317
- geminiConfig.systemInstruction = systemPrompt;
318
+ geminiConfig.systemInstruction = systemPrompt.replaceAll(
319
+ SYSTEM_PROMPT_CACHE_BOUNDARY,
320
+ "\n\n",
321
+ );
318
322
  }
319
323
  if (maxTokens) {
320
324
  geminiConfig.maxOutputTokens = maxTokens;
@@ -101,6 +101,15 @@ export class MinimaxProvider extends OpenAIChatCompletionsProvider {
101
101
  providerName: "minimax",
102
102
  providerLabel: "MiniMax",
103
103
  streamTimeoutMs: options.streamTimeoutMs,
104
+ // Without reasoning_split, MiniMax embeds reasoning in `content`
105
+ // wrapped in <think>...</think> tags (and also mirrors it into
106
+ // reasoning deltas), so raw tags leak into user-visible text. With it,
107
+ // reasoning arrives only via `reasoning_content`/`reasoning_details`,
108
+ // which the base provider already parses into thinking blocks.
109
+ extraCreateParams: { reasoning_split: true },
110
+ // MiniMax models reason between tool calls (interleaved thinking) and
111
+ // expect prior-turn reasoning replayed on multi-turn requests.
112
+ assistantReasoningField: "reasoning_content",
104
113
  });
105
114
  }
106
115
  }
@@ -13,7 +13,7 @@ const PROVIDER_MODEL_INTENTS: Record<string, Record<ModelIntent, string>> = {
13
13
  anthropic: {
14
14
  balanced: "claude-sonnet-4-6",
15
15
  "latency-optimized": "claude-haiku-4-5-20251001",
16
- "quality-optimized": "claude-opus-4-8",
16
+ "quality-optimized": "claude-fable-5",
17
17
  "vision-optimized": "claude-opus-4-6",
18
18
  },
19
19
  openai: {
@@ -43,7 +43,7 @@ const PROVIDER_MODEL_INTENTS: Record<string, Record<ModelIntent, string>> = {
43
43
  openrouter: {
44
44
  balanced: "anthropic/claude-sonnet-4.6",
45
45
  "latency-optimized": "anthropic/claude-haiku-4.5",
46
- "quality-optimized": "anthropic/claude-opus-4.8",
46
+ "quality-optimized": "anthropic/claude-fable-5",
47
47
  "vision-optimized": "anthropic/claude-opus-4.6",
48
48
  },
49
49
  };
@@ -1,5 +1,6 @@
1
1
  import OpenAI from "openai";
2
2
 
3
+ import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "../../prompts/cache-boundary.js";
3
4
  import { isAbortReason } from "../../util/abort-reasons.js";
4
5
  import { ProviderError } from "../../util/errors.js";
5
6
  import { extractRetryAfterMs } from "../../util/retry.js";
@@ -797,7 +798,7 @@ export class OpenAIChatCompletionsProvider implements Provider {
797
798
  if (systemPrompt) {
798
799
  result.push({
799
800
  role: "system",
800
- content: systemPrompt,
801
+ content: systemPrompt.replaceAll(SYSTEM_PROMPT_CACHE_BOUNDARY, "\n\n"),
801
802
  });
802
803
  }
803
804
 
@@ -1,5 +1,6 @@
1
1
  import OpenAI from "openai";
2
2
 
3
+ import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "../../prompts/cache-boundary.js";
3
4
  import { isAbortReason } from "../../util/abort-reasons.js";
4
5
  import { ProviderError } from "../../util/errors.js";
5
6
  import { getLogger } from "../../util/logger.js";
@@ -216,7 +217,10 @@ export class OpenAIResponsesProvider implements Provider {
216
217
  };
217
218
 
218
219
  if (systemPrompt) {
219
- params.instructions = systemPrompt;
220
+ params.instructions = systemPrompt.replaceAll(
221
+ SYSTEM_PROMPT_CACHE_BOUNDARY,
222
+ "\n\n",
223
+ );
220
224
  }
221
225
 
222
226
  if (maxTokens && !this.codexSubscription) {
@@ -268,6 +268,14 @@ function normalizeSendMessageOptions(
268
268
  if (nextConfig.thinking === undefined && resolved.thinking !== undefined) {
269
269
  nextConfig.thinking = resolved.thinking;
270
270
  }
271
+ // Not a wire field: consumed (and stripped) by provider clients that
272
+ // implement prompt caching, like `cacheTtl` / `disableTurnStartCache`.
273
+ if (
274
+ nextConfig.disableCache === undefined &&
275
+ resolved.disableCache !== undefined
276
+ ) {
277
+ nextConfig.disableCache = resolved.disableCache;
278
+ }
271
279
  // Forward OpenRouter-only routing preferences so `OpenRouterProvider` can
272
280
  // translate `openrouter.only` into the wire-format `provider: { only: [...] }`
273
281
  // body field on both the OpenAI-compat and Anthropic-compat endpoints.
@@ -222,6 +222,17 @@ export interface SendMessageConfig {
222
222
  * behavior.
223
223
  */
224
224
  mutableLatestUserMessage?: boolean;
225
+ /**
226
+ * When true, the provider sends no prompt-cache breakpoints at all (and
227
+ * strips any block-level `cache_control` markers callers stamped on
228
+ * messages). For one-shot call sites whose prompts are unique per call or
229
+ * whose call cadence exceeds the cache TTL, every breakpoint is a paid
230
+ * cache write that will never be read — opting out saves the write
231
+ * premium. Resolved per call site via `resolveCallSiteConfig` (see
232
+ * `disableCache` in the LLM config schema); a per-call explicit value
233
+ * wins. Default false — existing behavior.
234
+ */
235
+ disableCache?: boolean;
225
236
  [key: string]: unknown;
226
237
  }
227
238
 
@@ -20,6 +20,12 @@ SSE handlers built on `ReadableStream` shed slow subscribers when `controller.de
20
20
 
21
21
  GET handlers must be safe and side-effect-free — they must not enqueue background jobs, mutate database state, or trigger writes. If a feature needs server-initiated work in response to a client request, use an explicit POST endpoint or a push-based flow (SSE event → client refetch). See [RFC 9110 §9.2.1 — Safe Methods](https://httpwg.org/specs/rfc9110.html#safe.methods).
22
22
 
23
+ Accepted exceptions (stale-while-revalidate caches): a GET handler may kick off a bounded, fire-and-forget background refresh of a generated-content cache when no fresh cache exists, provided the handler itself stays read-only and returns immediately with cached/fallback copy, the refresh is single-flight (concurrent GETs share one regeneration), and a TTL bounds regeneration frequency. Current instances:
24
+
25
+ - `GET /v1/identity/intro` — refreshes the generated greeting cache; the background prompt may only depend on static identity/soul context plus caller-supplied local hour/minute.
26
+ - `GET /v1/home/feed` — refreshes the personalized home greeting and suggested-prompt caches via `revalidateHomeContentInBackground()`, which publishes `home_feed_updated` when fresh content lands so clients refetch. This is intentional: home content is generated on demand (when a user actually views Home), never at daemon startup or on a timer.
27
+ - `GET /v1/conversation-starters` — enqueues a `generate_conversation_starters` memory job when the starter set is stale, cooldown-gated and deduped against in-flight jobs.
28
+
23
29
  ### Approvals (confirmations, secrets, trust rules)
24
30
 
25
31
  Approvals are **orthogonal to message sending**. The assistant asks for approval whenever it needs one — this is a separate concern from how a message enters the system.
@@ -173,7 +173,7 @@ mock.module("../../config/loader.js", () => ({
173
173
  getNestedValue: () => undefined,
174
174
  setNestedValue: () => {},
175
175
  API_KEY_PROVIDERS: [],
176
- _appendQuarantineBulletin: () => {},
176
+ _writeQuarantineNotice: () => {},
177
177
  invalidateConfigCache: () => {},
178
178
  }));
179
179
 
@@ -837,7 +837,7 @@ describe("wakeAgentForOpportunity", () => {
837
837
  });
838
838
 
839
839
  test("applies caller-supplied trustContext to the target before the agent loop runs", async () => {
840
- // Background system jobs (memory consolidation, update-bulletin) need
840
+ // Background system jobs (e.g. memory consolidation) need
841
841
  // guardian trust to clear the side-effect approval gate. The wake must
842
842
  // call setTrustContext BEFORE agentLoop.run so the per-turn snapshot
843
843
  // captures the elevated trust.
@@ -66,7 +66,7 @@ import {
66
66
  import {
67
67
  recordCompactionEndBestEffort,
68
68
  recordCompactionStartBestEffort,
69
- } from "../memory/compaction-log-writer-clickhouse.js";
69
+ } from "../memory/compaction-log-store-clickhouse.js";
70
70
  import { getConversationOverrideProfile } from "../memory/conversation-crud.js";
71
71
  import {
72
72
  buildProviderErrorResponsePayload,
@@ -167,10 +167,10 @@ export interface WakeOptions {
167
167
  }
168
168
 
169
169
  /**
170
- * Reason a wake returned `invoked: false`. Callers (CLI, update-bulletin
171
- * job) need to distinguish "conversation doesn't exist" from "conversation
172
- * exists but stayed busy past the wait-until-idle timeout" — the former is
173
- * a user-visible error, the latter is an expected transient condition.
170
+ * Reason a wake returned `invoked: false`. Callers (e.g. the CLI) need to
171
+ * distinguish "conversation doesn't exist" from "conversation exists but
172
+ * stayed busy past the wait-until-idle timeout" — the former is a
173
+ * user-visible error, the latter is an expected transient condition.
174
174
  */
175
175
  export type WakeSkipReason =
176
176
  | "not_found"
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * `runBackgroundJob()` consolidates the bootstrap → processMessage → timeout
5
5
  * pattern that every background producer (heartbeat, filing, scheduler, memory
6
- * consolidation, watcher, update-bulletin, subagent, sequence) has been
6
+ * consolidation, watcher, subagent, sequence) has been
7
7
  * open-coding. Wrapping it here lets us:
8
8
  *
9
9
  * - apply a single timeout policy
@@ -182,7 +182,7 @@ export async function runBackgroundJob(
182
182
  // failed" rows visible in the sidebar the moment a real user hatches the
183
183
  // assistant — see `pre-first-message-gate.ts` for the rationale.
184
184
  //
185
- // Service-level callers (heartbeat, update-bulletin) are expected to gate
185
+ // Service-level callers (e.g. heartbeat) are expected to gate
186
186
  // earlier and never reach this point; reaching the gate here means a
187
187
  // caller either forgot to gate or deliberately opted in via
188
188
  // `allowPreFirstUserMessage`. We log at `info` (not `warn`) because the
@@ -150,9 +150,6 @@ describe("DefaultPathResolver prompts/USER.md translation", () => {
150
150
  expect(resolver.resolve("prompts/SOUL.md")).toBe(
151
151
  join(WORKSPACE_ROOT, "SOUL.md"),
152
152
  );
153
- expect(resolver.resolve("prompts/UPDATES.md")).toBe(
154
- join(WORKSPACE_ROOT, "UPDATES.md"),
155
- );
156
153
  });
157
154
 
158
155
  test("skips unknown prompt filenames regardless of guardian state", () => {
@@ -230,6 +227,71 @@ describe("analyzeImport for legacy prompts/USER.md", () => {
230
227
  const userMd = report.files.find((f) => f.path === "prompts/USER.md");
231
228
  expect(userMd!.action).toBe("skip");
232
229
  });
230
+
231
+ test("retired workspace/UPDATES.md (current format) is a non-blocking skip", () => {
232
+ const resolver = new DefaultPathResolver(
233
+ WORKSPACE_ROOT,
234
+ undefined,
235
+ () => null,
236
+ );
237
+
238
+ const { manifest } = buildVBundle({
239
+ files: [
240
+ {
241
+ path: "data/db/assistant.db",
242
+ data: new Uint8Array(),
243
+ },
244
+ {
245
+ path: "workspace/UPDATES.md",
246
+ data: new TextEncoder().encode("## Old release notes\n"),
247
+ },
248
+ ],
249
+ ...defaultV1Options(),
250
+ });
251
+
252
+ const report = analyzeImport({ manifest, pathResolver: resolver });
253
+
254
+ expect(report.can_import).toBe(true);
255
+ expect(report.conflicts).toHaveLength(0);
256
+ const updatesMd = report.files.find(
257
+ (f) => f.path === "workspace/UPDATES.md",
258
+ );
259
+ expect(updatesMd).toBeDefined();
260
+ expect(updatesMd!.action).toBe("skip");
261
+ });
262
+
263
+ test("retired prompts/UPDATES.md in a legacy bundle is a non-blocking skip", () => {
264
+ const resolver = new DefaultPathResolver(
265
+ WORKSPACE_ROOT,
266
+ undefined,
267
+ () => null,
268
+ );
269
+
270
+ const { manifest } = buildVBundle({
271
+ files: [
272
+ {
273
+ path: "data/db/assistant.db",
274
+ data: new Uint8Array(),
275
+ },
276
+ {
277
+ path: "prompts/UPDATES.md",
278
+ data: new TextEncoder().encode("## Old release notes\n"),
279
+ },
280
+ ],
281
+ ...defaultV1Options(),
282
+ });
283
+
284
+ const report = analyzeImport({ manifest, pathResolver: resolver });
285
+
286
+ // Bundles exported while the update-bulletin feature existed must stay
287
+ // restorable — the retired file is dropped, never written back, and
288
+ // must not register an UNKNOWN_ARCHIVE_PATH conflict.
289
+ expect(report.can_import).toBe(true);
290
+ expect(report.conflicts).toHaveLength(0);
291
+ const updatesMd = report.files.find((f) => f.path === "prompts/UPDATES.md");
292
+ expect(updatesMd).toBeDefined();
293
+ expect(updatesMd!.action).toBe("skip");
294
+ });
233
295
  });
234
296
 
235
297
  // ---------------------------------------------------------------------------
@@ -358,6 +420,91 @@ describe("commitImport for legacy prompts/USER.md", () => {
358
420
  expect(existsSync(join(USERS_DIR, "USER.md"))).toBe(false);
359
421
  });
360
422
 
423
+ test("retired workspace/UPDATES.md (current format) is never written back", () => {
424
+ const resolver = new DefaultPathResolver(
425
+ WORKSPACE_ROOT,
426
+ undefined,
427
+ () => null,
428
+ );
429
+
430
+ const { archive } = buildVBundle({
431
+ files: [
432
+ {
433
+ path: "data/db/assistant.db",
434
+ data: new Uint8Array(),
435
+ },
436
+ {
437
+ path: "workspace/UPDATES.md",
438
+ data: new TextEncoder().encode("## Old release notes\n"),
439
+ },
440
+ ],
441
+ ...defaultV1Options(),
442
+ });
443
+
444
+ const result = commitImport({
445
+ archiveData: archive,
446
+ pathResolver: resolver,
447
+ });
448
+
449
+ expect(result.ok).toBe(true);
450
+ if (result.ok) {
451
+ expect(result.report.success).toBe(true);
452
+ expect(
453
+ result.report.files.find((f) => f.path === "workspace/UPDATES.md")
454
+ ?.action,
455
+ ).toBe("skipped");
456
+ expect(result.report.warnings.some((w) => w.includes("UPDATES.md"))).toBe(
457
+ false,
458
+ );
459
+ }
460
+
461
+ // The retired bulletin file must not be resurrected in the workspace.
462
+ expect(existsSync(join(WORKSPACE_ROOT, "UPDATES.md"))).toBe(false);
463
+ });
464
+
465
+ test("retired prompts/UPDATES.md skips silently — no warning, nothing written", () => {
466
+ const resolver = new DefaultPathResolver(
467
+ WORKSPACE_ROOT,
468
+ undefined,
469
+ () => null,
470
+ );
471
+
472
+ const { archive } = buildVBundle({
473
+ files: [
474
+ {
475
+ path: "data/db/assistant.db",
476
+ data: new Uint8Array(),
477
+ },
478
+ {
479
+ path: "prompts/UPDATES.md",
480
+ data: new TextEncoder().encode("## Old release notes\n"),
481
+ },
482
+ ],
483
+ ...defaultV1Options(),
484
+ });
485
+
486
+ const result = commitImport({
487
+ archiveData: archive,
488
+ pathResolver: resolver,
489
+ });
490
+
491
+ expect(result.ok).toBe(true);
492
+ if (result.ok) {
493
+ expect(result.report.success).toBe(true);
494
+ expect(
495
+ result.report.files.find((f) => f.path === "prompts/UPDATES.md")
496
+ ?.action,
497
+ ).toBe("skipped");
498
+ // Silent skip: the commit report must agree with preflight, which
499
+ // raises neither a conflict nor a warning for retired paths.
500
+ expect(
501
+ result.report.warnings.some((w) => w.includes("prompts/UPDATES.md")),
502
+ ).toBe(false);
503
+ }
504
+
505
+ expect(existsSync(join(WORKSPACE_ROOT, "UPDATES.md"))).toBe(false);
506
+ });
507
+
361
508
  test("does NOT overwrite a customized users/<slug>.md", () => {
362
509
  const slug = "captain.md";
363
510
  const guardianPath = join(USERS_DIR, slug);
@@ -19,6 +19,7 @@ import { join, resolve } from "node:path";
19
19
 
20
20
  import { resolveGuardianPersonaPath } from "../../prompts/persona-resolver.js";
21
21
  import { getLogger } from "../../util/logger.js";
22
+ import { isRetiredArchivePath } from "./vbundle-import-policy.js";
22
23
  import type { ManifestType } from "./vbundle-validator.js";
23
24
 
24
25
  const log = getLogger("vbundle-import-analyzer");
@@ -30,12 +31,7 @@ const log = getLogger("vbundle-import-analyzer");
30
31
  * on import, its content is translated to `users/<slug>.md` at the
31
32
  * current guardian's location (see `DefaultPathResolver.resolve`).
32
33
  */
33
- const ALLOWED_PROMPT_FILENAMES = new Set([
34
- "IDENTITY.md",
35
- "SOUL.md",
36
- "USER.md",
37
- "UPDATES.md",
38
- ]);
34
+ const ALLOWED_PROMPT_FILENAMES = new Set(["IDENTITY.md", "SOUL.md", "USER.md"]);
39
35
 
40
36
  /** Archive path for the legacy guardian user persona file. */
41
37
  const LEGACY_USER_MD_ARCHIVE_PATH = "prompts/USER.md";
@@ -123,6 +119,14 @@ export class DefaultPathResolver implements PathResolver {
123
119
  return null;
124
120
  }
125
121
 
122
+ // Retired-feature files (both legacy and workspace-format spellings)
123
+ // must never resolve to a disk target — resolving would write them
124
+ // back into the workspace on import. The analyzer and both importers
125
+ // turn the resulting null into a silent non-blocking skip.
126
+ if (isRetiredArchivePath(archivePath)) {
127
+ return null;
128
+ }
129
+
126
130
  // New format: workspace/ prefix — maps directly into the workspace dir
127
131
  if (archivePath.startsWith("workspace/") && this.workspaceDir) {
128
132
  const relPath = archivePath.slice("workspace/".length);
@@ -288,6 +292,25 @@ export function analyzeImport(
288
292
  continue;
289
293
  }
290
294
 
295
+ // Retired-feature file from a legacy bundle: nothing consumes it
296
+ // anymore, so drop it silently instead of blocking the restore with
297
+ // an UNKNOWN_ARCHIVE_PATH conflict.
298
+ if (isRetiredArchivePath(fileEntry.path)) {
299
+ log.info(
300
+ { path: fileEntry.path },
301
+ "Retired archive path in legacy bundle — will be skipped on import",
302
+ );
303
+ files.push({
304
+ path: fileEntry.path,
305
+ action: "skip",
306
+ bundle_size: fileEntry.size_bytes,
307
+ bundle_sha256: fileEntry.sha256,
308
+ current_size: null,
309
+ current_sha256: null,
310
+ });
311
+ continue;
312
+ }
313
+
291
314
  // Unknown archive path — would have nowhere to write
292
315
  conflicts.push({
293
316
  code: "UNKNOWN_ARCHIVE_PATH",
@@ -34,6 +34,29 @@ export function isLegacyPersonaArchivePath(archivePath: string): boolean {
34
34
  return archivePath === LEGACY_USER_MD_ARCHIVE_PATH;
35
35
  }
36
36
 
37
+ /**
38
+ * Archive paths for retired features. Bundles exported while those features
39
+ * existed still contain these entries; preflight and both importers must
40
+ * treat them as silent non-blocking skips (no conflict, no warning) so old
41
+ * backups stay restorable without noise, and the files are never written
42
+ * back to the workspace.
43
+ *
44
+ * `UPDATES.md` carried the retired update-bulletin release notes. Legacy
45
+ * bundles archived it as `prompts/UPDATES.md`; current-format bundles walk
46
+ * the workspace dir, so it appears as `workspace/UPDATES.md`. Both spellings
47
+ * must be retired — `DefaultPathResolver.resolve()` short-circuits them to
48
+ * `null` before any prefix handling so the workspace-format entry cannot
49
+ * resolve normally and recreate the file on import.
50
+ */
51
+ export const RETIRED_ARCHIVE_PATHS: ReadonlySet<string> = new Set([
52
+ "prompts/UPDATES.md",
53
+ "workspace/UPDATES.md",
54
+ ]);
55
+
56
+ export function isRetiredArchivePath(archivePath: string): boolean {
57
+ return RETIRED_ARCHIVE_PATHS.has(archivePath);
58
+ }
59
+
37
60
  export function isConfigArchivePath(archivePath: string): boolean {
38
61
  return CONFIG_ARCHIVE_PATHS.has(archivePath);
39
62
  }