@retaw106/mmc-oc 1.2.2 → 2.0.0

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 (227) hide show
  1. package/dist/cli.js +7095 -0
  2. package/dist/config/index.d.ts +0 -12
  3. package/dist/config/index.d.ts.map +1 -1
  4. package/dist/config/legacy-harness-blocks.d.ts +27 -0
  5. package/dist/config/legacy-harness-blocks.d.ts.map +1 -0
  6. package/dist/config/live-run-config.d.ts.map +1 -1
  7. package/dist/config/live-snapshot.d.ts +2 -1
  8. package/dist/config/live-snapshot.d.ts.map +1 -1
  9. package/dist/config/project-security.d.ts +4 -5
  10. package/dist/config/project-security.d.ts.map +1 -1
  11. package/dist/config/raw-loader.d.ts +7 -26
  12. package/dist/config/raw-loader.d.ts.map +1 -1
  13. package/dist/config/schema/magic-context.d.ts +164 -259
  14. package/dist/config/schema/magic-context.d.ts.map +1 -1
  15. package/dist/config/variable.d.ts +8 -0
  16. package/dist/config/variable.d.ts.map +1 -1
  17. package/dist/config-init.js +5595 -0
  18. package/dist/{context-a6px04s0.js → context-95zkksn3.js} +2471 -2438
  19. package/dist/{derive-budgets-fhk00zv8.js → derive-budgets-rjjz5f7y.js} +2 -2
  20. package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
  21. package/dist/features/magic-context/message-index.d.ts +2 -0
  22. package/dist/features/magic-context/message-index.d.ts.map +1 -1
  23. package/dist/features/magic-context/migration-v3-perf-schema.d.ts +8 -0
  24. package/dist/features/magic-context/migration-v3-perf-schema.d.ts.map +1 -0
  25. package/dist/features/magic-context/migration-v4-temporal-decisions.d.ts +12 -0
  26. package/dist/features/magic-context/migration-v4-temporal-decisions.d.ts.map +1 -0
  27. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  28. package/dist/features/magic-context/overflow-detection.d.ts +6 -0
  29. package/dist/features/magic-context/overflow-detection.d.ts.map +1 -1
  30. package/dist/features/magic-context/reclaim-protection.d.ts +15 -10
  31. package/dist/features/magic-context/reclaim-protection.d.ts.map +1 -1
  32. package/dist/features/magic-context/session-cache-ttl.d.ts +4 -2
  33. package/dist/features/magic-context/session-cache-ttl.d.ts.map +1 -1
  34. package/dist/features/magic-context/storage-db.d.ts +1 -1
  35. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  36. package/dist/features/magic-context/storage-historian-runs.d.ts +2 -0
  37. package/dist/features/magic-context/storage-historian-runs.d.ts.map +1 -1
  38. package/dist/features/magic-context/storage-ops.d.ts +2 -0
  39. package/dist/features/magic-context/storage-ops.d.ts.map +1 -1
  40. package/dist/features/magic-context/storage-session-tables.d.ts.map +1 -1
  41. package/dist/features/magic-context/storage-tags.d.ts +15 -2
  42. package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
  43. package/dist/features/magic-context/storage.d.ts +2 -2
  44. package/dist/features/magic-context/storage.d.ts.map +1 -1
  45. package/dist/features/magic-context/temporal-decisions.d.ts +10 -0
  46. package/dist/features/magic-context/temporal-decisions.d.ts.map +1 -0
  47. package/dist/features/magic-context/tool-definition-tokens.d.ts +3 -0
  48. package/dist/features/magic-context/tool-definition-tokens.d.ts.map +1 -1
  49. package/dist/hooks/magic-context/active-anthropic-turn.d.ts +8 -0
  50. package/dist/hooks/magic-context/active-anthropic-turn.d.ts.map +1 -0
  51. package/dist/hooks/magic-context/apply-operations.d.ts +15 -0
  52. package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
  53. package/dist/hooks/magic-context/auto-search-deadline.d.ts +10 -3
  54. package/dist/hooks/magic-context/auto-search-deadline.d.ts.map +1 -1
  55. package/dist/hooks/magic-context/auto-search-runner.d.ts +1 -1
  56. package/dist/hooks/magic-context/auto-search-runner.d.ts.map +1 -1
  57. package/dist/hooks/magic-context/caveman-cleanup.d.ts.map +1 -1
  58. package/dist/hooks/magic-context/channel2-cycle.d.ts +2 -0
  59. package/dist/hooks/magic-context/channel2-cycle.d.ts.map +1 -1
  60. package/dist/hooks/magic-context/compaction-marker-manager.d.ts.map +1 -1
  61. package/dist/hooks/magic-context/compartment-render-epoch.d.ts +8 -0
  62. package/dist/hooks/magic-context/compartment-render-epoch.d.ts.map +1 -1
  63. package/dist/hooks/magic-context/compartment-runner-partial-recomp.d.ts.map +1 -1
  64. package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
  65. package/dist/hooks/magic-context/compartment-trigger.d.ts +3 -1
  66. package/dist/hooks/magic-context/compartment-trigger.d.ts.map +1 -1
  67. package/dist/hooks/magic-context/ctx-reduce-availability.d.ts +12 -0
  68. package/dist/hooks/magic-context/ctx-reduce-availability.d.ts.map +1 -1
  69. package/dist/hooks/magic-context/decision-calibration.d.ts +3 -0
  70. package/dist/hooks/magic-context/decision-calibration.d.ts.map +1 -1
  71. package/dist/hooks/magic-context/emergency-drop.d.ts +2 -0
  72. package/dist/hooks/magic-context/emergency-drop.d.ts.map +1 -1
  73. package/dist/hooks/magic-context/emergency-fail-closed.d.ts +21 -1
  74. package/dist/hooks/magic-context/emergency-fail-closed.d.ts.map +1 -1
  75. package/dist/hooks/magic-context/final-wire-token-estimate.d.ts +30 -1
  76. package/dist/hooks/magic-context/final-wire-token-estimate.d.ts.map +1 -1
  77. package/dist/hooks/magic-context/heuristic-cleanup.d.ts +2 -0
  78. package/dist/hooks/magic-context/heuristic-cleanup.d.ts.map +1 -1
  79. package/dist/hooks/magic-context/inject-compartments.d.ts +10 -8
  80. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  81. package/dist/hooks/magic-context/latest-assistant-turn.d.ts +25 -0
  82. package/dist/hooks/magic-context/latest-assistant-turn.d.ts.map +1 -0
  83. package/dist/hooks/magic-context/lkg-measured-request.d.ts +69 -0
  84. package/dist/hooks/magic-context/lkg-measured-request.d.ts.map +1 -0
  85. package/dist/hooks/magic-context/read-session-formatting.d.ts +3 -0
  86. package/dist/hooks/magic-context/read-session-formatting.d.ts.map +1 -1
  87. package/dist/hooks/magic-context/reasoning-budget-status.d.ts +15 -0
  88. package/dist/hooks/magic-context/reasoning-budget-status.d.ts.map +1 -0
  89. package/dist/hooks/magic-context/reasoning-budget.d.ts +40 -0
  90. package/dist/hooks/magic-context/reasoning-budget.d.ts.map +1 -0
  91. package/dist/hooks/magic-context/reasoning-removal.d.ts +6 -6
  92. package/dist/hooks/magic-context/reasoning-removal.d.ts.map +1 -1
  93. package/dist/hooks/magic-context/recomp-orchestrator.d.ts +2 -0
  94. package/dist/hooks/magic-context/recomp-orchestrator.d.ts.map +1 -1
  95. package/dist/hooks/magic-context/sentinel.d.ts +2 -0
  96. package/dist/hooks/magic-context/sentinel.d.ts.map +1 -1
  97. package/dist/hooks/magic-context/strip-content.d.ts +7 -3
  98. package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
  99. package/dist/hooks/magic-context/supersession-reclaim.d.ts +4 -0
  100. package/dist/hooks/magic-context/supersession-reclaim.d.ts.map +1 -1
  101. package/dist/hooks/magic-context/tag-messages.d.ts +13 -0
  102. package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
  103. package/dist/hooks/magic-context/tail-hygiene-walk.d.ts +5 -0
  104. package/dist/hooks/magic-context/tail-hygiene-walk.d.ts.map +1 -1
  105. package/dist/hooks/magic-context/temporal-awareness.d.ts +6 -3
  106. package/dist/hooks/magic-context/temporal-awareness.d.ts.map +1 -1
  107. package/dist/hooks/magic-context/temporal-served-projection.d.ts +4 -0
  108. package/dist/hooks/magic-context/temporal-served-projection.d.ts.map +1 -0
  109. package/dist/hooks/magic-context/tool-reclaim.d.ts +2 -0
  110. package/dist/hooks/magic-context/tool-reclaim.d.ts.map +1 -1
  111. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +34 -12
  112. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  113. package/dist/hooks/magic-context/transform.d.ts +11 -5
  114. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  115. package/dist/hooks/magic-context/wrapup-orchestrator.d.ts.map +1 -1
  116. package/dist/{index-wgt4gamw.js → index-1mjhzqq7.js} +422 -31
  117. package/dist/index-33gprt9g.js +30 -0
  118. package/dist/{index-zx63j2e2.js → index-5cep2r9n.js} +3 -2
  119. package/dist/{index-by6h2pav.js → index-bdt9my9y.js} +374 -239
  120. package/dist/{index-3wagyrnj.js → index-x37vg474.js} +5 -1
  121. package/dist/{index-1vs6anwz.js → index-xt7y10x6.js} +2 -1
  122. package/dist/{index-me3zcj5t.js → index-z2g20msd.js} +4 -8
  123. package/dist/index.js +2 -2
  124. package/dist/{message-time-backfill-5ewk0axe.js → message-time-backfill-zfernxbw.js} +1 -1
  125. package/dist/{model-resolution-5haaxnpt.js → model-resolution-wazv26n0.js} +1 -1
  126. package/dist/plugin/rpc-handlers.d.ts +4 -0
  127. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  128. package/dist/{read-session-chunk-apjwrttc.js → read-session-chunk-hp464g13.js} +2 -2
  129. package/dist/{recomp-orchestrator-gyeevxaw.js → recomp-orchestrator-3qxeqwb9.js} +6 -6
  130. package/dist/shared/auto-search-lifecycle.d.ts +7 -0
  131. package/dist/shared/auto-search-lifecycle.d.ts.map +1 -0
  132. package/dist/shared/cache-ttl-display.d.ts +6 -4
  133. package/dist/shared/cache-ttl-display.d.ts.map +1 -1
  134. package/dist/shared/model-cache-ttl.d.ts +1 -1
  135. package/dist/shared/model-cache-ttl.d.ts.map +1 -1
  136. package/dist/shared/model-resolution.d.ts +5 -2
  137. package/dist/shared/model-resolution.d.ts.map +1 -1
  138. package/dist/shared/model-suggestion-retry.d.ts +2 -0
  139. package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
  140. package/dist/shared/protected-tools-policy.d.ts +6 -0
  141. package/dist/shared/protected-tools-policy.d.ts.map +1 -0
  142. package/dist/shared/provider-output-failure.d.ts +19 -0
  143. package/dist/shared/provider-output-failure.d.ts.map +1 -0
  144. package/dist/shared/temporal-upgrade-fixture.d.ts +19 -0
  145. package/dist/shared/temporal-upgrade-fixture.d.ts.map +1 -0
  146. package/dist/shared/window-geometry.d.ts +4 -0
  147. package/dist/shared/window-geometry.d.ts.map +1 -1
  148. package/dist/shared/write-transaction-timing.d.ts +8 -1
  149. package/dist/shared/write-transaction-timing.d.ts.map +1 -1
  150. package/dist/tools/ctx-reduce/tools.d.ts +2 -0
  151. package/dist/tools/ctx-reduce/tools.d.ts.map +1 -1
  152. package/dist/v2/hidden-child-native.d.ts +7 -0
  153. package/dist/v2/hidden-child-native.d.ts.map +1 -1
  154. package/dist/v2/hidden-child-record.d.ts +13 -0
  155. package/dist/v2/hidden-child-record.d.ts.map +1 -1
  156. package/dist/v2/hidden-completion.d.ts.map +1 -1
  157. package/dist/v2/hooks/context.d.ts.map +1 -1
  158. package/dist/v2/hooks/hidden-child.d.ts +46 -2
  159. package/dist/v2/hooks/hidden-child.d.ts.map +1 -1
  160. package/dist/v2/hooks/payload.d.ts.map +1 -1
  161. package/dist/v2/hooks/store.d.ts.map +1 -1
  162. package/dist/v2/hooks/tools.d.ts.map +1 -1
  163. package/dist/v2/hooks/types.d.ts +8 -0
  164. package/dist/v2/hooks/types.d.ts.map +1 -1
  165. package/dist/v2/server.js +4540 -3889
  166. package/dist/v2/tool-result.d.ts +8 -0
  167. package/dist/v2/tool-result.d.ts.map +1 -0
  168. package/dist/web/api.d.ts +6 -0
  169. package/dist/web/api.d.ts.map +1 -1
  170. package/dist/web/context-store.d.ts +13 -0
  171. package/dist/web/context-store.d.ts.map +1 -1
  172. package/dist/web/display-path.d.ts +28 -0
  173. package/dist/web/display-path.d.ts.map +1 -0
  174. package/dist/web/entry-args.d.ts +33 -0
  175. package/dist/web/entry-args.d.ts.map +1 -0
  176. package/dist/web/host-pi.d.ts +2 -0
  177. package/dist/web/host-pi.d.ts.map +1 -1
  178. package/dist/web/main.d.ts +4 -6
  179. package/dist/web/main.d.ts.map +1 -1
  180. package/dist/web/main.js +772 -77
  181. package/dist/web/message-stats.d.ts +45 -0
  182. package/dist/web/message-stats.d.ts.map +1 -0
  183. package/dist/web/ui/index.html +1 -0
  184. package/dist/web/ui/panel.css +5 -0
  185. package/dist/web/ui/panel.js +220 -70
  186. package/dist/web/views.d.ts +23 -0
  187. package/dist/web/views.d.ts.map +1 -1
  188. package/dist/{wrapup-orchestrator-z9sqpqjf.js → wrapup-orchestrator-wgb2nd19.js} +7 -6
  189. package/package.json +7 -2
  190. package/src/config/index.test.ts +257 -126
  191. package/src/config/index.ts +45 -39
  192. package/src/config/legacy-harness-blocks.test.ts +78 -0
  193. package/src/config/legacy-harness-blocks.ts +71 -0
  194. package/src/config/live-run-config.test.ts +49 -5
  195. package/src/config/live-run-config.ts +1 -0
  196. package/src/config/live-snapshot.ts +5 -1
  197. package/src/config/profiles.ts +1 -1
  198. package/src/config/project-config-trust-boundary.test.ts +2 -2
  199. package/src/config/project-security.hostile-matrix.test.ts +51 -65
  200. package/src/config/project-security.test.ts +6 -1
  201. package/src/config/project-security.ts +13 -12
  202. package/src/config/protected-tokens.test.ts +1 -1
  203. package/src/config/raw-loader.test.ts +29 -352
  204. package/src/config/raw-loader.ts +17 -406
  205. package/src/config/removed-config-keys.test.ts +1 -1
  206. package/src/config/schema/magic-context.test.ts +60 -64
  207. package/src/config/schema/magic-context.ts +105 -137
  208. package/src/config/variable.ts +21 -12
  209. package/src/shared/auto-search-lifecycle.ts +27 -0
  210. package/src/shared/cache-ttl-display.test.ts +1 -1
  211. package/src/shared/cache-ttl-display.ts +17 -16
  212. package/src/shared/model-cache-ttl.test.ts +2 -2
  213. package/src/shared/model-cache-ttl.ts +8 -4
  214. package/src/shared/model-resolution.test.ts +22 -32
  215. package/src/shared/model-resolution.ts +13 -18
  216. package/src/shared/model-suggestion-retry.ts +63 -7
  217. package/src/shared/protected-tools-policy.ts +25 -0
  218. package/src/shared/provider-output-failure.ts +176 -0
  219. package/src/shared/status-detail-text.test.ts +1 -1
  220. package/src/shared/status-view.test.ts +1 -1
  221. package/src/shared/temporal-upgrade-fixture.ts +50 -0
  222. package/src/shared/window-geometry.ts +10 -0
  223. package/src/shared/write-transaction-timing.ts +25 -1
  224. package/dist/features/magic-context/window-report-ledger.d.ts +0 -70
  225. package/dist/features/magic-context/window-report-ledger.d.ts.map +0 -1
  226. package/dist/index-e1q8002h.js +0 -16
  227. package/src/config/misplaced-agent-model.test.ts +0 -55
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import { isValidLanguageCode } from "../../agents/language-directive";
3
3
  import { toolTemplateError } from "../../shared/historian-tool-template";
