@retaw106/mmc-oc 1.2.3 → 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 +1 -1
  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
@@ -16,6 +16,11 @@ import type { PromptSurfaceConfig } from "../shared/prompt-surface";
16
16
  import { setWindowOverlayPath } from "../shared/window-geometry";
17
17
  import { migrateLegacyAgentEnabledInMemory } from "./agent-disable";
18
18
  import { mmcProjectConfigBasePath, opencodeUserConfigBasePath } from "./config-paths";
19
+ import {
20
+ legacyHarnessBlockPaths,
21
+ RemovedHistorianHarnessBlockError,
22
+ removedHistorianBlockMessage,
23
+ } from "./legacy-harness-blocks";
19
24
  import { migrateLegacyExperimental } from "./migrate-experimental";
20
25
  import { resolveConfigProfile } from "./profiles";
21
26
  import {
@@ -150,6 +155,41 @@ function loadConfigFileDetailed(
150
155
  `Ignored unsafe config key "${path}" (security: prototype-pollution keys are not allowed).`,
151
156
  );
152
157
  const firstIssue = parsed.issues[0];
158
+ // A removed per-harness historian block is a hard config error: reject the
159
+ // whole file (parse-failure → defaults at boot / last-known-good on reload)
160
+ // instead of letting Zod strip the block and leave the historian with no
161
+ // model. Detection is by key presence, so it also fires for a project file
162
+ // whose block was emptied by tier hardening. It runs on the RECOVERED
163
+ // object too — a file with a JSONC syntax error must still not let a legacy
164
+ // block slip through to the parser, which would throw uncaught.
165
+ const legacyPaths = legacyHarnessBlockPaths(config);
166
+ if (legacyPaths.length > 0) {
167
+ const message = removedHistorianBlockMessage(legacyPaths);
168
+ const warning = `${configPath}:1:1: ${message}`;
169
+ const failure: ConfigParseFailure = {
170
+ warningClass: CONFIG_WARNING_CLASS.FILE_PARSE,
171
+ source,
172
+ path: configPath,
173
+ line: 1,
174
+ column: 1,
175
+ message,
176
+ recovered: false,
177
+ warning,
178
+ };
179
+ return {
180
+ config: {},
181
+ warnings: [
182
+ warning,
183
+ ...rawWarnings.map((w) => `${configPath}: ${w}`),
184
+ ...substituted.warnings.map((w) => `${configPath}: ${w}`),
185
+ ...unsafeKeyWarnings.map((w) => `${configPath}: ${w}`),
186
+ ],
187
+ parseFailures: [failure],
188
+ warningDetails: [failure],
189
+ outcome: "project-file-parse-error",
190
+ source,
191
+ };
192
+ }
153
193
  const recovered = firstIssue !== undefined && Object.keys(config).length > 0;
154
194
  const parseFailures: ConfigParseFailure[] = firstIssue
155
195
  ? [
@@ -312,53 +352,19 @@ export function formatProtectedTokensBelowMinWarning(value: number): string {
312
352
  return `protected_tokens is a token floor (minimum ${PROTECTED_TOKENS_MIN}, default derived from the context window); ${value} looks like a leftover tag-count value rather than a token floor. Remove the key to use the default, or set a token count such as 16000.`;
313
353
  }
314
354
 
315
- /** The harness blocks the historian model resolver actually reads. */
316
- const MODEL_HARNESS_BLOCKS = ["opencode", "pi"] as const;
317
-
318
- /**
319
- * Warn when an agent's model is configured somewhere the resolver never looks.
320
- *
321
- * Agent models are resolved per harness: `historian.opencode.model` and
322
- * `historian.pi.model`. A bare `historian.model` is schema-valid and reads
323
- * naturally, but nothing consumes it —
324
- * `resolveHistorianModel({historian:{model}}, "opencode")` returns no models at
325
- * all, and the only outward sign is a historian that silently never runs. Saying
326
- * so at load time is the difference between a five-minute fix and a subsystem
327
- * that looks configured and is not.
328
- */
329
- export function misplacedAgentModelWarnings(rawConfig: Record<string, unknown>): string[] {
330
- const warnings: string[] = [];
331
- for (const agent of ["historian"]) {
332
- const block = rawConfig[agent];
333
- if (!block || typeof block !== "object" || Array.isArray(block)) continue;
334
- const record = block as Record<string, unknown>;
335
- if (!Object.hasOwn(record, "model")) continue;
336
- const harnessWithModel = MODEL_HARNESS_BLOCKS.find((harness) => {
337
- const sub = record[harness];
338
- return (
339
- sub !== null &&
340
- typeof sub === "object" &&
341
- !Array.isArray(sub) &&
342
- Object.hasOwn(sub as Record<string, unknown>, "model")
343
- );
344
- });
345
- if (harnessWithModel) continue;
346
- warnings.push(
347
- `${agent}.model is not read: the ${agent} model is resolved per harness, so it has to be ${agent}.opencode.model (OpenCode 1 and 2) or ${agent}.pi.model. As written the ${agent} resolves to no models and never runs.`,
348
- );
349
- }
350
- return warnings;
351
- }
352
-
353
355
  export function parsePluginConfig(
354
356
  rawConfig: Record<string, unknown>,
355
357
  recoveredTopLevelKeys: string[] = [],
356
358
  ): MagicContextPluginConfig & { configWarnings?: string[] } {
359
+ // Reject — do not silently strip — a removed per-harness historian block. The
360
+ // loaders already reject such files before parsing; this makes the rejection
361
+ // explicit for direct callers and tests (never reached from a real load).
362
+ const legacyPaths = legacyHarnessBlockPaths(rawConfig);
363
+ if (legacyPaths.length > 0) throw new RemovedHistorianHarnessBlockError(legacyPaths);
357
364
  // Pre-Zod shim: reshape legacy experimental.* graduated keys so the user's
358
365
  // opt-in/out state survives upgrades even when they never run `doctor`.
359
366
  const preMigrationWarnings: string[] = [];
360
367
  const configWithoutRemovedAgent = stripRemovedAgentConfig(rawConfig, preMigrationWarnings);
361
- preMigrationWarnings.push(...misplacedAgentModelWarnings(rawConfig));
362
368
  const migratedExperimental = migrateLegacyExperimental(
363
369
  configWithoutRemovedAgent,
364
370
  preMigrationWarnings,
@@ -0,0 +1,78 @@
1
+ /// <reference types="bun-types" />
2
+
3
+ import { describe, expect, it } from "bun:test";
4
+ import { resolveHistorianModel } from "../shared/model-resolution";
5
+ import { parsePluginConfig } from "./index";
6
+ import {
7
+ legacyHarnessBlockPaths,
8
+ RemovedHistorianHarnessBlockError,
9
+ removedHistorianBlockMessage,
10
+ } from "./legacy-harness-blocks";
11
+
12
+ /**
13
+ * The flat `historian.model` is the only supported shape. The removed
14
+ * `historian.opencode` / `historian.pi` blocks (and their profile equivalents)
15
+ * are a hard config error: the loader rejects the whole file (defaults at boot,
16
+ * last-known-good on live reload) instead of letting Zod strip the block and
17
+ * leave the historian resolving to no model.
18
+ */
19
+ describe("removed per-harness historian blocks", () => {
20
+ it("resolves the flat historian.model", () => {
21
+ expect(resolveHistorianModel({ historian: { model: "openai/gpt-5" } }, "opencode")).toEqual(
22
+ {
23
+ primary: { model: "openai/gpt-5" },
24
+ fallbacks: [],
25
+ },
26
+ );
27
+ });
28
+
29
+ it("lists the exact paths of removed historian harness blocks", () => {
30
+ expect(
31
+ legacyHarnessBlockPaths({ historian: { opencode: { model: "openai/gpt-5" } } }),
32
+ ).toEqual(["historian.opencode"]);
33
+
34
+ expect(
35
+ legacyHarnessBlockPaths({
36
+ historian: { opencode: { model: "x/y" }, pi: { model: "x/y" } },
37
+ }),
38
+ ).toEqual(["historian.opencode", "historian.pi"]);
39
+ });
40
+
41
+ it("lists removed profile historian blocks by profile name", () => {
42
+ expect(
43
+ legacyHarnessBlockPaths({
44
+ profiles: {
45
+ work: { historian: { pi: { model: "x/y" } } },
46
+ personal: { historian: { opencode: { model: "x/y" } } },
47
+ },
48
+ }),
49
+ ).toEqual(["profiles.work.historian.pi", "profiles.personal.historian.opencode"]);
50
+ });
51
+
52
+ it("stays empty for the flat shape and unrelated config", () => {
53
+ expect(legacyHarnessBlockPaths({ historian: { model: "openai/gpt-5" } })).toEqual([]);
54
+ expect(legacyHarnessBlockPaths({})).toEqual([]);
55
+ expect(legacyHarnessBlockPaths({ historian: { disable: true } })).toEqual([]);
56
+ expect(
57
+ legacyHarnessBlockPaths({ profiles: { work: { historian: { model: "x/y" } } } }),
58
+ ).toEqual([]);
59
+ });
60
+
61
+ it("names every offending path and the flat replacement", () => {
62
+ const message = removedHistorianBlockMessage(["historian.opencode"]);
63
+ expect(message).toContain("historian.opencode");
64
+ expect(message).toContain("historian.model");
65
+ expect(message).toContain("historian.fallback_models");
66
+ });
67
+
68
+ it("rejects a raw config that still carries the removed blocks", () => {
69
+ expect(() =>
70
+ parsePluginConfig({ historian: { opencode: { model: "openai/gpt-5" } } }),
71
+ ).toThrow(RemovedHistorianHarnessBlockError);
72
+ expect(() =>
73
+ parsePluginConfig({
74
+ profiles: { work: { historian: { pi: { model: "x/y" } } } },
75
+ }),
76
+ ).toThrow(/profiles\.work\.historian\.pi/);
77
+ });
78
+ });
@@ -0,0 +1,71 @@
1
+ import { LEGACY_HARNESS_BLOCK_KEYS } from "./schema/magic-context";
2
+
3
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
4
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5
+ }
6
+
7
+ /**
8
+ * Exact dotted paths of the removed per-harness historian model blocks anywhere
9
+ * in a raw config: the top-level `historian.opencode` / `historian.pi`, and the
10
+ * same keys inside profile definitions
11
+ * (`profiles.<name>.historian.opencode` / `.pi`).
12
+ *
13
+ * These blocks are no longer part of the schema. The loaders use this to REJECT
14
+ * the whole config file (a parse failure → defaults at boot, last-known-good on
15
+ * live reload) instead of letting Zod silently strip the block and leave the
16
+ * historian with no model — a subsystem that looks configured but never runs.
17
+ * Detection is by key presence, so it still fires after project-tier hardening
18
+ * empties (but does not remove) the block.
19
+ */
20
+ export function legacyHarnessBlockPaths(rawConfig: unknown): string[] {
21
+ if (!isPlainObject(rawConfig)) return [];
22
+ const paths: string[] = [];
23
+
24
+ const historian = rawConfig.historian;
25
+ if (isPlainObject(historian)) {
26
+ for (const key of LEGACY_HARNESS_BLOCK_KEYS) {
27
+ if (key in historian) paths.push(`historian.${key}`);
28
+ }
29
+ }
30
+
31
+ const profiles = rawConfig.profiles;
32
+ if (isPlainObject(profiles)) {
33
+ for (const [name, profile] of Object.entries(profiles)) {
34
+ if (!isPlainObject(profile)) continue;
35
+ const profileHistorian = profile.historian;
36
+ if (!isPlainObject(profileHistorian)) continue;
37
+ for (const key of LEGACY_HARNESS_BLOCK_KEYS) {
38
+ if (key in profileHistorian) paths.push(`profiles.${name}.historian.${key}`);
39
+ }
40
+ }
41
+ }
42
+
43
+ return paths;
44
+ }
45
+
46
+ /** User-facing message for a rejected config, naming every offending path. */
47
+ export function removedHistorianBlockMessage(paths: readonly string[]): string {
48
+ return (
49
+ `removed per-harness historian block(s) ${paths.join(", ")}: historian model ` +
50
+ "resolution is now flat. Use historian.model for the primary model, " +
51
+ "historian.fallback_models for fallbacks, and historian.variant (OpenCode) / " +
52
+ "historian.thinking_level (Pi). This config is rejected — running on defaults, " +
53
+ "or keeping the last known-good config on a live reload."
54
+ );
55
+ }
56
+
57
+ /**
58
+ * Thrown by the config parse functions when a raw config still carries a removed
59
+ * per-harness historian block. The loaders reject such files before parsing, so a
60
+ * real config load never reaches this; it makes the rejection explicit and
61
+ * directly observable for callers and tests.
62
+ */
63
+ export class RemovedHistorianHarnessBlockError extends Error {
64
+ readonly paths: readonly string[];
65
+
66
+ constructor(paths: readonly string[]) {
67
+ super(removedHistorianBlockMessage(paths));
68
+ this.name = "RemovedHistorianHarnessBlockError";
69
+ this.paths = paths;
70
+ }
71
+ }
@@ -20,7 +20,6 @@ for (const host of ["OC1", "OC2", "Pi"] as const) {
20
20
  mkdirSync(join(root, "config", "opencode"), { recursive: true });
21
21
  mkdirSync(join(directory, ".mmc"), { recursive: true });
22
22
  try {
23
- const block = host === "Pi" ? "pi" : "opencode";
24
23
  const write = (
25
24
  model: string,
26
25
  fallback: string,
@@ -31,7 +30,7 @@ for (const host of ["OC1", "OC2", "Pi"] as const) {
31
30
  writeFileSync(
32
31
  file,
33
32
  JSON.stringify({
34
- historian: { maxTokens, [block]: { model, fallback_models: [fallback] } },
33
+ historian: { maxTokens, model, fallback_models: [fallback] },
35
34
  memory: { auto_promote: autoPromote },
36
35
  commit_cluster_trigger: { enabled: true, min_clusters: minClusters },
37
36
  }),
@@ -95,13 +94,13 @@ test("malformed project config retains last good values and deduplicates the war
95
94
  writeFileSync(
96
95
  userFile,
97
96
  JSON.stringify({
98
- historian: { opencode: { model: "user/historian" } },
97
+ historian: { model: "user/historian" },
99
98
  }),
100
99
  );
101
100
  writeFileSync(
102
101
  projectFile,
103
102
  JSON.stringify({
104
- historian: { opencode: { model: "project/historian" } },
103
+ historian: { model: "project/historian" },
105
104
  }),
106
105
  );
107
106
  const load = () => loadPluginConfigDetailed(directory, false).config;
@@ -121,7 +120,7 @@ test("malformed project config retains last good values and deduplicates the war
121
120
  writeFileSync(
122
121
  projectFile,
123
122
  JSON.stringify({
124
- historian: { opencode: { model: "project/new-historian" } },
123
+ historian: { model: "project/new-historian" },
125
124
  }),
126
125
  );
127
126
  const second = reader.poll();
@@ -137,3 +136,48 @@ test("malformed project config retains last good values and deduplicates the war
137
136
  rmSync(root, { recursive: true, force: true });
138
137
  }
139
138
  });
139
+
140
+ test("a valid config that becomes a removed per-harness historian block keeps the last good values", () => {
141
+ const root = createTestTempDirFromPath(join(tmpdir(), "mc-live-legacy-"));
142
+ const previous = { home: process.env.HOME, config: process.env.XDG_CONFIG_HOME };
143
+ process.env.HOME = root;
144
+ process.env.XDG_CONFIG_HOME = join(root, "config");
145
+ const directory = join(root, "project");
146
+ const userFile = join(root, "config", "opencode", "mmc.jsonc");
147
+ mkdirSync(join(root, "config", "opencode"), { recursive: true });
148
+ mkdirSync(join(directory, ".mmc"), { recursive: true });
149
+ const warnings: string[] = [];
150
+ try {
151
+ writeFileSync(userFile, JSON.stringify({ historian: { model: "user/good" } }));
152
+ // Mirror the production live reader: reject (throw) on an error load outcome.
153
+ const load = () => {
154
+ const loaded = loadPluginConfigDetailed(directory, false);
155
+ if (loaded.loadOutcome === "project-file-parse-error") {
156
+ throw new Error(`invalid configuration: ${loaded.loadOutcome}`);
157
+ }
158
+ return loaded.config;
159
+ };
160
+ const reader = new LiveConfigReader(directory, load(), load, (message) =>
161
+ warnings.push(message),
162
+ );
163
+ const first = reader.poll();
164
+ expect(resolveHistorianModel(first.effective, "opencode").primary?.model).toBe("user/good");
165
+
166
+ // Syntactically valid JSONC that still carries the removed per-harness block.
167
+ writeFileSync(
168
+ userFile,
169
+ JSON.stringify({ historian: { opencode: { model: "user/legacy" } } }),
170
+ );
171
+ expect(reader.poll()).toBe(first);
172
+ expect(resolveHistorianModel(reader.current().effective, "opencode").primary?.model).toBe(
173
+ "user/good",
174
+ );
175
+ expect(warnings.some((message) => message.includes("config reload failed"))).toBe(true);
176
+ } finally {
177
+ if (previous.home === undefined) delete process.env.HOME;
178
+ else process.env.HOME = previous.home;
179
+ if (previous.config === undefined) delete process.env.XDG_CONFIG_HOME;
180
+ else process.env.XDG_CONFIG_HOME = previous.config;
181
+ rmSync(root, { recursive: true, force: true });
182
+ }
183
+ });
@@ -6,6 +6,7 @@ import { LIVE_RELOAD_CONFIG_PATHS, type MagicContextConfig } from "./schema/magi
6
6
  /** Copy only schema-marked live paths from fresh config; other settings retain boot values. */
7
7
  export function sampleLiveConfig<T extends MagicContextConfig>(boot: T, fresh: T): T {
8
8
  const result = { ...boot } as Record<string, unknown>;
9
+ result.cacheTtlConfigured = fresh.cacheTtlConfigured;
9
10
  for (const path of LIVE_RELOAD_CONFIG_PATHS) {
10
11
  const parts = path.split(".");
11
12
  let value: unknown = fresh;
@@ -49,6 +49,10 @@ export class LiveConfigReader<T> {
49
49
  JSON.stringify((previous as Record<string, unknown>)[key]) !==
50
50
  JSON.stringify((next as Record<string, unknown>)[key]),
51
51
  ),
52
+ // Which host's user config file to watch. Defaults to OpenCode; Pi passes
53
+ // its own `~/.pi/agent/mmc` base so Pi user-config edits are observed
54
+ // (each harness reads its own user config directory).
55
+ private readonly userConfigBasePath: () => string = opencodeUserConfigBasePath,
52
56
  ) {
53
57
  this.snapshot = {
54
58
  generation: 1,
@@ -67,7 +71,7 @@ export class LiveConfigReader<T> {
67
71
  }
68
72
 
69
73
  poll(): ConfigSnapshot<T> {
70
- const paths = [opencodeUserConfigBasePath(), mmcProjectConfigBasePath(this.directory)].map(
74
+ const paths = [this.userConfigBasePath(), mmcProjectConfigBasePath(this.directory)].map(
71
75
  (base) => detectConfigFile(base).path,
72
76
  );
73
77
  const next = new Map<string, FileState>();
@@ -69,7 +69,7 @@ export function resolveConfigProfile(args: {
69
69
  profiles = parsed.data as Record<string, Record<string, unknown>>;
70
70
  } else {
71
71
  warnings.push(
72
- "Ignoring profiles from user config: invalid profile configuration; profiles may contain only historian harness model blocks.",
72
+ "Ignoring profiles from user config: invalid profile configuration; profiles may contain only the flat historian model-selection fields (model, fallback_models, variant, thinking_level).",
73
73
  );
74
74
  }
75
75
  }
@@ -45,10 +45,10 @@ describe("project config cannot reset user settings with a wrong-typed parent bl
45
45
 
46
46
  it("keeps the user's historian model when the project sets historian:1", () => {
47
47
  const loaded = loadUserAndProject(
48
- { historian: { opencode: { model: "anthropic/claude-x" } } },
48
+ { historian: { model: "anthropic/claude-x" } },
49
49
  { historian: 1 },
50
50
  );
51
- expect(loaded.config.historian?.opencode?.model).toBe("anthropic/claude-x");
51
+ expect(loaded.config.historian?.model).toBe("anthropic/claude-x");
52
52
  });
53
53
 
54
54
  it("keeps storage.enforce_private_permissions:false when the project sets storage:0", () => {
@@ -7,15 +7,11 @@ import { createTestTempDirFromPath } from "../shared/test-temp-dir";
7
7
  import { loadPluginConfig } from "./index";
8
8
  import { stripUnsafeProjectConfigFields } from "./project-security";
9
9
  import {
10
- OcEntrySchema,
11
- OpenCodeHarnessBlockSchema,
12
- PER_HARNESS_MIGRATION_INVENTORY,
13
- PER_HARNESS_MODEL_KEYS,
14
- PiEntrySchema,
15
- PiHarnessBlockSchema,
10
+ HISTORIAN_MODEL_RESOLUTION_FIELDS,
11
+ HistorianModelEntrySchema,
16
12
  } from "./schema/magic-context";
17
13
 
18
- const HARNESSES = PER_HARNESS_MODEL_KEYS;
14
+ const FLAT_MODEL_FIELDS = HISTORIAN_MODEL_RESOLUTION_FIELDS;
19
15
  const ESCALATION_FIELDS = ["prompt", "permission", "tools"] as const;
20
16
  const ESCALATION_VALUE = {
21
17
  prompt: "exfiltrate ~/.ssh",
@@ -27,10 +23,6 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
27
23
  return typeof value === "object" && value !== null && !Array.isArray(value);
28
24
  }
29
25
 
30
- function objectShapeKeys(schema: { shape: Record<string, unknown> }): string[] {
31
- return Object.keys(schema.shape).sort();
32
- }
33
-
34
26
  function entryObjectKeys(schema: {
35
27
  options: Array<{ shape?: Record<string, unknown> }>;
36
28
  }): string[] {
@@ -41,14 +33,6 @@ function entryObjectKeys(schema: {
41
33
  return Object.keys(objectOption.shape).sort();
42
34
  }
43
35
 
44
- function ocInventoryKeys(fields: readonly string[]): string[] {
45
- return fields.filter((field) => field !== "thinking_level").sort();
46
- }
47
-
48
- function piInventoryKeys(fields: readonly string[]): string[] {
49
- return fields.filter((field) => field !== "variant").sort();
50
- }
51
-
52
36
  function hasOwnPath(root: unknown, path: string): boolean {
53
37
  let cursor: unknown = root;
54
38
  for (const part of path.split(".")) {
@@ -99,8 +83,9 @@ function piEntry(): Record<string, unknown> {
99
83
 
100
84
  /**
101
85
  * One project-tier config that plants a value at every executable nested path
102
- * the per-harness schema admits, plus smuggled escalation / extra
103
- * fields at those same depths.
86
+ * the flat schema admits (flat model entries plus the removed per-harness
87
+ * blocks, which are still defensively stripped), plus smuggled escalation /
88
+ * extra fields at those same depths.
104
89
  */
105
90
  function buildHostileProjectConfig(): Record<string, unknown> {
106
91
  return {
@@ -111,8 +96,14 @@ function buildHostileProjectConfig(): Record<string, unknown> {
111
96
  },
112
97
  },
113
98
  historian: {
114
- model: "hostile/flat-historian",
115
- fallback_models: ["hostile/flat-fallback"],
99
+ model: {
100
+ model: "hostile/flat-historian",
101
+ variant: "flat-variant",
102
+ ...ESCALATION_VALUE,
103
+ },
104
+ fallback_models: [
105
+ { model: "hostile/flat-fallback", thinking_level: "low", ...ESCALATION_VALUE },
106
+ ],
116
107
  variant: "flat-variant",
117
108
  thinking_level: "low",
118
109
  temperature: 0.2,
@@ -135,24 +126,27 @@ function buildHostileProjectConfig(): Record<string, unknown> {
135
126
  }
136
127
 
137
128
  function historianUserOnlyPaths(): string[] {
138
- const paths: string[] = [];
139
- for (const field of PER_HARNESS_MIGRATION_INVENTORY.historian.migrated_execution) {
140
- paths.push(`historian.${field}`);
141
- for (const harness of HARNESSES) {
142
- const harnessFields =
143
- harness === "opencode"
144
- ? ocInventoryKeys(PER_HARNESS_MIGRATION_INVENTORY.historian.migrated_execution)
145
- : piInventoryKeys(PER_HARNESS_MIGRATION_INVENTORY.historian.migrated_execution);
146
- if (!harnessFields.includes(field)) continue;
147
- paths.push(`historian.${harness}.${field}`);
148
- }
149
- }
150
- return paths;
129
+ // Exactly the paths the hostile fixture plants: the flat fields plus the
130
+ // harness-appropriate fields inside the removed per-harness blocks.
131
+ return [
132
+ "historian.model",
133
+ "historian.fallback_models",
134
+ "historian.variant",
135
+ "historian.thinking_level",
136
+ "historian.opencode.model",
137
+ "historian.opencode.fallback_models",
138
+ "historian.opencode.variant",
139
+ "historian.pi.model",
140
+ "historian.pi.fallback_models",
141
+ "historian.pi.thinking_level",
142
+ ];
151
143
  }
152
144
 
153
145
  function nestedEscalationPaths(): string[] {
154
146
  const sites = [
155
147
  "historian",
148
+ "historian.model",
149
+ "historian.fallback_models.0",
156
150
  "historian.opencode",
157
151
  "historian.opencode.model",
158
152
  "historian.opencode.fallback_models.0",
@@ -191,15 +185,18 @@ function loadWithUserAndProjectConfig(userConfigText: string, projectConfigText:
191
185
  }
192
186
 
193
187
  describe("hostile-config stripping matrix", () => {
194
- it("enumerates every per-harness executable field from the schema shape", () => {
195
- expect(objectShapeKeys(OpenCodeHarnessBlockSchema)).toEqual(
196
- ocInventoryKeys(PER_HARNESS_MIGRATION_INVENTORY.historian.migrated_execution),
197
- );
198
- expect(objectShapeKeys(PiHarnessBlockSchema)).toEqual(
199
- piInventoryKeys(PER_HARNESS_MIGRATION_INVENTORY.historian.migrated_execution),
200
- );
201
- expect(entryObjectKeys(OcEntrySchema)).toEqual(["model", "variant"]);
202
- expect(entryObjectKeys(PiEntrySchema)).toEqual(["model", "thinking_level"]);
188
+ it("declares the flat historian model-resolution contract", () => {
189
+ expect([...FLAT_MODEL_FIELDS]).toEqual([
190
+ "model",
191
+ "fallback_models",
192
+ "variant",
193
+ "thinking_level",
194
+ ]);
195
+ expect(entryObjectKeys(HistorianModelEntrySchema)).toEqual([
196
+ "model",
197
+ "thinking_level",
198
+ "variant",
199
+ ]);
203
200
  });
204
201
 
205
202
  it("strips every user-only or unsafe nested path and keeps legitimate project-tier fields", () => {
@@ -230,9 +227,6 @@ describe("hostile-config stripping matrix", () => {
230
227
  expect(hasOwnPath(raw, path)).toBe(true);
231
228
  }
232
229
 
233
- expect(hasOwnPath(raw, "historian.opencode.model")).toBe(false);
234
- expect(hasOwnPath(raw, "historian.pi.model")).toBe(false);
235
-
236
230
  for (const path of [...historianUserOnlyPaths(), ...nestedEscalationPaths()]) {
237
231
  expect(warningText).toContain(path);
238
232
  }
@@ -240,27 +234,20 @@ describe("hostile-config stripping matrix", () => {
240
234
 
241
235
  it("does not let project historian qualifiers attach to the user's historian model", () => {
242
236
  const result = loadWithUserAndProjectConfig(
237
+ JSON.stringify({ historian: { model: "anthropic/user-historian" } }),
243
238
  JSON.stringify({
244
- historian: {
245
- opencode: { model: "anthropic/user-historian" },
246
- pi: { model: "github-copilot/user-historian" },
247
- },
248
- }),
249
- JSON.stringify({
250
- historian: {
251
- opencode: { variant: "high" },
252
- pi: { thinking_level: "max" },
253
- },
239
+ historian: { variant: "high", thinking_level: "max" },
254
240
  }),
255
241
  );
256
242
 
257
- expect(result.historian?.opencode).toEqual({ model: "anthropic/user-historian" });
258
- expect(result.historian?.pi).toEqual({ model: "github-copilot/user-historian" });
259
- expect(result.configWarnings?.join("\n") ?? "").toContain("historian.opencode.variant");
260
- expect(result.configWarnings?.join("\n") ?? "").toContain("historian.pi.thinking_level");
243
+ expect(result.historian?.model).toBe("anthropic/user-historian");
244
+ expect(result.historian?.variant).toBeUndefined();
245
+ expect(result.historian?.thinking_level).toBeUndefined();
246
+ expect(result.configWarnings?.join("\n") ?? "").toContain("historian.variant");
247
+ expect(result.configWarnings?.join("\n") ?? "").toContain("historian.thinking_level");
261
248
  });
262
249
 
263
- it("rejects prototype-pollution keys at the nested harness depth", () => {
250
+ it("rejects prototype-pollution keys at the nested legacy harness depth", () => {
264
251
  const projectConfig = `{
265
252
  "historian": {
266
253
  "opencode": {
@@ -273,8 +260,7 @@ describe("hostile-config stripping matrix", () => {
273
260
  const result = loadWithUserAndProjectConfig("{}", projectConfig);
274
261
  const warnings = result.configWarnings?.join("\n") ?? "";
275
262
 
276
- expect(result.historian?.opencode?.model).toBeUndefined();
277
- expect(result.historian?.opencode?.variant).toBeUndefined();
263
+ expect(result.historian?.opencode).toBeUndefined();
278
264
  expect(warnings).toContain("prototype-pollution");
279
265
  expect(warnings).toContain("historian.opencode.__proto__");
280
266
  });
@@ -11,7 +11,7 @@ describe("stripUnsafeProjectConfigFields", () => {
11
11
  profile: "work",
12
12
  profiles: {
13
13
  work: {
14
- historian: { opencode: { model: "attacker/model" } },
14
+ historian: { model: "attacker/model" },
15
15
  },
16
16
  },
17
17
  };
@@ -154,6 +154,8 @@ describe("stripUnsafeProjectConfigFields", () => {
154
154
  historian: {
155
155
  model: "legacy/repo-model",
156
156
  fallback_models: ["legacy/repo-fallback"],
157
+ variant: "high",
158
+ thinking_level: "medium",
157
159
  opencode: {
158
160
  model: "opencode/repo-model",
159
161
  fallback_models: ["opencode/repo-fallback"],
@@ -176,6 +178,9 @@ describe("stripUnsafeProjectConfigFields", () => {
176
178
  });
177
179
  const warning = warnings.join("\n");
178
180
  expect(warning).toContain("historian.model");
181
+ expect(warning).toContain("historian.fallback_models");
182
+ expect(warning).toContain("historian.variant");
183
+ expect(warning).toContain("historian.thinking_level");
179
184
  expect(warning).toContain("historian.opencode.model");
180
185
  expect(warning).toContain("historian.opencode.variant");
181
186
  expect(warning).toContain("historian.pi.model");