@retaw106/mmc-oc 1.2.3 → 2.1.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.
- package/dist/cli.js +7095 -0
- package/dist/config/index.d.ts +0 -12
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/legacy-harness-blocks.d.ts +27 -0
- package/dist/config/legacy-harness-blocks.d.ts.map +1 -0
- package/dist/config/live-run-config.d.ts.map +1 -1
- package/dist/config/live-snapshot.d.ts +2 -1
- package/dist/config/live-snapshot.d.ts.map +1 -1
- package/dist/config/project-security.d.ts +4 -5
- package/dist/config/project-security.d.ts.map +1 -1
- package/dist/config/raw-loader.d.ts +7 -26
- package/dist/config/raw-loader.d.ts.map +1 -1
- package/dist/config/schema/magic-context.d.ts +164 -259
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/config/variable.d.ts +8 -0
- package/dist/config/variable.d.ts.map +1 -1
- package/dist/config-init.js +5595 -0
- package/dist/{context-a6px04s0.js → context-vhcstgs3.js} +5758 -4727
- package/dist/{derive-budgets-fhk00zv8.js → derive-budgets-rjjz5f7y.js} +2 -2
- package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
- package/dist/features/magic-context/memory/types.d.ts +1 -0
- package/dist/features/magic-context/memory/types.d.ts.map +1 -1
- package/dist/features/magic-context/message-index.d.ts +2 -0
- package/dist/features/magic-context/message-index.d.ts.map +1 -1
- package/dist/features/magic-context/migration-v3-perf-schema.d.ts +8 -0
- package/dist/features/magic-context/migration-v3-perf-schema.d.ts.map +1 -0
- package/dist/features/magic-context/migration-v4-temporal-decisions.d.ts +12 -0
- package/dist/features/magic-context/migration-v4-temporal-decisions.d.ts.map +1 -0
- package/dist/features/magic-context/migrations.d.ts.map +1 -1
- package/dist/features/magic-context/overflow-detection.d.ts +6 -0
- package/dist/features/magic-context/overflow-detection.d.ts.map +1 -1
- package/dist/features/magic-context/reclaim-protection.d.ts +15 -10
- package/dist/features/magic-context/reclaim-protection.d.ts.map +1 -1
- package/dist/features/magic-context/session-cache-ttl.d.ts +4 -2
- package/dist/features/magic-context/session-cache-ttl.d.ts.map +1 -1
- package/dist/features/magic-context/session-project-storage.d.ts +8 -2
- package/dist/features/magic-context/session-project-storage.d.ts.map +1 -1
- package/dist/features/magic-context/storage-db.d.ts +1 -1
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage-historian-runs.d.ts +2 -0
- package/dist/features/magic-context/storage-historian-runs.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-persisted.d.ts +12 -0
- package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-session.d.ts +6 -0
- package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
- package/dist/features/magic-context/storage-ops.d.ts +2 -0
- package/dist/features/magic-context/storage-ops.d.ts.map +1 -1
- package/dist/features/magic-context/storage-session-tables.d.ts.map +1 -1
- package/dist/features/magic-context/storage-tags.d.ts +17 -4
- package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
- package/dist/features/magic-context/storage.d.ts +2 -2
- package/dist/features/magic-context/storage.d.ts.map +1 -1
- package/dist/features/magic-context/temporal-decisions.d.ts +10 -0
- package/dist/features/magic-context/temporal-decisions.d.ts.map +1 -0
- package/dist/features/magic-context/tool-definition-tokens.d.ts +3 -0
- package/dist/features/magic-context/tool-definition-tokens.d.ts.map +1 -1
- package/dist/features/magic-context/transform-decision-log.d.ts +3 -1
- package/dist/features/magic-context/transform-decision-log.d.ts.map +1 -1
- package/dist/hooks/magic-context/active-anthropic-turn.d.ts +8 -0
- package/dist/hooks/magic-context/active-anthropic-turn.d.ts.map +1 -0
- package/dist/hooks/magic-context/apply-operations.d.ts +15 -0
- package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
- package/dist/hooks/magic-context/auto-search-deadline.d.ts +10 -3
- package/dist/hooks/magic-context/auto-search-deadline.d.ts.map +1 -1
- package/dist/hooks/magic-context/auto-search-runner.d.ts +1 -1
- package/dist/hooks/magic-context/auto-search-runner.d.ts.map +1 -1
- package/dist/hooks/magic-context/caveman-cleanup.d.ts.map +1 -1
- package/dist/hooks/magic-context/channel2-cycle.d.ts +2 -0
- package/dist/hooks/magic-context/channel2-cycle.d.ts.map +1 -1
- package/dist/hooks/magic-context/compaction-marker-manager.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-render-epoch.d.ts +8 -0
- package/dist/hooks/magic-context/compartment-render-epoch.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-partial-recomp.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-types.d.ts +3 -5
- package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner.d.ts +2 -1
- package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-trigger.d.ts +3 -1
- package/dist/hooks/magic-context/compartment-trigger.d.ts.map +1 -1
- package/dist/hooks/magic-context/ctx-reduce-availability.d.ts +12 -0
- package/dist/hooks/magic-context/ctx-reduce-availability.d.ts.map +1 -1
- package/dist/hooks/magic-context/decision-calibration.d.ts +3 -0
- package/dist/hooks/magic-context/decision-calibration.d.ts.map +1 -1
- package/dist/hooks/magic-context/emergency-drop.d.ts +2 -0
- package/dist/hooks/magic-context/emergency-drop.d.ts.map +1 -1
- package/dist/hooks/magic-context/emergency-fail-closed.d.ts +21 -1
- package/dist/hooks/magic-context/emergency-fail-closed.d.ts.map +1 -1
- package/dist/hooks/magic-context/final-wire-token-estimate.d.ts +32 -1
- package/dist/hooks/magic-context/final-wire-token-estimate.d.ts.map +1 -1
- package/dist/hooks/magic-context/heuristic-cleanup.d.ts +2 -0
- package/dist/hooks/magic-context/heuristic-cleanup.d.ts.map +1 -1
- package/dist/hooks/magic-context/historian-drain-gate.d.ts +11 -0
- package/dist/hooks/magic-context/historian-drain-gate.d.ts.map +1 -0
- package/dist/hooks/magic-context/inject-compartments.d.ts +10 -8
- package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
- package/dist/hooks/magic-context/latest-assistant-turn.d.ts +25 -0
- package/dist/hooks/magic-context/latest-assistant-turn.d.ts.map +1 -0
- package/dist/hooks/magic-context/lkg-measured-request.d.ts +69 -0
- package/dist/hooks/magic-context/lkg-measured-request.d.ts.map +1 -0
- package/dist/hooks/magic-context/lkg-replay-fit.d.ts +118 -0
- package/dist/hooks/magic-context/lkg-replay-fit.d.ts.map +1 -0
- package/dist/hooks/magic-context/lkg-replay.d.ts +2 -0
- package/dist/hooks/magic-context/lkg-replay.d.ts.map +1 -1
- package/dist/hooks/magic-context/protected-tail-boundary.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-formatting.d.ts +3 -0
- package/dist/hooks/magic-context/read-session-formatting.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts.map +1 -1
- package/dist/hooks/magic-context/reasoning-budget-status.d.ts +15 -0
- package/dist/hooks/magic-context/reasoning-budget-status.d.ts.map +1 -0
- package/dist/hooks/magic-context/reasoning-budget.d.ts +40 -0
- package/dist/hooks/magic-context/reasoning-budget.d.ts.map +1 -0
- package/dist/hooks/magic-context/reasoning-removal.d.ts +6 -6
- package/dist/hooks/magic-context/reasoning-removal.d.ts.map +1 -1
- package/dist/hooks/magic-context/recomp-orchestrator.d.ts +2 -0
- package/dist/hooks/magic-context/recomp-orchestrator.d.ts.map +1 -1
- package/dist/hooks/magic-context/sentinel.d.ts +2 -0
- package/dist/hooks/magic-context/sentinel.d.ts.map +1 -1
- package/dist/hooks/magic-context/strip-content.d.ts +7 -3
- package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
- package/dist/hooks/magic-context/supersession-reclaim.d.ts +4 -0
- package/dist/hooks/magic-context/supersession-reclaim.d.ts.map +1 -1
- package/dist/hooks/magic-context/tag-messages.d.ts +13 -0
- package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
- package/dist/hooks/magic-context/tail-hygiene-walk.d.ts +5 -0
- package/dist/hooks/magic-context/tail-hygiene-walk.d.ts.map +1 -1
- package/dist/hooks/magic-context/temporal-awareness.d.ts +6 -3
- package/dist/hooks/magic-context/temporal-awareness.d.ts.map +1 -1
- package/dist/hooks/magic-context/temporal-served-projection.d.ts +4 -0
- package/dist/hooks/magic-context/temporal-served-projection.d.ts.map +1 -0
- package/dist/hooks/magic-context/tool-reclaim.d.ts +2 -0
- package/dist/hooks/magic-context/tool-reclaim.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts +3 -28
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +34 -12
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +11 -5
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/wrapup-orchestrator.d.ts.map +1 -1
- package/dist/{index-fg34g6a5.js → index-1j34a56j.js} +49 -8
- package/dist/index-33gprt9g.js +30 -0
- package/dist/{index-zx63j2e2.js → index-5cep2r9n.js} +3 -2
- package/dist/{index-5pg0acf7.js → index-amycb7b1.js} +1 -1
- package/dist/{index-wgt4gamw.js → index-dr0gepzw.js} +507 -53
- package/dist/{index-1vs6anwz.js → index-n0vnzz6h.js} +2 -1
- package/dist/{index-977gxfz0.js → index-ramz5kkm.js} +1 -1
- package/dist/{index-by6h2pav.js → index-wtb9hzk6.js} +499 -284
- package/dist/{index-3wagyrnj.js → index-x37vg474.js} +5 -1
- package/dist/{index-me3zcj5t.js → index-z2g20msd.js} +4 -8
- package/dist/index.js +1 -1
- package/dist/{message-time-backfill-5ewk0axe.js → message-time-backfill-phrwp2fz.js} +2 -2
- package/dist/{model-resolution-5haaxnpt.js → model-resolution-wazv26n0.js} +1 -1
- package/dist/plugin/rpc-handlers.d.ts +4 -0
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/{read-session-chunk-apjwrttc.js → read-session-chunk-82rkqa8j.js} +4 -4
- package/dist/{recomp-orchestrator-gyeevxaw.js → recomp-orchestrator-vc5dkzy2.js} +9 -9
- package/dist/{send-session-notification-y29y9fsr.js → send-session-notification-rnc4nrsr.js} +3 -3
- package/dist/shared/auto-search-lifecycle.d.ts +7 -0
- package/dist/shared/auto-search-lifecycle.d.ts.map +1 -0
- package/dist/shared/cache-ttl-display.d.ts +6 -4
- package/dist/shared/cache-ttl-display.d.ts.map +1 -1
- package/dist/shared/harness.d.ts +1 -1
- package/dist/shared/harness.d.ts.map +1 -1
- package/dist/shared/historian-tool-expansions.d.ts +1 -1
- package/dist/shared/historian-tool-expansions.d.ts.map +1 -1
- package/dist/shared/historian-tool-template.d.ts +2 -2
- package/dist/shared/historian-tool-template.d.ts.map +1 -1
- package/dist/shared/model-cache-ttl.d.ts +1 -1
- package/dist/shared/model-cache-ttl.d.ts.map +1 -1
- package/dist/shared/model-resolution.d.ts +6 -3
- package/dist/shared/model-resolution.d.ts.map +1 -1
- package/dist/shared/model-suggestion-retry.d.ts +2 -0
- package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
- package/dist/shared/protected-tools-policy.d.ts +6 -0
- package/dist/shared/protected-tools-policy.d.ts.map +1 -0
- package/dist/shared/provider-output-failure.d.ts +19 -0
- package/dist/shared/provider-output-failure.d.ts.map +1 -0
- package/dist/shared/sqlite.d.ts +14 -1
- package/dist/shared/sqlite.d.ts.map +1 -1
- package/dist/shared/temporal-upgrade-fixture.d.ts +19 -0
- package/dist/shared/temporal-upgrade-fixture.d.ts.map +1 -0
- package/dist/shared/window-geometry.d.ts +4 -0
- package/dist/shared/window-geometry.d.ts.map +1 -1
- package/dist/shared/write-transaction-timing.d.ts +8 -1
- package/dist/shared/write-transaction-timing.d.ts.map +1 -1
- package/dist/tools/ctx-expand/render.d.ts.map +1 -1
- package/dist/tools/ctx-reduce/tools.d.ts +2 -0
- package/dist/tools/ctx-reduce/tools.d.ts.map +1 -1
- package/dist/v2/fold/host-media.d.ts +4 -0
- package/dist/v2/fold/host-media.d.ts.map +1 -1
- package/dist/v2/fold/memory-cache.d.ts +70 -0
- package/dist/v2/fold/memory-cache.d.ts.map +1 -0
- package/dist/v2/fold/native-codec.d.ts +10 -0
- package/dist/v2/fold/native-codec.d.ts.map +1 -0
- package/dist/v2/fold/native-replay.d.ts +58 -0
- package/dist/v2/fold/native-replay.d.ts.map +1 -0
- package/dist/v2/fold/policy.d.ts +24 -0
- package/dist/v2/fold/policy.d.ts.map +1 -0
- package/dist/v2/hidden-child-native.d.ts +7 -0
- package/dist/v2/hidden-child-native.d.ts.map +1 -1
- package/dist/v2/hidden-child-record.d.ts +19 -0
- package/dist/v2/hidden-child-record.d.ts.map +1 -1
- package/dist/v2/hidden-completion.d.ts.map +1 -1
- package/dist/v2/hooks/context.d.ts.map +1 -1
- package/dist/v2/hooks/hidden-child.d.ts +46 -2
- package/dist/v2/hooks/hidden-child.d.ts.map +1 -1
- package/dist/v2/hooks/payload.d.ts.map +1 -1
- package/dist/v2/hooks/store.d.ts.map +1 -1
- package/dist/v2/hooks/tools.d.ts.map +1 -1
- package/dist/v2/hooks/types.d.ts +14 -0
- package/dist/v2/hooks/types.d.ts.map +1 -1
- package/dist/v2/server.js +13226 -11340
- package/dist/v2/store-reader.d.ts +16 -0
- package/dist/v2/store-reader.d.ts.map +1 -1
- package/dist/v2/tool-result.d.ts +8 -0
- package/dist/v2/tool-result.d.ts.map +1 -0
- package/dist/web/api.d.ts +6 -0
- package/dist/web/api.d.ts.map +1 -1
- package/dist/web/context-store.d.ts +13 -0
- package/dist/web/context-store.d.ts.map +1 -1
- package/dist/web/display-path.d.ts +28 -0
- package/dist/web/display-path.d.ts.map +1 -0
- package/dist/web/entry-args.d.ts +33 -0
- package/dist/web/entry-args.d.ts.map +1 -0
- package/dist/web/fixtures.d.ts +41 -0
- package/dist/web/fixtures.d.ts.map +1 -1
- package/dist/web/host-opencode.d.ts +9 -0
- package/dist/web/host-opencode.d.ts.map +1 -1
- package/dist/web/host-pi.d.ts +2 -0
- package/dist/web/host-pi.d.ts.map +1 -1
- package/dist/web/main.d.ts +4 -6
- package/dist/web/main.d.ts.map +1 -1
- package/dist/web/main.js +1477 -130
- package/dist/web/memories.d.ts +18 -2
- package/dist/web/memories.d.ts.map +1 -1
- package/dist/web/message-stats.d.ts +45 -0
- package/dist/web/message-stats.d.ts.map +1 -0
- package/dist/web/ui/index.html +1 -0
- package/dist/web/ui/panel.css +7 -0
- package/dist/web/ui/panel.js +332 -124
- package/dist/web/views.d.ts +73 -9
- package/dist/web/views.d.ts.map +1 -1
- package/dist/{wrapup-orchestrator-z9sqpqjf.js → wrapup-orchestrator-x9xamts2.js} +10 -9
- package/package.json +7 -2
- package/src/config/index.test.ts +257 -126
- package/src/config/index.ts +45 -39
- package/src/config/legacy-harness-blocks.test.ts +78 -0
- package/src/config/legacy-harness-blocks.ts +71 -0
- package/src/config/live-run-config.test.ts +49 -5
- package/src/config/live-run-config.ts +29 -0
- package/src/config/live-snapshot.ts +5 -1
- package/src/config/profiles.ts +1 -1
- package/src/config/project-config-trust-boundary.test.ts +2 -2
- package/src/config/project-security.hostile-matrix.test.ts +51 -65
- package/src/config/project-security.test.ts +6 -1
- package/src/config/project-security.ts +13 -12
- package/src/config/protected-tokens.test.ts +1 -1
- package/src/config/raw-loader.test.ts +29 -352
- package/src/config/raw-loader.ts +17 -406
- package/src/config/removed-config-keys.test.ts +1 -1
- package/src/config/schema/magic-context.test.ts +60 -64
- package/src/config/schema/magic-context.ts +105 -137
- package/src/config/variable.ts +21 -12
- package/src/shared/auto-search-lifecycle.ts +27 -0
- package/src/shared/cache-ttl-display.test.ts +1 -1
- package/src/shared/cache-ttl-display.ts +17 -16
- package/src/shared/harness.ts +1 -1
- package/src/shared/historian-tool-defaults.json +6 -6
- package/src/shared/historian-tool-expansions.ts +65 -8
- package/src/shared/historian-tool-preview-defaults.json +13 -0
- package/src/shared/historian-tool-template.ts +48 -14
- package/src/shared/model-cache-ttl.test.ts +2 -2
- package/src/shared/model-cache-ttl.ts +8 -4
- package/src/shared/model-resolution.test.ts +22 -32
- package/src/shared/model-resolution.ts +14 -19
- package/src/shared/model-suggestion-retry.ts +63 -7
- package/src/shared/protected-tools-policy.ts +25 -0
- package/src/shared/provider-output-failure.ts +176 -0
- package/src/shared/sqlite-acquisition-routing.test.ts +18 -0
- package/src/shared/sqlite.ts +100 -15
- package/src/shared/status-detail-text.test.ts +1 -1
- package/src/shared/status-view.test.ts +1 -1
- package/src/shared/temporal-upgrade-fixture.ts +50 -0
- package/src/shared/window-geometry.ts +10 -0
- package/src/shared/write-transaction-attribution.test.ts +2 -2
- package/src/shared/write-transaction-timing.ts +25 -1
- package/dist/features/magic-context/window-report-ledger.d.ts +0 -70
- package/dist/features/magic-context/window-report-ledger.d.ts.map +0 -1
- package/dist/index-e1q8002h.js +0 -16
- package/src/config/misplaced-agent-model.test.ts +0 -55
package/src/config/raw-loader.ts
CHANGED
|
@@ -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
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
|
30
|
+
let bytes: Buffer;
|
|
331
31
|
try {
|
|
332
|
-
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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, "
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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?.
|
|
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: {
|
|
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("
|
|
295
|
-
it("
|
|
296
|
-
expect(
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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?.
|
|
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
|
|
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: {
|
|
346
|
-
{ historian: {
|
|
347
|
-
{ historian: {
|
|
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("
|
|
418
|
-
expect(
|
|
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", () => {
|