4
4
  import { isValidPromptSurfaceModelKey } from "../../shared/prompt-surface";
5
+ import { DEFAULT_PROTECTED_TOOLS, mergeProtectedTools } from "../../shared/protected-tools-policy";
5
6
  import { AgentOverrideConfigSchema } from "./agent-overrides";
6
7
 
7
8
  export const DEFAULT_EXECUTE_THRESHOLD_PERCENTAGE = 65;
@@ -96,131 +97,66 @@ export const PromptSurfaceConfigSchema = z
96
97
  export type PromptSurfaceConfig = z.infer<typeof PromptSurfaceConfigSchema>;
97
98
 
98
99
  /**
99
- * The flat model-resolution fields are the only fields that move into harness
100
- * blocks. Keeping this inventory next to the schema gives the loader migration
101
- * an exhaustive contract instead of a catch-all rule.
100
+ * The flat historian model-resolution fields. This is the exhaustive contract
101
+ * for what a project-tier (repo-supplied) config must never set: the model, its
102
+ * fallbacks, and the per-harness qualifiers (OpenCode `variant`, Pi
103
+ * `thinking_level`) all merge onto the user's historian model at resolve time,
104
+ * so a cloned repo could otherwise force extra variant/thinking spend.
102
105
  */
103
- export const PER_HARNESS_MIGRATION_INVENTORY = {
104
- historian: {
105
- retained: [
106
- "temperature",
107
- "top_p",
108
- "prompt",
109
- "tools",
110
- "disable",
111
- "description",
112
- "mode",
113
- "color",
114
- "maxSteps",
115
- "permission",
116
- "maxTokens",
117
- "two_pass",
118
- "disallowed_tools",
119
- ],
120
- migrated_execution: ["model", "fallback_models", "variant", "thinking_level"],
121
- },
122
- } as const;
106
+ export const HISTORIAN_MODEL_RESOLUTION_FIELDS = [
107
+ "model",
108
+ "fallback_models",
109
+ "variant",
110
+ "thinking_level",
111
+ ] as const;
123
112
 
