@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
@@ -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
  }
@@ -691,6 +691,24 @@ const RAW_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
691
691
  outputPer1mTokens: 2.5,
692
692
  },
693
693
  },
694
+ {
695
+ id: "accounts/fireworks/models/minimax-m3",
696
+ displayName: "MiniMax M3",
697
+ // The model supports 1M context, but Fireworks serves it with a
698
+ // 512K (524,288-token) window; advertise the served limit.
699
+ contextWindowTokens: 524288,
700
+ maxOutputTokens: 512000,
701
+ supportsThinking: true,
702
+ supportsCaching: true,
703
+ supportsVision: true,
704
+ supportsToolUse: true,
705
+ maxEffort: "high",
706
+ pricing: {
707
+ inputPer1mTokens: 0.3,
708
+ outputPer1mTokens: 1.2,
709
+ cacheReadPer1mTokens: 0.06,
710
+ },
711
+ },
694
712
  {
695
713
  id: "accounts/fireworks/models/minimax-m2p7",
696
714
  displayName: "MiniMax M2.7",
@@ -1241,6 +1259,16 @@ const RAW_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
1241
1259
  linkLabel: "Open MiniMax Dashboard",
1242
1260
  },
1243
1261
  models: [
1262
+ {
1263
+ id: "MiniMax-M3",
1264
+ displayName: "MiniMax M3",
1265
+ contextWindowTokens: 1000000,
1266
+ maxOutputTokens: 512000,
1267
+ supportsThinking: true,
1268
+ supportsCaching: true,
1269
+ supportsVision: true,
1270
+ supportsToolUse: true,
1271
+ },
1244
1272
  {
1245
1273
  id: "MiniMax-M2.7",
1246
1274
  displayName: "MiniMax M2.7",
@@ -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: {
@@ -35,7 +35,7 @@ const PROVIDER_MODEL_INTENTS: Record<string, Record<ModelIntent, string>> = {
35
35
  "vision-optimized": "llama3.2",
36
36
  },
37
37
  fireworks: {
38
- balanced: "accounts/fireworks/models/kimi-k2p6",
38
+ balanced: "accounts/fireworks/models/minimax-m3",
39
39
  "latency-optimized": "accounts/fireworks/models/kimi-k2p5",
40
40
  "quality-optimized": "accounts/fireworks/models/kimi-k2p6",
41
41
  "vision-optimized": "accounts/fireworks/models/kimi-k2p6",
@@ -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";
@@ -17,6 +18,7 @@ import {
17
18
  ContextOverflowError,
18
19
  extractOverflowTokensFromMessage,
19
20
  } from "../types.js";
21
+ import { wrapUnparseableToolArgs } from "../unparseable-tool-args.js";
20
22
 
21
23
  /**
22
24
  * Detect OpenAI-compatible context-overflow signals on an `OpenAI.APIError`.
@@ -634,7 +636,7 @@ export class OpenAIChatCompletionsProvider implements Provider {
634
636
  try {
635
637
  input = JSON.parse(tc.args);
636
638
  } catch {
637
- input = { _raw: tc.args };
639
+ input = wrapUnparseableToolArgs(tc.args);
638
640
  }
639
641
  content.push({
640
642
  type: "tool_use",
@@ -797,7 +799,7 @@ export class OpenAIChatCompletionsProvider implements Provider {
797
799
  if (systemPrompt) {
798
800
  result.push({
799
801
  role: "system",
800
- content: systemPrompt,
802
+ content: systemPrompt.replaceAll(SYSTEM_PROMPT_CACHE_BOUNDARY, "\n\n"),
801
803
  });
802
804
  }
803
805
 
@@ -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";
@@ -14,6 +15,7 @@ import type {
14
15
  SendMessageOptions,
15
16
  } from "../types.js";
16
17
  import { ContextOverflowError } from "../types.js";
18
+ import { wrapUnparseableToolArgs } from "../unparseable-tool-args.js";
17
19
  import { detectOpenAICompatibleContextOverflow } from "./chat-completions-provider.js";
18
20
 
19
21
  const log = getLogger("openai-responses");
@@ -216,7 +218,10 @@ export class OpenAIResponsesProvider implements Provider {
216
218
  };
217
219
 
218
220
  if (systemPrompt) {
219
- params.instructions = systemPrompt;
221
+ params.instructions = systemPrompt.replaceAll(
222
+ SYSTEM_PROMPT_CACHE_BOUNDARY,
223
+ "\n\n",
224
+ );
220
225
  }
221
226
 
222
227
  if (maxTokens && !this.codexSubscription) {
@@ -461,7 +466,7 @@ export class OpenAIResponsesProvider implements Provider {
461
466
  try {
462
467
  input = JSON.parse(tc.args);
463
468
  } catch {
464
- input = { _raw: tc.args };
469
+ input = wrapUnparseableToolArgs(tc.args);
465
470
  }
466
471
  content.push({
467
472
  type: "tool_use",
@@ -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
 
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Marker shape for tool-call arguments that failed JSON parsing.
3
+ *
4
+ * Streaming providers accumulate tool-call argument deltas as a string and
5
+ * parse the result once the stream completes. Some models/gateways emit
6
+ * truncated or malformed argument JSON (e.g. MiniMax M3 via OpenRouter cutting
7
+ * the stream mid-object). The provider cannot drop the tool_use block — the
8
+ * protocol requires a paired tool_result for every tool_use id — so it wraps
9
+ * the raw string under this marker key instead.
10
+ *
11
+ * The tool execution layer detects the marker and rejects the invocation with
12
+ * an error tool result, so the model sees the failure and can retry, instead
13
+ * of a tool executing with garbage input.
14
+ */
15
+ const UNPARSEABLE_TOOL_ARGS_KEY = "_raw";
16
+
17
+ /** Wrap raw, unparseable tool-call argument text in the marker shape. */
18
+ export function wrapUnparseableToolArgs(raw: string): Record<string, unknown> {
19
+ return { [UNPARSEABLE_TOOL_ARGS_KEY]: raw };
20
+ }
21
+
22
+ /**
23
+ * Detect input produced by {@link wrapUnparseableToolArgs}: exactly one key,
24
+ * the marker key, holding the raw argument string. The exact-shape check
25
+ * avoids false positives on hypothetical legitimate inputs that merely
26
+ * contain a `_raw` field among others.
27
+ */
28
+ export function isUnparseableToolArgs(
29
+ input: Record<string, unknown>,
30
+ ): input is { _raw: string } {
31
+ const keys = Object.keys(input);
32
+ return (
33
+ keys.length === 1 &&
34
+ keys[0] === UNPARSEABLE_TOOL_ARGS_KEY &&
35
+ typeof input[UNPARSEABLE_TOOL_ARGS_KEY] === "string"
36
+ );
37
+ }
38
+
39
+ /**
40
+ * Build the error message returned to the model when it sends unparseable
41
+ * tool arguments. Includes a bounded prefix of what was received so the model
42
+ * can see where its output was cut off or malformed.
43
+ */
44
+ export function unparseableToolArgsMessage(
45
+ toolName: string,
46
+ raw: string,
47
+ ): string {
48
+ const PREVIEW_LIMIT = 200;
49
+ const preview =
50
+ raw.length > PREVIEW_LIMIT ? `${raw.slice(0, PREVIEW_LIMIT)}…` : raw;
51
+ return (
52
+ `Error: the arguments for "${toolName}" were not valid JSON — the argument stream was malformed or truncated, so the tool was NOT executed. ` +
53
+ `Received: ${preview || "(empty)"}\n` +
54
+ `Retry the call with complete, valid JSON arguments.`
55
+ );
56
+ }
@@ -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