@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
@@ -1,38 +1,10 @@
1
- import {
2
- closeSync,
3
- existsSync,
4
- linkSync,
5
- openSync,
6
- readFileSync,
7
- realpathSync,
8
- renameSync,
9
- statSync,
10
- unlinkSync,
11
- writeFileSync,
12
- } from "node:fs";
13
- import { basename, dirname, join } from "node:path";
14
-
15
- import { removeJsoncValue, setJsoncValue } from "../shared/jsonc-edit";
16
- import { parseJsonc } from "../shared/jsonc-parser";
1
+ import { existsSync, readFileSync } from "node:fs";
17
2
 
18
3
  export type ConfigTier = "user" | "project";
19
4
 
20
- export interface FlatConfigDiagnostic {
21
- path: string;
22
- message: string;
23
- }
24
-
25
- export interface FlatConfigMigration {
26
- bytes: Buffer;
27
- hasFlatKeys: boolean;
28
- diagnostics: FlatConfigDiagnostic[];
29
- }
30
-
31
5
  export interface RawConfigLoadOptions {
32
6
  configPath: string;
33
7
  tier: ConfigTier;
34
- /** Test-only seam for making a competing loader replace the target before this loader re-checks it. */
35
- afterTemporaryWrite?: () => void;
36
8
  }
37
9
 