124
- /** Harness keys that own independent hidden-agent model-selection blocks. */
125
- export const PER_HARNESS_MODEL_KEYS = ["opencode", "pi"] as const;
113
+ /**
114
+ * The removed per-harness historian model blocks. They are no longer part of
115
+ * the schema; this list exists only so the loaders can detect them and REJECT
116
+ * the config file loudly (a parse failure → defaults / last-known-good) instead
117
+ * of letting Zod silently strip them (which would leave the historian with no
118
+ * model and look "configured but never running").
119
+ */
120
+ export const LEGACY_HARNESS_BLOCK_KEYS = ["opencode", "pi"] as const;
126
121
 
127
- /** OpenCode entry objects permit `variant` and reject Pi-only `thinking_level`. */
128
- const OcEntryObjectSchema = z
122
+ /**
123
+ * A historian model entry: either a bare model ID string, or an object that may
124
+ * carry the OpenCode `variant` and/or the Pi `thinking_level` qualifier. The
125
+ * active harness reads only its own qualifier and ignores the other; there is
126
+ * no cross-harness mapping.
127
+ */
128
+ const HistorianModelEntryObjectSchema = z
129
129
  .object({
130
- model: z.string().describe("OpenCode model ID (for example, provider/model)."),
130
+ model: z.string().describe("Model ID (for example, provider/model)."),
131
131
  variant: z.string().optional().describe("OpenCode reasoning variant for this entry."),
132
- })
133
- .strict();
134
- export const OcEntrySchema = z.union([z.string(), OcEntryObjectSchema]);
135
- export type OcEntry = z.infer<typeof OcEntrySchema>;
136
-
137
- /** Pi entry objects permit `thinking_level` and reject OpenCode-only `variant`. */
138
- const PiEntryObjectSchema = z
139
- .object({
140
- model: z.string().describe("Pi model ID (for example, provider/model)."),
141
132
  thinking_level: PiThinkingLevelSchema.describe("Pi thinking level for this entry."),
142
133
  })
143
134
  .strict();
144
- export const PiEntrySchema = z.union([z.string(), PiEntryObjectSchema]);
145
- export type PiEntry = z.infer<typeof PiEntrySchema>;
146
-
147
- /** Strict model-resolution block used by historian.opencode. */
148
- export const OpenCodeHarnessBlockSchema = z
149
- .object({
150
- model: OcEntrySchema.optional().describe("Primary OpenCode model entry."),
151
- fallback_models: z
152
- .array(OcEntrySchema)
153
- .optional()
154
- .describe(
155
- "Ordered fallback OpenCode entries. New-shape configuration requires an array; legacy singleton values migrate to a one-element array.",
156
- ),
157
- variant: z
158
- .string()
159
- .optional()
160
- .describe(
161
- "OpenCode reasoning variant for the primary entry when it declares none. Fallback entries declare variants per-entry.",
162
- ),
163
- })
164
- .strict()
165
- .describe("Strict OpenCode model-resolution block. It accepts no Pi vocabulary.");
166
- export type OpenCodeHarnessBlock = z.infer<typeof OpenCodeHarnessBlockSchema>;
167
-
168
- /** Strict model-resolution block used by historian.pi. */
169
- export const PiHarnessBlockSchema = z
170
- .object({
171
- model: PiEntrySchema.optional().describe("Primary Pi model entry."),
172
- fallback_models: z
173
- .array(PiEntrySchema)
174
- .optional()
175
- .describe(
176
- "Ordered fallback Pi entries. New-shape configuration requires an array; legacy singleton values migrate to a one-element array.",
177
- ),
178
- thinking_level: PiThinkingLevelSchema.describe(
179
- "Pi thinking level for the primary entry when it declares none. Fallback entries declare thinking levels per-entry.",
180
- ),
181
- })
182
- .strict()
183
- .describe("Strict Pi model-resolution block. It accepts no OpenCode vocabulary.");
184
- export type PiHarnessBlock = z.infer<typeof PiHarnessBlockSchema>;
135
+ export const HistorianModelEntrySchema = z.union([z.string(), HistorianModelEntryObjectSchema]);
136
+ export type HistorianModelEntry = z.infer<typeof HistorianModelEntrySchema>;
185
137
 
186
138
  /**
187
- * A profile may select models but must not alter execution policy. Keep this
188
- * separate from the full harness schemas, which also admit execution-policy
189
- * fields beyond model selection.
139
+ * A profile may select models but must not alter execution policy. It uses the
140
+ * same flat historian model-resolution fields; execution metadata is not
141
+ * allowed here.
190
142
  */
191
- const ProfileOpenCodeModelBlockSchema = z
143
+ const ProfileHistorianSchema = z
192
144
  .object({
193
- model: OcEntrySchema.optional().describe("Primary OpenCode model entry."),
145
+ model: HistorianModelEntrySchema.optional().describe("Primary historian model entry."),
194
146
  fallback_models: z
195
- .array(OcEntrySchema)
147
+ .array(HistorianModelEntrySchema)
196
148
  .optional()
197
- .describe("Ordered fallback OpenCode model entries."),
149
+ .describe("Ordered fallback historian model entries."),
198
150
  variant: z
199
151
  .string()
200
152
  .optional()
201
- .describe("OpenCode reasoning variant for the primary model entry."),
202
- })
203
- .strict()
204
- .describe("Strict profile-only OpenCode model-selection block.");
205
- const ProfilePiModelBlockSchema = z
206
- .object({
207
- model: PiEntrySchema.optional().describe("Primary Pi model entry."),
208
- fallback_models: z
209
- .array(PiEntrySchema)
210
- .optional()
211
- .describe("Ordered fallback Pi model entries."),
153
+ .describe("OpenCode reasoning variant for the primary entry. Ignored by Pi."),
212
154
  thinking_level: PiThinkingLevelSchema.describe(
213
- "Pi thinking level for the primary model entry.",
155
+ "Pi thinking level for the primary entry. Ignored by OpenCode.",
214
156
  ),
215
157
  })
216
158
  .strict()
217
- .describe("Strict profile-only Pi model-selection block.");
218
- const ProfileHistorianSchema = z
219
- .object({
220
- opencode: ProfileOpenCodeModelBlockSchema.optional(),
221
- pi: ProfilePiModelBlockSchema.optional(),
222
- })
223
- .strict();
159
+ .describe("Strict profile-only historian model-selection block.");
224
160
 
225
161
  export const ConfigProfileSchema = z
226
162
  .object({
@@ -228,7 +164,7 @@ export const ConfigProfileSchema = z
228
164
  })
229
165
  .strict()