38
10
  export interface RawConfigLoadResult {
@@ -43,393 +15,32 @@ export interface RawConfigLoadResult {
43
15
  migrated: boolean;
44
16
  }
45
17
 
46
- const MODEL_FIELDS = ["model", "fallback_models"] as const;
47
- const PRE_PER_HARNESS_BACKUP_SUFFIX = ".pre-per-harness.bak";
48
- let temporaryFileSequence = 0;
49
-
50
- function isRecord(value: unknown): value is Record<string, unknown> {
51
- return typeof value === "object" && value !== null && !Array.isArray(value);
52
- }
53
-
54
- function asDocument(text: string): Record<string, unknown> | null {
55
- try {
56
- // JSON.parse rejects a UTF-8 BOM, while JSONC files commonly retain one.
57
- // Remove it only for inspection; every byte-writing path adds it back.
58
- const document = parseJsonc<unknown>(text.startsWith("\uFEFF") ? text.slice(1) : text);
59
- return isRecord(document) ? document : null;
60
- } catch {
61
- return null;
62
- }
63
- }
64
-
65
- function getAtPath(document: Record<string, unknown>, path: readonly string[]): unknown {
66
- let current: unknown = document;
67
- for (const part of path) {
68
- if (!isRecord(current) || !Object.hasOwn(current, part)) return undefined;
69
- current = current[part];
70
- }
71
- return current;
72
- }
73
-
74
- function stableJson(value: unknown): string {
75
- if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
76
- if (!isRecord(value)) return JSON.stringify(value);
77
- return `{${Object.keys(value)
78
- .sort()
79
- .map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
80
- .join(",")}}`;
81
- }
82
-
83
- function valuesMatch(left: unknown, right: unknown): boolean {
84
- return stableJson(left) === stableJson(right);
85
- }
86
-
87
- function valueForDiagnostic(value: unknown): string {
88
- return stableJson(value);
89
- }
90
-
91
- function migrateEntryForHarness(value: unknown, harness: "opencode" | "pi"): unknown {
92
- if (Array.isArray(value)) return value.map((entry) => migrateEntryForHarness(entry, harness));
93
- if (!isRecord(value) || !Object.hasOwn(value, "model")) return value;
94
-
95
- const entry: Record<string, unknown> = { model: value.model };
96
- if (harness === "opencode" && Object.hasOwn(value, "variant")) {
97
- entry.variant = value.variant;
98
- }
99
- if (harness === "pi" && Object.hasOwn(value, "thinking_level")) {
100
- entry.thinking_level = value.thinking_level;
101
- }
102
- return entry;
103
- }
104
-
105
- function migrateFallbackForHarness(value: unknown, harness: "opencode" | "pi"): unknown {
106
- if (typeof value === "string") return [value];
107
- return migrateEntryForHarness(value, harness);
108
- }
109
-
110
- function canCreateAtPath(document: Record<string, unknown>, path: readonly string[]): boolean {
111
- let current: unknown = document;
112
- for (const part of path) {
113
- if (current === undefined) return true;
114
- if (!isRecord(current)) return false;
115
- current = current[part];
116
- }
117
- return current === undefined || isRecord(current);
118
- }
119
-
120
- function flatFieldPath(parts: readonly string[]): string {
121
- return parts.join(".");
122
- }
123
-
124
- function updateDocumentForFlatFields(text: string): {
125
- text: string;
126
- hasFlatKeys: boolean;
127
- diagnostics: FlatConfigDiagnostic[];
128
- flatPaths: string[];
129
- } {
130
- const hasBom = text.startsWith("\uFEFF");
131
- const editableText = hasBom ? text.slice(1) : text;
132
- const document = asDocument(text);
133
- if (!document) {
134
- return { text, hasFlatKeys: false, diagnostics: [], flatPaths: [] };
135
- }
136
-
137
- let nextText = editableText;
138
- let hasFlatKeys = false;
139
- const diagnostics: FlatConfigDiagnostic[] = [];
140
- const flatPaths: string[] = [];
141
- const sourcePathsToRemove: string[][] = [];
142
-
143
- const addDestination = (
144
- sourcePath: string[],
145
- destinationPath: string[],
146
- destinationValue: unknown,
147
- ): void => {
148
- const sourceLabel = flatFieldPath(sourcePath);
149
- const destinationLabel = flatFieldPath(destinationPath);
150
- if (!canCreateAtPath(document, destinationPath.slice(0, -1))) {
151
- diagnostics.push({
152
- path: sourceLabel,
153
- message: `Flat config field "${sourceLabel}" (${valueForDiagnostic(destinationValue)}) conflicts with non-object destination "${destinationLabel}" (${valueForDiagnostic(getAtPath(document, destinationPath.slice(0, -1)))}); kept the destination and ignored the flat field.`,
154
- });
155
- return;
156
- }
157
-
158
- const existing = getAtPath(document, destinationPath);
159
- if (existing !== undefined) {
160
- if (!valuesMatch(existing, destinationValue)) {
161
- diagnostics.push({
162
- path: sourceLabel,
163
- message: `Flat config field "${sourceLabel}" (${valueForDiagnostic(destinationValue)}) conflicts with "${destinationLabel}" (${valueForDiagnostic(existing)}); kept "${destinationLabel}" and ignored the flat field.`,
164
- });
165
- }
166
- return;
167
- }
168
-
169
- nextText = setJsoncValue(nextText, destinationPath, destinationValue);
170
- };
171
-
172
- const migrateAgentFields = (agentName: "historian"): void => {
173
- const agent = document[agentName];
174
- if (!isRecord(agent)) return;
175
-
176
- for (const field of MODEL_FIELDS) {
177
- if (!Object.hasOwn(agent, field)) continue;
178
- const sourcePath = [agentName, field];
179
- hasFlatKeys = true;
180
- flatPaths.push(flatFieldPath(sourcePath));
181
- sourcePathsToRemove.push(sourcePath);
182
- const migrateValue =
183
- field === "fallback_models" ? migrateFallbackForHarness : migrateEntryForHarness;
184
- addDestination(
185
- sourcePath,
186
- [agentName, "opencode", field],
187
- migrateValue(agent[field], "opencode"),
188
- );
189
- addDestination(sourcePath, [agentName, "pi", field], migrateValue(agent[field], "pi"));
190
- }
191
-
192
- if (Object.hasOwn(agent, "variant")) {
193
- const sourcePath = [agentName, "variant"];
194
- hasFlatKeys = true;
195
- flatPaths.push(flatFieldPath(sourcePath));
196
- sourcePathsToRemove.push(sourcePath);
197
- addDestination(sourcePath, [agentName, "opencode", "variant"], agent.variant);
198
- }
199
-
200
- if (Object.hasOwn(agent, "thinking_level")) {
201
- const sourcePath = [agentName, "thinking_level"];
202
- hasFlatKeys = true;
203
- flatPaths.push(flatFieldPath(sourcePath));
204
- sourcePathsToRemove.push(sourcePath);
205
- addDestination(sourcePath, [agentName, "pi", "thinking_level"], agent.thinking_level);
206
- }
207
- };
208
-
209
- migrateAgentFields("historian");
210
-
211
- for (const sourcePath of sourcePathsToRemove) {
212
- nextText = removeJsoncValue(nextText, sourcePath);
213
- }
214
-
215
- return {
216
- text: hasBom ? `\uFEFF${nextText}` : nextText,
217
- hasFlatKeys,
218
- diagnostics,
219
- flatPaths,
220
- };
221
- }
222
-
223
- /** True when historian fields still use the flat shape. */
224
- export function hasFlatKeys(input: Buffer | string): boolean {
225
- const text = typeof input === "string" ? input : input.toString("utf-8");
226
- return updateDocumentForFlatFields(text).hasFlatKeys;
227
- }
228
-
229
18
  /**
230
- * Deterministically maps flat model fields into per-harness blocks while leaving
231
- * unrelated JSONC regions untouched. The return type matches the caller's input
232
- * representation so callers that operate on file bytes retain BOM and line-ending
233
- * details exactly outside the surgical edits.
234
- */
235
- export function migrateFlat(input: string): string;
236
- export function migrateFlat(input: Buffer): Buffer;
237
- export function migrateFlat(input: Buffer | string): Buffer | string {
238
- const text = typeof input === "string" ? input : input.toString("utf-8");
239
- const migrated = updateDocumentForFlatFields(text).text;
240
- return typeof input === "string" ? migrated : Buffer.from(migrated, "utf-8");
241
- }
242
-
243
- export function migrateFlatDetailed(input: Buffer | string): FlatConfigMigration {
244
- const bytes = typeof input === "string" ? Buffer.from(input, "utf-8") : input;
245
- const result = updateDocumentForFlatFields(bytes.toString("utf-8"));
246
- return {
247
- bytes: Buffer.from(result.text, "utf-8"),
248
- hasFlatKeys: result.hasFlatKeys,
249
- diagnostics: result.diagnostics,
250
- };
251
- }
252
-
253
- function writeExclusiveBackup(backupPath: string, bytes: Buffer, mode: number): void {
254
- const temporaryPath = writeTemporaryCandidate(backupPath, bytes, mode);
255
- try {
256
- try {
257
- // A hard link publishes the fully written candidate without replacing a concurrent backup.
258
- linkSync(temporaryPath, backupPath);
259
- return;
260
- } catch (error) {
261
- if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
262
- }
263
-
264
- const existingBytes = readFileSync(backupPath);
265
- if (existingBytes.equals(bytes)) return;
266
-
267
- // A prefix may be a partial file left when the former direct-write approach crashed.
268
- // Preserve any other content because it may be a complete backup from an earlier run.
269
- if (bytes.subarray(0, existingBytes.length).equals(existingBytes)) {
270
- renameSync(temporaryPath, backupPath);
271
- return;
272
- }
273
- } finally {
274
- try {
275
- unlinkSync(temporaryPath);
276
- } catch {
277
- // The candidate may already be linked or renamed into place; cleanup failure must
278
- // not obscure the result of creating or validating the permanent backup.
279
- }
280
- }
281
- }
282
-
283
- function writeTemporaryCandidate(configPath: string, bytes: Buffer, mode: number): string {
284
- const directory = dirname(configPath);
285
- const stem = basename(configPath);
286
- for (let attempt = 0; attempt < 32; attempt++) {
287
- temporaryFileSequence += 1;
288
- const path = join(
289
- directory,
290
- `.${stem}.per-harness-${process.pid}-${temporaryFileSequence}.tmp`,
291
- );
292
- let descriptor: number | undefined;
293
- try {
294
- descriptor = openSync(path, "wx", mode);
295
- writeFileSync(descriptor, bytes);
296
- closeSync(descriptor);
297
- return path;
298
- } catch (error) {
299
- if (descriptor !== undefined) {
300
- try {
301
- closeSync(descriptor);
302
- } catch {
303
- // The write error below remains the actionable error.
304
- }
305
- }
306
- try {
307
- unlinkSync(path);
308
- } catch {
309
- // A failed cleanup must not hide the original write error.
310
- }
311
- if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
312
- }
313
- }
314
- throw new Error(`Could not allocate a temporary config file beside ${configPath}`);
315
- }
316
-
317
- function migrationWarning(diagnostic: FlatConfigDiagnostic): string {
318
- return diagnostic.message;
319
- }
320
-
321
- /**
322
- * Reads the raw tier before substitution and schema validation. User files are
323
- * rewritten through an exact-byte backup and same-directory atomic replacement;
324
- * project files are adapted only in memory. This is the common seam used by both
325
- * harness loaders and their doctor paths.
19
+ * Reads the raw tier before substitution and schema validation. This is a PURE
20
+ * read: Magic Context never rewrites config files. The former per-harness
21
+ * historian migration (flat model fields -> historian.opencode / historian.pi
22
+ * blocks, with a `.pre-per-harness.bak` backup and same-directory rewrite) was
23
+ * removed in favour of the flat contract, so the file is returned byte-for-byte.
24
+ * Legacy per-harness blocks are detected and rejected as a config error by the
25
+ * loaders, never rewritten.
326
26
  */
327
27
  export function loadRawConfigFile(options: RawConfigLoadOptions): RawConfigLoadResult | null {
328
28
  if (!existsSync(options.configPath)) return null;
329
29
 
330
- let observedBytes: Buffer;
30
+ let bytes: Buffer;
331
31
  try {
332
- observedBytes = readFileSync(options.configPath);
32
+ bytes = readFileSync(options.configPath);
333
33
  } catch (error) {
334
34
  throw new Error(
335
35
  `failed to read config: ${error instanceof Error ? error.message : String(error)}`,
336
36
  );
337
37
  }
338
38
 
339
- const initialMigration = migrateFlatDetailed(observedBytes);
340
- if (!initialMigration.hasFlatKeys) {
341
- return {
342
- configPath: options.configPath,
343
- bytes: observedBytes,
344
- text: observedBytes.toString("utf-8"),
345
- warnings: [],
346
- migrated: false,
347
- };
348
- }
349
-
350
- if (options.tier === "project") {
351
- return {
352
- configPath: options.configPath,
353
- bytes: initialMigration.bytes,
354
- text: initialMigration.bytes.toString("utf-8"),
355
- warnings: [
356
- "Adapted flat model config in memory; use historian.opencode/historian.pi instead. Project config files are never rewritten.",
357
- ...initialMigration.diagnostics.map(migrationWarning),
358
- ],
359
- migrated: false,
360
- };
361
- }
362
-
363
- const backupPath = `${options.configPath}${PRE_PER_HARNESS_BACKUP_SUFFIX}`;
364
- for (;;) {
365
- const migration = migrateFlatDetailed(observedBytes);
366
- if (!migration.hasFlatKeys) {
367
- return {
368
- configPath: options.configPath,
369
- bytes: observedBytes,
370
- text: observedBytes.toString("utf-8"),
371
- warnings: [],
372
- migrated: false,
373
- };
374
- }
375
-
376
- let temporaryPath: string | undefined;
377
- try {
378
- // Write through a symlinked config (stow, chezmoi, home-manager keep
379
- // it in a dotfiles repo): the temporary file is created next to the
380
- // real file and renamed onto it, so the link itself stays a link.
381
- const writePath = realpathSync(options.configPath);
382
- const mode = options.tier === "user" ? 0o600 : statSync(writePath).mode & 0o777;
383
- writeExclusiveBackup(backupPath, observedBytes, mode);
384
- temporaryPath = writeTemporaryCandidate(writePath, migration.bytes, mode);
385
- options.afterTemporaryWrite?.();
386
-
387
- const currentBytes = readFileSync(options.configPath);
388
- if (!hasFlatKeys(currentBytes)) {
389
- unlinkSync(temporaryPath);
390
- return {
391
- configPath: options.configPath,
392
- bytes: currentBytes,
393
- text: currentBytes.toString("utf-8"),
394
- warnings: [],
395
- migrated: false,
396
- };
397
- }
398
- if (!currentBytes.equals(observedBytes)) {
399
- unlinkSync(temporaryPath);
400
- observedBytes = currentBytes;
401
- continue;
402
- }
403
-
404
- renameSync(temporaryPath, writePath);
405
- return {
406
- configPath: options.configPath,
407
- bytes: migration.bytes,
408
- text: migration.bytes.toString("utf-8"),
409
- warnings: [
410
- "Migrated flat historian model config to per-harness blocks.",
411
- ...migration.diagnostics.map(migrationWarning),
412
- ],
413
- migrated: true,
414
- };
415
- } catch (error) {
416
- if (temporaryPath) {
417
- try {
418
- unlinkSync(temporaryPath);
419
- } catch {
420
- // The migration error is more useful than a failed temp cleanup.
421
- }
422
- }
423
- return {
424
- configPath: options.configPath,
425
- bytes: observedBytes,
426
- text: observedBytes.toString("utf-8"),
427
- warnings: [
428
- `Could not migrate flat model config: ${error instanceof Error ? error.message : String(error)}. Flat fields were not applied.`,
429
- ...migration.diagnostics.map(migrationWarning),
430
- ],
431
- migrated: false,
432
- };
433
- }
434
- }
39
+ return {
40
+ configPath: options.configPath,
41
+ bytes,
42
+ text: bytes.toString("utf-8"),
43
+ warnings: [],
44
+ migrated: false,
45
+ };
435
46
  }
@@ -97,7 +97,7 @@ describe("removed Phase 2 config keys", () => {
97
97
  // e2e suite is deleted with 01.A.)
98
98
  it("no docs / generated schema references the removed keys", () => {
99
99
  const docFiles = [
100
- join(REPO_ROOT, "CONFIGURATION.md"),
100
+ join(REPO_ROOT, "docs", "guides", "配置说明.md"),
101
101
  join(REPO_ROOT, "README.md"),
102
102
  join(REPO_ROOT, "assets", "magic-context.schema.json"),
103
103
  ];
@@ -2,9 +2,9 @@ import { describe, expect, it } from "bun:test";
2
2
  import {
3
3
  DEFAULT_HISTORIAN_TIMEOUT_MS,
4
4
  DEFAULT_HISTORY_BUDGET_PERCENTAGE,
5
+ HISTORIAN_MODEL_RESOLUTION_FIELDS,
5
6
  type MagicContextConfig,
6
7
  MagicContextConfigSchema,
7
- PER_HARNESS_MIGRATION_INVENTORY,
8
8
  } from "./magic-context";
9
9
 
10
10
  describe("MagicContextConfigSchema", () => {
@@ -20,7 +20,6 @@ describe("MagicContextConfigSchema", () => {
20
20
  cache_ttl: "5m",
21
21
  prompt_surface: { default: "full" },
22
22
  execute_threshold_percentage: 65,
23
- clear_reasoning_age: 50,
24
23
  history_budget_percentage: DEFAULT_HISTORY_BUDGET_PERCENTAGE,
25
24
  historian_timeout_ms: DEFAULT_HISTORIAN_TIMEOUT_MS,
26
25
  memory: {
@@ -90,7 +89,6 @@ describe("MagicContextConfigSchema", () => {
90
89
  cache_ttl: "10m",
91
90
  prompt_surface: { default: "full" },
92
91
  execute_threshold_percentage: 75,
93
- clear_reasoning_age: 60,
94
92
  history_budget_percentage: 0.2,
95
93
  historian_timeout_ms: 360_000,
96
94
  commit_cluster_trigger: {
@@ -115,7 +113,7 @@ describe("MagicContextConfigSchema", () => {
115
113
  enabled: false,
116
114
  overlay: false,
117
115
  },
118
- smart_drops: false,
116
+ protected_tools: { mmc_reduce: 3, todowrite: 1 },
119
117
  caveman_text_compression: {
120
118
  enabled: false,
121
119
  min_chars: 500,
@@ -236,32 +234,22 @@ describe("MagicContextConfigSchema", () => {
236
234
  });
237
235
 
238
236
  describe("config profiles", () => {
239
- it("admits model-selection fields and preserves per-entry harness qualifiers", () => {
237
+ it("admits flat model-selection fields and preserves per-entry qualifiers", () => {
240
238
  const result = MagicContextConfigSchema.parse({
241
239
  profile: "work",
242
240
  profiles: {
243
241
  work: {
244
242
  historian: {
245
- opencode: {
246
- model: { model: "anthropic/work-historian", variant: "high" },
247
- fallback_models: [
248
- { model: "openai/work-fallback", variant: "low" },
249
- ],
250
- variant: "high",
251
- },
252
- pi: {
253
- model: {
254
- model: "github-copilot/work-historian",
255
- thinking_level: "high",
256
- },
257
- thinking_level: "high",
258
- },
243
+ model: { model: "anthropic/work-historian", variant: "high" },
244
+ fallback_models: [{ model: "openai/work-fallback", variant: "low" }],
245
+ variant: "high",
246
+ thinking_level: "high",
259
247
  },
260
248
  },
261
249
  },
262
250
  });
263
251
 
264
- expect(result.profiles?.work?.historian?.opencode?.fallback_models).toEqual([
252
+ expect(result.profiles?.work?.historian?.fallback_models).toEqual([
265
253
  { model: "openai/work-fallback", variant: "low" },
266
254
  ]);
267
255
  });
@@ -282,7 +270,8 @@ describe("MagicContextConfigSchema", () => {
282
270
  const profilesWithExcludedFields = [
283
271
  { work: { embedding: { provider: "off" } } },
284
272
  { work: { historian: { two_pass: true } } },
285
- { work: { historian: { opencode: { prompt: "override" } } } },
273
+ { work: { historian: { prompt: "override" } } },
274
+ { work: { historian: { opencode: { model: "x/y" } } } },
286
275
  ];
287
276
 
288
277
  for (const profiles of profilesWithExcludedFields) {
@@ -291,60 +280,65 @@ describe("MagicContextConfigSchema", () => {
291
280
  });
292
281
  });
293
282
 
294
- describe("per-harness model configuration", () => {
295
- it("keeps the migration inventory exhaustive and field-specific", () => {
296
- expect(PER_HARNESS_MIGRATION_INVENTORY).toEqual({
297
- historian: {
298
- retained: [
299
- "temperature",
300
- "top_p",
301
- "prompt",
302
- "tools",
303
- "disable",
304
- "description",
305
- "mode",
306
- "color",
307
- "maxSteps",
308
- "permission",
309
- "maxTokens",
310
- "two_pass",
311
- "disallowed_tools",
312
- ],
313
- migrated_execution: ["model", "fallback_models", "variant", "thinking_level"],
314
- },
315
- });
283
+ describe("flat historian model configuration", () => {
284
+ it("declares exactly the flat model-resolution fields", () => {
285
+ expect(HISTORIAN_MODEL_RESOLUTION_FIELDS).toEqual([
286
+ "model",
287
+ "fallback_models",
288
+ "variant",
289
+ "thinking_level",
290
+ ]);
316
291
  });
317
292
 
318
- it("keeps agent metadata separate from strict harness execution blocks", () => {
293
+ it("keeps agent metadata and flat model resolution in the same historian block", () => {
319
294
  const result = MagicContextConfigSchema.parse({
320
295
  historian: {
321
296
  disable: false,
322
297
  two_pass: true,
323
298
  disallowed_tools: ["read"],
324
- opencode: {
325
- model: { model: "anthropic/claude-sonnet-4-6", variant: "high" },
326
- fallback_models: ["openai/gpt-5.4"],
327
- variant: "medium",
328
- },
329
- pi: {
330
- model: {
331
- model: "github-copilot/gpt-5.4",
332
- thinking_level: "high",
333
- },
334
- fallback_models: ["openai/gpt-5.4"],
335
- thinking_level: "medium",
336
- },
299
+ model: { model: "anthropic/claude-sonnet-4-6", variant: "high" },
300
+ fallback_models: ["openai/gpt-5.4"],
301
+ variant: "medium",
302
+ thinking_level: "high",
337
303
  },
338
304
  });
339
305
 
340
- expect(result.historian?.opencode?.fallback_models).toEqual(["openai/gpt-5.4"]);
306
+ expect(result.historian?.fallback_models).toEqual(["openai/gpt-5.4"]);
307
+ expect(result.historian?.model).toEqual({
308
+ model: "anthropic/claude-sonnet-4-6",
309
+ variant: "high",
310
+ });
311
+ expect(result.historian?.variant).toBe("medium");
312
+ expect(result.historian?.thinking_level).toBe("high");
341
313
  });
342
314
 
343
- it("rejects cross-harness vocabulary, non-array fallbacks, and execution fields at metadata depth", () => {
315
+ it("rejects the removed per-harness historian blocks instead of silently stripping them", () => {
316
+ const result = MagicContextConfigSchema.safeParse({
317
+ historian: {
318
+ opencode: { model: "x/y" },
319
+ pi: { model: "x/y" },
320
+ two_pass: true,
321
+ },
322
+ });
323
+
324
+ expect(result.success).toBe(false);
325
+ if (!result.success) {
326
+ const issue = result.error.issues.find(
327
+ (candidate) => candidate.code === "unrecognized_keys",
328
+ );
329
+ expect(issue?.path).toEqual(["historian"]);
330
+ const keys = issue && "keys" in issue ? issue.keys : [];
331
+ expect(keys).toContain("opencode");
332
+ expect(keys).toContain("pi");
333
+ }
334
+ });
335
+
336
+ it("rejects non-array fallbacks and malformed model entries", () => {
344
337
  const invalidConfigs = [
345
- { historian: { opencode: { thinking_level: "high" } } },
346
- { historian: { pi: { variant: "high" } } },
347
- { historian: { opencode: { fallback_models: "anthropic/claude-sonnet-4-6" } } },
338
+ { historian: { fallback_models: "anthropic/claude-sonnet-4-6" } },
339
+ { historian: { model: 123 } },
340
+ { historian: { model: { variant: "high" } } },
341
+ { historian: { fallback_models: [{ thinking_level: "high" }] } },
348
342
  ];
349
343
 
350
344
  for (const config of invalidConfigs) {
@@ -414,8 +408,10 @@ describe("MagicContextConfigSchema", () => {
414
408
  ).toBe(30000);
415
409
  });
416
410
 
417
- it("rejects clear_reasoning_age below minimum", () => {
418
- expect(() => MagicContextConfigSchema.parse({ clear_reasoning_age: 9 })).toThrow();
411
+ it("accepts deprecated clear_reasoning_age without applying its old validation", () => {
412
+ expect(
413
+ MagicContextConfigSchema.parse({ clear_reasoning_age: 9 }).keep_reasoning_tokens,
414
+ ).toBeUndefined();
419
415
  });
420
416
 
421
417
  it("rejects historian_timeout_ms below minimum", () => {