230
166
  .describe(
231
- "User-owned model-selection overlay. Only historian harness model blocks are allowed.",
167
+ "User-owned model-selection overlay. Only the flat historian model-resolution fields are allowed.",
232
168
  );
233
169
  export type ConfigProfile = z.infer<typeof ConfigProfileSchema>;
234
170
 
@@ -253,8 +189,10 @@ const AgentMetadataSchema = AgentOverrideConfigSchema.pick({
253
189
  });
254
190
 
255
191
  /**
256
- * Historian metadata remains harness-independent. Only model resolution moves to
257
- * the strict opencode and pi blocks; two_pass and disallowed_tools stay here.
192
+ * Historian metadata stays harness-independent. Model resolution uses the flat
193
+ * `model` / `fallback_models` fields plus the harness's own qualifier
194
+ * (`variant` for OpenCode, `thinking_level` for Pi); two_pass and
195
+ * disallowed_tools stay here.
258
196
  */
259
197
  export const HistorianConfigSchema = AgentMetadataSchema.extend({
260
198
  expand_tools: z
@@ -272,8 +210,22 @@ export const HistorianConfigSchema = AgentMetadataSchema.extend({
272
210
  .describe(
273
211
  'Readable historian tool expansions, keyed by exact host tool name. Templates override built-in defaults; false disables an expansion. Supports ${input.path}, ${output.path}, bare ${output}, array [N], [*].field, .each("${field}"), .join("separator"), .count and final .truncate(N). Missing fields are empty; placeholders default to 300 characters, lists to 10 elements, expansions to 1000 characters. Valid in user and project config; affects historian/recomp and verbose ctx_expand only, never the wire or default ctx_expand transcript.',
274
212
  ),
275
- opencode: OpenCodeHarnessBlockSchema.optional(),
276
- pi: PiHarnessBlockSchema.optional(),
213
+ model: HistorianModelEntrySchema.optional().describe(
214
+ "Primary historian model entry. The active harness reads its own qualifier (OpenCode variant, Pi thinking_level); the other harness's qualifier is ignored.",
215
+ ),
216
+ fallback_models: z
217
+ .array(HistorianModelEntrySchema)
218
+ .optional()
219
+ .describe("Ordered fallback historian model entries, tried in order."),
220
+ variant: z
221
+ .string()
222
+ .optional()
223
+ .describe(
224
+ "OpenCode reasoning variant for the primary entry when it declares none. Ignored by Pi.",
225
+ ),
226
+ thinking_level: PiThinkingLevelSchema.describe(
227
+ "Pi thinking level for the primary entry when it declares none. Ignored by OpenCode.",
228
+ ),
277
229
  two_pass: z
278
230
  .boolean()
279
231
  .default(false)
@@ -286,10 +238,14 @@ export const HistorianConfigSchema = AgentMetadataSchema.extend({
286
238
  .describe(
287
239
  'OpenCode only. Tools to REMOVE from the historian\'s default allow-list [read, aft_outline, aft_zoom, aft_search]. Applies to both historian and historian-editor agents. Use ["*"] to strip all tool definitions from the model request — this prevents weak instruction-following models (e.g. mistral-small-latest) from entering tool-calling loops. Individual tool names remove just that tool. Note: a user-supplied historian.permission override can re-allow a tool that disallowed_tools removed — disallowed_tools sets the baseline, permission overrides take precedence. (default: [])',
288
240
  ),
289
- }).optional();
241
+ })
242
+ .strict()
243
+ .optional();
290
244
  export type HistorianConfig = NonNullable<z.infer<typeof HistorianConfigSchema>>;
291
245
 
292
246
  export interface MagicContextConfig {
247
+ /** The loader sets this to distinguish an explicit `5m` from the built-in default; users do not configure it. */
248
+ cacheTtlConfigured?: boolean;
293
249
  enabled: boolean;
294
250
  /** User-level setting that lets a session in the canonical home directory use project memory. */
295
251
  allow_home_project: boolean;
@@ -319,7 +275,10 @@ export interface MagicContextConfig {
319
275
  * matching provider input limits. Values above 90% × context_limit are clamped with a warning. */
320
276
  execute_threshold_tokens?: { default?: number; [modelKey: string]: number | undefined };
321
277
  protected_tokens?: number;
322
- clear_reasoning_age: number;
278
+ /** Deprecated and ignored. Use keep_reasoning_tokens instead. */
279
+ clear_reasoning_age?: unknown;
280
+ /** Reasoning tokens to keep on rebuilding passes; per-model map allowed. */
281
+ keep_reasoning_tokens?: number | Record<string, number>;
323
282
  history_budget_percentage: number;
324
283
  historian_timeout_ms: number;
325
284
  commit_cluster_trigger: {
@@ -394,15 +353,10 @@ export interface MagicContextConfig {
394
353
  };
395
354
  /** Pi-only child-process extension controls. */
396
355
  pi?: PiConfig;
397
- /** Content-aware reclaim of tool output that a later call supersedes, added
398
- * to the normal age-based auto-drop: superseded todowrite/mmc_reduce/meta
399
- * outputs are dropped, and older edits to a file are compressed to a marker
400
- * that keeps only the filePath. Only runs on a transform pass that is
401
- * already rewriting the messages, so it never triggers a prompt-cache miss
402
- * on its own; when off, the messages sent to the model are byte-identical to
403
- * the age-based-only behavior. Experimental, opt-in, default off until cache
404
- * stability is proven. */
405
- smart_drops: boolean;
356
+ /** Deprecated and ignored: supersession reclaim is always on. */
357
+ smart_drops?: unknown;
358
+ /** Keep each tool's newest N still-active results in every drop lane. */
359
+ protected_tools: Record<string, number>;
406
360
  /**
407
361
  * Age-tier caveman compression for long user/assistant text parts.
408
362
  * Graduated from `experimental.caveman_text_compression`; opt-in, default off.
@@ -517,16 +471,16 @@ export const MagicContextConfigSchema = z.object({
517
471
  "Select a named user-owned model profile. A valid project name overrides this user default; an empty string, null, or other non-string project value is ignored with a warning so the user selection still applies. Unknown names warn and use the base configuration.",
518
472
  ),
519
473
  profiles: ConfigProfilesSchema.optional().describe(
520
- "User-level named model profiles. A profile may contain only historian model, fallback_models, OpenCode variant, and Pi thinking_level fields. Project configs may select a name but cannot define profiles.",
474
+ "User-level named model profiles. A profile may contain only the flat historian model-resolution fields (model, fallback_models, OpenCode variant, Pi thinking_level). Project configs may select a name but cannot define profiles.",
521
475
  ),
522
476
  historian: HistorianConfigSchema.describe(
523
- "Historian metadata plus independent strict OpenCode and Pi execution blocks. Retained metadata stays at historian; model, fallback_models, variant, and thinking_level belong only in historian.opencode or historian.pi.",
477
+ "Historian metadata plus the flat model-resolution fields (model, fallback_models, variant, thinking_level). The active harness reads its own qualifier; the removed historian.opencode / historian.pi blocks are no longer accepted.",
524
478
  ),
525
479
  cache_ttl: z
526
480
  .union([z.string(), z.object({ default: z.string() }).catchall(z.string())])
527
481
  .default("5m")
528
482
  .describe(
529
- 'How long Magic Context assumes the provider\'s cached prefix stays valid. This is MC\'s own deferral gate — it does not change the provider\'s actual cache lifetime. String (e.g. "5m", "1h", "30s") or per-model object ({ default: "5m", "provider/model": "1h", "provider/*": "never" }); keys resolve most-specific first (exact provider/model, bare model ID, shorter dash-prefixes, then the provider/* wildcard). Explicit per-model entries win; otherwise GPT-5.6 and later (including gpt-6*, through any provider prefix) use a built-in 30m lifetime before the object default or 5m fallback. An unset or global "5m" opts into built-in defaults; any other global string is an explicit policy and wins. Policy is frozen per session, including across restarts; a model switch resolves against that frozen policy. /mmc-status shows the effective value and source. OpenAI documents at least 30 minutes since the latest write or reuse: https://developers.openai.com/api/docs/guides/prompt-caching (Cache lifetime and Summary of model differences). Set to "never" to mean MC never assumes expiry (for lanes kept warm externally by a cache-keep tool) — disables the idle-TTL heuristic so MC never initiates a rebuild based on elapsed time. Provider-side extended TTL is a separate request-level concern (cache_control: { ttl } in the request body).',
483
+ 'How long Magic Context assumes the provider\'s cached prefix stays valid. This is MC\'s own deferral gate — it does not change the provider\'s actual cache lifetime. String (e.g. "5m", "1h", "30s") or per-model object ({ default: "5m", "provider/model": "1h", "provider/*": "never" }); keys resolve most-specific first (exact provider/model, bare model ID, shorter dash-prefixes, then the provider/* wildcard). Explicit per-model entries win; otherwise GPT-5.6 and later (including gpt-6*, through any provider prefix) use a built-in 30m lifetime before the object default or 5m fallback. An unset or global "5m" opts into built-in defaults; any other global string is an explicit policy and wins. User and project settings, including an explicit "5m" or object default, apply on the next pass. Only built-in defaults are frozen per session (per model), including across restarts. /mmc-status shows the effective value and source. OpenAI documents at least 30 minutes since the latest write or reuse: https://developers.openai.com/api/docs/guides/prompt-caching (Cache lifetime and Summary of model differences). Set to "never" to mean MC never assumes expiry (for lanes kept warm externally by a cache-keep tool) — disables the idle-TTL heuristic so MC never initiates a rebuild based on elapsed time. Provider-side extended TTL is a separate request-level concern (cache_control: { ttl } in the request body).',
530
484
  ),
531
485
  prompt_surface: PromptSurfaceConfigSchema.default({ default: "full" }).describe(
532
486
  "Prompt-surface presets: default is full; models use bare model IDs, provider/model, or provider/* routing keys. Guidance and tool-description overrides are user-level only. OpenCode 1.x and Pi register tool descriptions once per process (they follow the default preset). OpenCode 2 rewrites the five mmc_* descriptions per request from the draft model.",
@@ -586,10 +540,23 @@ export const MagicContextConfigSchema = z.object({
586
540
  "Positive integer token floor to protect from automatic reclaim (min: 4_000, max: 1_000_000). When omitted, the derived default is clamp(round(0.05 × usableSoft), min(16_000, round(0.08 × usableSoft)), 64_000).",
587
541
  ),
588
542
  clear_reasoning_age: z
589
- .number()
590
- .min(10)
591
- .default(50)
592
- .describe("Clear reasoning/thinking blocks older than N tags (default: 50)"),
543
+ .unknown()
544
+ .optional()
545
+ .describe("Deprecated and ignored. Use keep_reasoning_tokens instead.")
546
+ .meta({ deprecated: true }),
547
+ keep_reasoning_tokens: z
548
+ .union([
549
+ z.number().int().min(0).max(1_000_000),
550
+ z
551
+ .object({
552
+ default: z.number().int().min(0).max(1_000_000).optional(),
553
+ })
554
+ .catchall(z.number().int().min(0).max(1_000_000)),
555
+ ])
556
+ .optional()
557
+ .describe(
558
+ "Reasoning tokens to keep on rebuilding passes. Number or per-model object; exact, shorter model keys, provider/*, then default. Omitted: fixed 10,000. 0 removes all eligible historical reasoning; newest and exempt steps always stay.",
559
+ ),
593
560
  history_budget_percentage: z
594
561
  .number()
595
562
  .min(0.05)
@@ -744,11 +711,13 @@ export const MagicContextConfigSchema = z.object({
744
711
  pi: PiConfigSchema.describe(
745
712
  "Pi-only child-process extension controls. This setting is user-level only; project configuration cannot choose which extensions a user's subagent children load.",
746
713
  ),
747
- smart_drops: z
748
- .boolean()
749
- .default(false)
714
+ smart_drops: z.union([z.boolean(), z.record(z.string(), z.unknown())]).optional(),
715
+ protected_tools: z
716
+ .record(z.string(), z.number().int().nonnegative())
717
+ .default({ ...DEFAULT_PROTECTED_TOOLS })
718
+ .transform((map) => mergeProtectedTools(map))
750
719
  .describe(
751
- "Content-aware reclaim of provably-superseded tool output, layered on the existing execute-pass auto-drop. When on: superseded todowrite (keep newest 1), spent mmc_reduce (keep newest 3), and zero-value meta (bash_status, bash_kill, mmc_note read/dismiss) outputs are dropped; older edits to a file are compressed to a filePath-preserving marker while the newest edit per file stays full. Only acts on passes already busting the cache, so it never originates a cache bust. Honors the protected-tag reserve. Experimental: opt-in, default off until cache stability is proven; when off the wire is byte-identical to the positional-only reclaim. Requires a restart.",
720
+ "Keep each tool's newest N still-active results in every automatic drop lane (upstream issue 621). User and project maps merge over defaults {todowrite: 1, mmc_reduce: 3}; 0 turns protection off. Names are case-insensitive and ignore leading mcp_ (upstream's ctx_reduce maps to mmc_reduce). Holds even at 95% pressure, with no byte cap: large protected outputs can reach refusal sooner. Queued drops, from the agent or historian publication, are held until newer calls displace the result and a later cache-rebuilding pass applies them. The historian's summary is unaffected; the raw result leaves at the next fold. Frozen strips are unaffected. Changes and rotation never originate a bust; dropped results are never restored.",
752
721
  ),
753
722
  caveman_text_compression: z
754
723
  .object({
@@ -964,13 +933,12 @@ export const MagicContextConfigSchema = z.object({
964
933
 
965
934
  /** Settings whose fresh values can be used by later agent runs without changing rendered prompt bytes. */
966
935
  export const LIVE_RELOAD_CONFIG_PATHS = [
936
+ "cache_ttl",
967
937
  "toast_duration_ms",
968
- "historian.opencode.model",
969
- "historian.opencode.fallback_models",
970
- "historian.opencode.variant",
971
- "historian.pi.model",
972
- "historian.pi.fallback_models",
973
- "historian.pi.thinking_level",
938
+ "historian.model",
939
+ "historian.fallback_models",
940
+ "historian.variant",
941
+ "historian.thinking_level",
974
942
  "historian.two_pass",
975
943
  "historian.expand_tools",
976
944
  "historian.maxTokens",
@@ -31,6 +31,14 @@ export interface SubstituteResult {
31
31
  * user via logger/toast/startup notification.
32
32
  */
33
33
  warnings: string[];
34
+ /**
35
+ * The subset of {@link warnings} that are unresolved references: a missing
36
+ * or empty env var, a missing file, or an unreadable file that fell back to
37
+ * empty string. Purely advisory to the runtime (which keeps the lenient
38
+ * warning behavior); `config-check` treats these as hard config errors.
39
+ * Does not include the sensitive-path warning.
40
+ */
41
+ failures: string[];
34
42
  }
35
43
 
36
44
  const ENV_PATTERN = /\{env:([^}]+)\}/g;
@@ -78,6 +86,7 @@ function sensitiveFilePathReason(resolvedPath: string): string | null {
78
86
  */
79
87
  export function substituteConfigVariables(input: SubstituteInput): SubstituteResult {
80
88
  const warnings: string[] = [];
89
+ const failures: string[] = [];
81
90
  let text = input.text;
82
91
 
83
92
  if (input.isProjectConfig) {
@@ -96,7 +105,7 @@ export function substituteConfigVariables(input: SubstituteInput): SubstituteRes
96
105
  `Project-level config no longer supports ${tokenTypes} tokens for security reasons; leaving tokens literal. Move secret expansion to user-level config.`,
97
106
  );
98
107
  }
99
- return { text, warnings };
108
+ return { text, warnings, failures };
100
109
  }
101
110
 
102
111
  // Strip JSONC comments before substitution so tokens in comments cannot
@@ -108,9 +117,9 @@ export function substituteConfigVariables(input: SubstituteInput): SubstituteRes
108
117
  const varName = rawName.trim();
109
118
  const value = varName ? process.env[varName] : undefined;
110
119
  if (value === undefined || value === "") {
111
- warnings.push(
112
- `Environment variable ${varName} is not set (referenced via {env:${varName}}); using empty string`,
113
- );
120
+ const message = `Environment variable ${varName} is not set (referenced via {env:${varName}}); using empty string`;
121
+ warnings.push(message);
122
+ failures.push(message);
114
123
  return "";
115
124
  }
116
125
 
@@ -119,7 +128,7 @@ export function substituteConfigVariables(input: SubstituteInput): SubstituteRes
119
128
 
120
129
  const fileMatches = Array.from(text.matchAll(FILE_PATTERN));
121
130
  if (fileMatches.length === 0) {
122
- return { text, warnings };
131
+ return { text, warnings, failures };
123
132
  }
124
133
 
125
134
  const configDir = input.configPath ? dirname(input.configPath) : process.cwd();
@@ -164,9 +173,9 @@ export function substituteConfigVariables(input: SubstituteInput): SubstituteRes
164
173
  }
165
174
 
166
175
  if (!existsSync(filePath)) {
167
- warnings.push(
168
- `File not found for ${token} (resolved to ${filePath}); using empty string`,
169
- );
176
+ const message = `File not found for ${token} (resolved to ${filePath}); using empty string`;
177
+ warnings.push(message);
178
+ failures.push(message);
170
179
  continue;
171
180
  }
172
181
 
@@ -175,9 +184,9 @@ export function substituteConfigVariables(input: SubstituteInput): SubstituteRes
175
184
  contents = readFileSync(filePath, "utf-8").trim();
176
185
  } catch (error) {
177
186
  const message = error instanceof Error ? error.message : String(error);
178
- warnings.push(
179
- `Failed to read file for ${token} (${filePath}): ${message}; using empty string`,
180
- );
187
+ const warning = `Failed to read file for ${token} (${filePath}): ${message}; using empty string`;
188
+ warnings.push(warning);
189
+ failures.push(warning);
181
190
  continue;
182
191
  }
183
192
 
@@ -188,5 +197,5 @@ export function substituteConfigVariables(input: SubstituteInput): SubstituteRes
188
197
  }
189
198
 
190
199
  output += text.slice(cursor);
191
- return { text: output, warnings };
200
+ return { text: output, warnings, failures };
192
201
  }
@@ -0,0 +1,27 @@
1
+ const pending = new Map<string, Set<AbortController>>();
2
+
3
+ /** Cleanup cancels optional searches and backfill, not durable served decisions. */
4
+ export function registerAutoSearchWork(sessionId: string): {
5
+ signal: AbortSignal;
6
+ done: () => void;
7
+ } {
8
+ const controller = new AbortController();
9
+ const work = pending.get(sessionId) ?? new Set<AbortController>();
10
+ pending.set(sessionId, work);
11
+ work.add(controller);
12
+ return {
13
+ signal: controller.signal,
14
+ done: () => {
15
+ work.delete(controller);
16
+ if (pending.get(sessionId) === work && work.size === 0) pending.delete(sessionId);
17
+ },
18
+ };
19
+ }
20
+
21
+ export function cancelAutoSearchWork(sessionId?: string): void {
22
+ for (const [id, work] of pending) {
23
+ if (sessionId !== undefined && id !== sessionId) continue;
24
+ pending.delete(id);
25
+ for (const controller of work) controller.abort();
26
+ }
27
+ }
@@ -45,7 +45,7 @@ describe("resolveCacheTtlDisplay", () => {
45
45
  });
46
46
 
47
47
  expect(configured.source).toBe("config");
48
- expect(session).toEqual({ value: "45m", source: "session", modelKey });
48
+ expect(session).toEqual({ value: "1h", source: "config", modelKey });
49
49
  expect(fallback).toEqual({ value: "5m", source: "default", modelKey });
50
50
  });
51
51
 
@@ -10,7 +10,7 @@ export interface CacheTtlDisplay {
10
10
  }
11
11
 
12
12
  export interface ResolveCacheTtlDisplayArgs {
13
- frozen?: ResolvedCacheTtl;
13
+ frozen?: ResolvedCacheTtl & { builtInDefault?: ResolvedCacheTtl };
14
14
  configured: MagicContextConfig["cache_ttl"];
15
15
  configuredExplicitly: boolean;
16
16
  modelKey: string | undefined;
@@ -20,12 +20,22 @@ export interface ResolveCacheTtlDisplayArgs {
20
20
  }
21
21
 
22
22
  /**
23
- * Resolve status-only TTL text without changing the scheduler's persisted truth.
24
- * Use the session value only when its persisted model key matches the model being
25
- * displayed; otherwise use live config because the session value may be for another model.
23
+ * Resolve the lifetime shown in status without changing the value saved for the scheduler.
24
+ * The next pass uses current settings; a saved built-in lifetime applies only to the same
25
+ * model, even after an override is removed.
26
26
  */
27
27
  export function resolveCacheTtlDisplay(args: ResolveCacheTtlDisplayArgs): CacheTtlDisplay {
28
- if (args.frozen) return args.frozen;
28
+ const resolved = resolveModelCacheTtl(
29
+ args.configured,
30
+ args.modelKey,
31
+ args.configuredExplicitly,
32
+ );
33
+ if (resolved.source === "config") return resolved;
34
+ if (args.frozen && (!args.modelKey || args.frozen.modelKey === args.modelKey)) {
35
+ return args.frozen.source === "config"
36
+ ? (args.frozen.builtInDefault ?? resolved)
37
+ : args.frozen;
38
+ }
29
39
  if (
30
40
  (args.sessionModelKey && (!args.modelKey || args.sessionModelKey === args.modelKey)) ||
31
41
  (!args.modelKey && !args.sessionModelKey && args.sessionValue !== "5m")
@@ -37,22 +47,13 @@ export function resolveCacheTtlDisplay(args: ResolveCacheTtlDisplayArgs): CacheT
37
47
  };
38
48
  }
39
49
 
40
- const resolved = resolveModelCacheTtl(args.configured, args.modelKey);
41
- if (
42
- resolved.source === "default" &&
43
- typeof args.configured === "string" &&
44
- args.configuredExplicitly
45
- )
46
- return { ...resolved, source: "config" };
47
50
  return resolved;
48
51
  }
49
52
 
50
53
  export function formatCacheTtlDisplay(display: CacheTtlDisplay): string {
51
- if (display.source === "OpenAI GPT-5.6+ default")
52
- return `Cache TTL: ${display.value} (${display.source})`;
53
54
  if (display.source === "session") return `Cache TTL: ${display.value} (session)`;
54
55
  if (display.source === "config") {
55
- return `Cache TTL: ${display.value} (config for ${display.modelKey ?? "current model"})`;
56
+ return `Cache TTL: ${display.value} (your config)`;
56
57
  }
57
- return `Cache TTL: ${display.value} (default — no cache_ttl for ${display.modelKey ?? "unknown model"})`;
58
+ return `Cache TTL: ${display.value} (built-in default, frozen for this session)`;
58
59
  }
@@ -29,8 +29,8 @@ describe("known model cache TTL", () => {
29
29
  expect(resolveCacheTtl("5m", "openai/gpt-6")).toBe("30m");
30
30
  expect(resolveCacheTtl("10m", "openai/gpt-6")).toBe("10m");
31
31
  });
32
- it("honors per-model entries before built-ins and built-ins before object defaults", () => {
33
- expect(resolveCacheTtl({ default: "10m" }, "openai/gpt-6")).toBe("30m");
32
+ it("honors per-model entries and configured defaults before built-ins", () => {
33
+ expect(resolveCacheTtl({ default: "10m" }, "openai/gpt-6")).toBe("10m");
34
34
  expect(resolveCacheTtl({ default: "10m", "gpt-6": "5m" }, "openai/gpt-6")).toBe("5m");
35
35
  expect(resolveCacheTtl({ default: "10m", "openai/*": "never" }, "openai/gpt-6")).toBe(
36
36
  "never",
@@ -28,6 +28,8 @@ const MODEL_CACHE_LIFETIMES = [
28
28
  export function resolveModelCacheTtl(
29
29
  config: CacheTtlConfig | undefined,
30
30
  modelKey: string | undefined,
31
+ configuredExplicitly = typeof config === "object" ||
32
+ (typeof config === "string" && config !== "5m"),
31
33
  ): ResolvedCacheTtl {
32
34
  if (config && typeof config !== "string") {
33
35
  const match =
@@ -37,11 +39,13 @@ export function resolveModelCacheTtl(
37
39
  modelKey !== "default"
38
40
  ? config[modelKey]
39
41
  : resolveModelConfigValue(config, modelKey)?.value;
40
- if (match !== undefined) return { value: match, source: "config", modelKey };
42
+ const value = match ?? config.default;
43
+ if (value !== undefined && configuredExplicitly)
44
+ return { value, source: "config", modelKey };
41
45
  }
42
- // A non-5m global string is an explicit policy. The generic 5m default is
43
- // not evidence of provider eviction: documented lifetimes avoid paid rewrites.
44
- if (typeof config === "string" && config !== "5m")
46
+ // The loader supplies provenance: an explicit 5m is policy too. Older callers
47
+ // without that bit still distinguish non-default strings from the schema's 5m.
48
+ if (typeof config === "string" && configuredExplicitly)
45
49
  return { value: config, source: "config", modelKey };
46
50
  const model =
47
51
  canonicalModelIdentity(modelKey ?? "")