@cortexkit/opencode-magic-context 0.49.28 → 0.49.30

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 (128) hide show
  1. package/dist/auto-search-worker.js +3 -3
  2. package/dist/chunk-embedding-write-worker.js +1 -1
  3. package/dist/config/commit-retention-recovery.d.ts +9 -0
  4. package/dist/config/commit-retention-recovery.d.ts.map +1 -0
  5. package/dist/config/index.d.ts +2 -1
  6. package/dist/config/index.d.ts.map +1 -1
  7. package/dist/config/live-child-output-cap.d.ts.map +1 -1
  8. package/dist/config/live-run-config.d.ts.map +1 -1
  9. package/dist/config/project-security.d.ts +1 -0
  10. package/dist/config/project-security.d.ts.map +1 -1
  11. package/dist/{conflict-warning-hook-tgnw019g.js → conflict-warning-hook-xgskea1v.js} +3 -3
  12. package/dist/{context-dxfztzsd.js → context-vnfr6d7t.js} +210 -50
  13. package/dist/{embedding-backfill-cwnyr4fj.js → embedding-backfill-mms9nb0h.js} +2 -2
  14. package/dist/features/magic-context/dreamer/evaluate-smart-notes.d.ts.map +1 -1
  15. package/dist/features/magic-context/dreamer/hidden-single-shot.d.ts.map +1 -1
  16. package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
  17. package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
  18. package/dist/features/magic-context/dreamer/verify-diff.d.ts.map +1 -1
  19. package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
  20. package/dist/features/magic-context/git-commits/indexer.d.ts +2 -0
  21. package/dist/features/magic-context/git-commits/indexer.d.ts.map +1 -1
  22. package/dist/features/magic-context/project-embedding-registry.d.ts +3 -1
  23. package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
  24. package/dist/features/magic-context/smart-notes/compiler.d.ts.map +1 -1
  25. package/dist/features/magic-context/work-metrics.d.ts +5 -3
  26. package/dist/features/magic-context/work-metrics.d.ts.map +1 -1
  27. package/dist/hooks/magic-context/apply-operations.d.ts +2 -0
  28. package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
  29. package/dist/hooks/magic-context/compartment-runner-types.d.ts +5 -1
  30. package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
  31. package/dist/hooks/magic-context/history-boundary-repair.d.ts +1 -1
  32. package/dist/hooks/magic-context/history-boundary-repair.d.ts.map +1 -1
  33. package/dist/hooks/magic-context/lkg-recovery-prefix.d.ts +3 -1
  34. package/dist/hooks/magic-context/lkg-recovery-prefix.d.ts.map +1 -1
  35. package/dist/hooks/magic-context/lkg-replay.d.ts.map +1 -1
  36. package/dist/hooks/magic-context/lkg-slot.d.ts +7 -0
  37. package/dist/hooks/magic-context/lkg-slot.d.ts.map +1 -1
  38. package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
  39. package/dist/hooks/magic-context/read-session-db.d.ts +3 -2
  40. package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
  41. package/dist/hooks/magic-context/replay-committed-drops.d.ts.map +1 -1
  42. package/dist/hooks/magic-context/strip-content.d.ts +1 -1
  43. package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
  44. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  45. package/dist/{index-yzjhx10y.js → index-0fe4q77z.js} +12 -6
  46. package/dist/{index-89k1w6x2.js → index-3jmx3fae.js} +2 -2
  47. package/dist/{index-7gbcg8xv.js → index-4s4zq97n.js} +1 -1
  48. package/dist/{index-31y1mznz.js → index-69gd2scx.js} +4 -4
  49. package/dist/{index-n8qmmd7f.js → index-adr4vfys.js} +3 -3
  50. package/dist/{index-k9jx7ma9.js → index-ekzzw66a.js} +328 -249
  51. package/dist/{index-tvzvjggy.js → index-gkee6p4g.js} +1 -1
  52. package/dist/{index-a2fz9hpa.js → index-gxkhzmjq.js} +4 -2
  53. package/dist/{index-4cpc59jv.js → index-hbt0sbv2.js} +1 -1
  54. package/dist/{index-dcqk0y27.js → index-k0dpkyn7.js} +139 -28
  55. package/dist/{index-t3rbb8wr.js → index-pe6b4bhs.js} +1 -1
  56. package/dist/{index-4ypqzvhf.js → index-vnn5405n.js} +1 -1
  57. package/dist/{index-dzfevacx.js → index-vtz789ex.js} +10 -6
  58. package/dist/{index-fq2p158w.js → index-vzz570j8.js} +13 -7
  59. package/dist/{index-077ht5cq.js → index-waktfbjy.js} +1 -1
  60. package/dist/{index-v0dhhxsq.js → index-y1qwzvxn.js} +5 -5
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +26 -30
  63. package/dist/{message-index-bounded-7ayzb2ah.js → message-index-bounded-zq7a5nm6.js} +3 -3
  64. package/dist/migration-worker.js +1 -1
  65. package/dist/plugin/dream-timer.d.ts.map +1 -1
  66. package/dist/plugin/embedding-bootstrap-helpers.d.ts +5 -1
  67. package/dist/plugin/embedding-bootstrap-helpers.d.ts.map +1 -1
  68. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  69. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  70. package/dist/{project-embedding-registry-94nncs5g.js → project-embedding-registry-cg281d9j.js} +1 -1
  71. package/dist/raw-ordinal-warmup-worker.js +2 -2
  72. package/dist/{read-session-chunk-jzb6w9w5.js → read-session-chunk-1ndphmb8.js} +2 -2
  73. package/dist/{recomp-orchestrator-02sa2ngr.js → recomp-orchestrator-w3pjp33d.js} +10 -10
  74. package/dist/{search-archive-q1vw7zg4.js → search-archive-sr07c6aj.js} +3 -3
  75. package/dist/{search-spq6nfyf.js → search-fbaz1m8r.js} +7 -7
  76. package/dist/{send-session-notification-afzzqy5b.js → send-session-notification-vz86nt79.js} +2 -2
  77. package/dist/shared/marker-only-text.d.ts +2 -0
  78. package/dist/shared/marker-only-text.d.ts.map +1 -0
  79. package/dist/{transform-postprocess-phase-6menyyxc.js → transform-postprocess-phase-6ccazq85.js} +10 -10
  80. package/dist/v2/auto-search-worker.js +3 -3
  81. package/dist/v2/chunk-embedding-write-worker.js +1 -1
  82. package/dist/v2/{context-0qhz1nnj.js → context-wx4sfhgr.js} +345 -102
  83. package/dist/v2/{embedding-backfill-rh5dgsva.js → embedding-backfill-35k6czrr.js} +2 -2
  84. package/dist/v2/hidden-child-cleanup.d.ts.map +1 -1
  85. package/dist/v2/hidden-child-native.d.ts.map +1 -1
  86. package/dist/v2/hidden-completion.d.ts.map +1 -1
  87. package/dist/v2/hooks/context.d.ts.map +1 -1
  88. package/dist/v2/hooks/dream-timer.d.ts.map +1 -1
  89. package/dist/v2/hooks/store.d.ts.map +1 -1
  90. package/dist/v2/{message-index-bounded-n09b74vr.js → message-index-bounded-jvgz7vk9.js} +3 -3
  91. package/dist/v2/message-times.d.ts +17 -0
  92. package/dist/v2/message-times.d.ts.map +1 -0
  93. package/dist/v2/migration-worker.js +1 -1
  94. package/dist/v2/{project-embedding-registry-6pak822h.js → project-embedding-registry-p1p10ks7.js} +1 -1
  95. package/dist/v2/raw-ordinal-warmup-worker.js +2 -2
  96. package/dist/v2/{read-session-chunk-vpt69tw9.js → read-session-chunk-djzgm4jg.js} +2 -2
  97. package/dist/v2/{recomp-orchestrator-txsmbnvw.js → recomp-orchestrator-wb28kr9k.js} +10 -10
  98. package/dist/v2/{search-archive-t9bcz4ht.js → search-archive-n3dhns4r.js} +3 -3
  99. package/dist/v2/{search-dcy9b6gd.js → search-q3jjw6zp.js} +7 -7
  100. package/dist/v2/{send-session-notification-vdsgnf4c.js → send-session-notification-x5nzy34v.js} +2 -2
  101. package/dist/v2/{server-txbc3czn.js → server-42nebt63.js} +10 -6
  102. package/dist/v2/{server-94wk1ey3.js → server-835q39kd.js} +45 -5
  103. package/dist/v2/{server-3vhxy2k5.js → server-a7x00tw8.js} +5 -5
  104. package/dist/v2/{server-e3hc93wk.js → server-d01pe8q5.js} +1 -1
  105. package/dist/v2/{server-z2n2xaeh.js → server-hwag0khr.js} +1 -1
  106. package/dist/v2/{server-jj9qye8y.js → server-k33t88h3.js} +97 -25
  107. package/dist/v2/{server-6avtp9jp.js → server-p0snacz3.js} +1 -1
  108. package/dist/v2/{server-z3mk9jdc.js → server-ph300d6m.js} +1 -1
  109. package/dist/v2/{server-6v3cdzsx.js → server-qc915kw2.js} +1 -1
  110. package/dist/v2/{server-xkvxss5g.js → server-rb8m4vry.js} +12 -6
  111. package/dist/v2/{server-zmcrvpm3.js → server-rr4cbkr0.js} +2 -2
  112. package/dist/v2/{server-yqek3s4k.js → server-zh3esb0z.js} +4 -2
  113. package/dist/v2/server.js +3 -3
  114. package/dist/v2/store-reader.d.ts +14 -2
  115. package/dist/v2/store-reader.d.ts.map +1 -1
  116. package/dist/v2/{wrapup-orchestrator-gjkszser.js → wrapup-orchestrator-2t0pdqzm.js} +10 -10
  117. package/dist/{wrapup-orchestrator-n5h5sr1x.js → wrapup-orchestrator-kjmkms5g.js} +11 -11
  118. package/package.json +1 -1
  119. package/src/config/commit-retention-recovery.ts +34 -0
  120. package/src/config/index.ts +22 -2
  121. package/src/config/live-child-output-cap.test.ts +28 -0
  122. package/src/config/live-child-output-cap.ts +2 -0
  123. package/src/config/live-run-config.test.ts +44 -1
  124. package/src/config/live-run-config.ts +8 -1
  125. package/src/config/project-config-trust-boundary.test.ts +57 -0
  126. package/src/config/project-security.ts +6 -2
  127. package/src/shared/marker-only-text.ts +8 -0
  128. package/src/shared/windows-hidden-spawns.test.ts +133 -23
@@ -0,0 +1,34 @@
1
+ import type { MagicContextConfig } from "./schema/magic-context";
2
+
3
+ type CommitIndexing = MagicContextConfig["memory"]["git_commit_indexing"];
4
+ const pausedRetention = new WeakSet<CommitIndexing>();
5
+ const RETENTION_FIELD = "memory.git_commit_indexing.max_commits";
6
+
7
+ /** Keep a defaulted cap useful for bounded reads without letting it delete stored history. */
8
+ export function applyCommitRetentionRecovery(
9
+ config: MagicContextConfig,
10
+ recoveredFieldPaths: readonly string[],
11
+ warnings: string[],
12
+ ): void {
13
+ if (
14
+ !recoveredFieldPaths.some(
15
+ (path) => RETENTION_FIELD === path || RETENTION_FIELD.startsWith(`${path}.`),
16
+ )
17
+ )
18
+ return;
19
+ pausedRetention.add(config.memory.git_commit_indexing);
20
+ const message = `${RETENTION_FIELD}: retention eviction is paused until the invalid value is fixed.`;
21
+ const index = warnings.findIndex((warning) => warning.includes(RETENTION_FIELD));
22
+ if (index >= 0) warnings[index] = `${warnings[index]} ${message}`;
23
+ else warnings.push(message);
24
+ }
25
+
26
+ export function isCommitRetentionPaused(settings: CommitIndexing): boolean {
27
+ return pausedRetention.has(settings);
28
+ }
29
+
30
+ /** Live sampling copies settings objects; recovery state must follow the sampled cap. */
31
+ export function copyCommitRetentionRecovery(from: CommitIndexing, to: CommitIndexing): void {
32
+ if (pausedRetention.has(from)) pausedRetention.add(to);
33
+ else pausedRetention.delete(to);
34
+ }
@@ -15,6 +15,7 @@ import { log } from "../shared/logger";
15
15
  import { setOutputReserveConfig } from "../shared/models-dev-cache";
16
16
  import type { PromptSurfaceConfig } from "../shared/prompt-surface";
17
17
  import { isCompactionEnabled, migrateLegacyAgentEnabledInMemory } from "./agent-disable";
18
+ import { applyCommitRetentionRecovery } from "./commit-retention-recovery";
18
19
  import { warnDeprecatedMuralModel, warnDeprecatedWindowOverlayPath } from "./deprecated-config";
19
20
  import {
20
21
  cortexKitProjectConfigBasePath,
@@ -121,6 +122,7 @@ export interface LoadResultDetailed {
121
122
  };
122
123
  substitutionFailures: Array<{ keyPath: string; source: "user" | "project"; message: string }>;
123
124
  recoveredTopLevelKeys: string[];
125
+ recoveredFieldPaths: string[];
124
126
  configParseFailures: ConfigParseFailure[];
125
127
  warningDetails: ConfigWarningDetail[];
126
128
  cacheTtlConfigured: boolean;
@@ -410,6 +412,7 @@ export function warnProtectedTagsDeprecationOnce(): void {
410
412
  export function parsePluginConfig(
411
413
  rawConfig: Record<string, unknown>,
412
414
  recoveredTopLevelKeys: string[] = [],
415
+ recoveredFieldPaths: string[] = [],
413
416
  ): MagicContextPluginConfig & { configWarnings?: string[] } {
414
417
  // Pre-Zod shim: reshape legacy experimental.* graduated keys so the user's
415
418
  // opt-in/out state survives upgrades even when they never run `doctor`.
@@ -543,6 +546,7 @@ export function parsePluginConfig(
543
546
  }
544
547
  if (prunedLeaves.length === issuePaths.length) {
545
548
  patched[key] = prunedBlock;
549
+ recoveredFieldPaths.push(...prunedLeaves.map((leaf) => `${key}.${leaf}`));
546
550
  const reason = customMessagesByKey.get(key);
547
551
  warnings.push(
548
552
  `"${key}": invalid nested field(s) ${prunedLeaves.map((leaf) => `"${key}.${leaf}"`).join(", ")}, using defaults for those.${reason ? ` ${reason}` : ""}`,
@@ -553,6 +557,7 @@ export function parsePluginConfig(
553
557
 
554
558
  // Root-level or unreachable agent errors cannot be repaired safely because
555
559
  // guessing a model configuration could select an expensive unintended model.
560
+ recoveredFieldPaths.push(key);
556
561
  if (isAgentConfig) {
557
562
  delete patched[key];
558
563
  warnings.push(
@@ -609,6 +614,13 @@ export function parsePluginConfig(
609
614
  // If even the patched version fails (shouldn't happen), fall back to full defaults
610
615
  // but keep enabled:true — the user intended to use the plugin.
611
616
  warnings.push("Config recovery failed, using all defaults.");
617
+ recoveredFieldPaths.push(
618
+ "embedding",
619
+ "memory",
620
+ "subc",
621
+ "shadow_embedding",
622
+ "allow_home_project",
623
+ );
612
624
  return {
613
625
  ...defaults,
614
626
  disabled_hooks: disabledHooks,
@@ -819,6 +831,7 @@ export function loadPluginConfigDetailed(
819
831
  // way that forces extra historian work on the user's account.
820
832
  const trustedBaseConfig = parsePluginConfig(trustedProfiledRaw);
821
833
  let projectRestoredTopLevelKeys: string[] = [];
834
+ let projectRestoredFieldPaths: string[] = [];
822
835
 
823
836
  if (projectLoaded) {
824
837
  mergedRaw = deepMergeRawConfig(mergedRaw, profileResolution.projectBase);
@@ -846,6 +859,7 @@ export function loadPluginConfigDetailed(
846
859
  allWarnings.push(`[project config] ${warning}`);
847
860
  }
848
861
  projectRestoredTopLevelKeys = restoredOverProject.restoredTopLevelKeys;
862
+ projectRestoredFieldPaths = restoredOverProject.restoredFieldPaths;
849
863
  for (const warning of constrainProjectCommands({
850
864
  mergedRaw,
851
865
  trustedRaw: trustedProfiledRaw,
@@ -857,8 +871,10 @@ export function loadPluginConfigDetailed(
857
871
  }
858
872
 
859
873
  const recoveredTopLevelKeys: string[] = [];
874
+ const recoveredFieldPaths: string[] = [];
860
875
  const cacheTtlConfigured = Object.hasOwn(mergedRaw, "cache_ttl");
861
- const config = parsePluginConfig(mergedRaw, recoveredTopLevelKeys);
876
+ const config = parsePluginConfig(mergedRaw, recoveredTopLevelKeys, recoveredFieldPaths);
877
+ recoveredFieldPaths.push(...projectRestoredFieldPaths);
862
878
  // An ignored invalid project value is still a config the user must fix, so
863
879
  // keep reporting it as schema recovery (live reload keeps the last good
864
880
  // config on that outcome, as it did before the user's value was restored).
@@ -873,7 +889,10 @@ export function loadPluginConfigDetailed(
873
889
  if (applyRuntimeGlobals) {
874
890
  setOutputReserveConfig(config.output_reserve);
875
891
  }
876
- const leafValidationWarnings = [...(config.configWarnings ?? [])];
892
+ const recoveryWarnings = config.configWarnings ?? [];
893
+ applyCommitRetentionRecovery(config, recoveredFieldPaths, recoveryWarnings);
894
+ if (recoveryWarnings.length > 0) config.configWarnings = recoveryWarnings;
895
+ const leafValidationWarnings = [...recoveryWarnings];
877
896
  if (config.configWarnings?.length) {
878
897
  allWarnings.push(
879
898
  ...config.configWarnings.map((w) => {
@@ -933,6 +952,7 @@ export function loadPluginConfigDetailed(
933
952
  sources,
934
953
  substitutionFailures,
935
954
  recoveredTopLevelKeys,
955
+ recoveredFieldPaths,
936
956
  configParseFailures,
937
957
  warningDetails,
938
958
  cacheTtlConfigured,
@@ -1,4 +1,5 @@
1
1
  import { expect, test } from "bun:test";
2
+ import type { HiddenCompletionExecutor } from "../hooks/magic-context/compartment-runner-types";
2
3
 
3
4
  import { producerInputTokenLimit } from "../hooks/magic-context/producer-window-guard";
4
5
  import {
@@ -53,6 +54,33 @@ test("OpenCode 2 dreamer passes a fresh output cap to each hidden child", async
53
54
  expect(received).toEqual([4096, 8192]);
54
55
  });
55
56
 
57
+ test("OpenCode 2 dreamer output-cap wrapper forwards partial output reads", async () => {
58
+ const boot = MagicContextConfigSchema.parse({});
59
+ const reads: Array<{ id: string; limit: number }> = [];
60
+ const executor: HiddenCompletionExecutor = {
61
+ capabilities: { tools: true, harness: "opencode2" },
62
+ open: async () => ({ id: "child" }),
63
+ attempt: async () => {},
64
+ collect: async () => {
65
+ throw new Error("no settled completion");
66
+ },
67
+ collectPartial: async (handle, limit) => {
68
+ reads.push({ id: handle.id, limit });
69
+ return {
70
+ text: "unfinished manifest",
71
+ usage: { input: 101, output: 11, cacheRead: 7, cacheWrite: 5 },
72
+ lengthCapped: false,
73
+ };
74
+ },
75
+ close: async () => {},
76
+ };
77
+ const wrapped = withLiveDreamerOutputCap(executor, boot, () => boot);
78
+ expect((await wrapped.collectPartial?.({ id: "child" }, 200))?.text).toBe(
79
+ "unfinished manifest",
80
+ );
81
+ expect(reads).toEqual([{ id: "child", limit: 200 }]);
82
+ });
83
+
56
84
  test("OpenCode 1 historian keeps the sampled reserve and child output cap", () => {
57
85
  const boot = MagicContextConfigSchema.parse({ historian: { maxTokens: 2048 } });
58
86
  let current = MagicContextConfigSchema.parse({ historian: { maxTokens: 4096 } });
@@ -29,6 +29,8 @@ export function withLiveDreamerOutputCap<T extends MagicContextConfig>(
29
29
  }),
30
30
  attempt: (handle, request) => executor.attempt(handle, request),
31
31
  collect: (handle, limit) => executor.collect(handle, limit),
32
+ collectPartial: (handle, limit) =>
33
+ executor.collectPartial?.(handle, limit) ?? executor.collect(handle, limit),
32
34
  close: (handle, settlement) => executor.close(handle, settlement),
33
35
  };
34
36
  }
@@ -9,10 +9,53 @@ import {
9
9
  import { producerInputTokenLimit } from "../hooks/magic-context/producer-window-guard";
10
10
  import { resolveDreamerTaskModel, resolveHistorianModel } from "../shared/model-resolution";
11
11
  import { createTestTempDirFromPath } from "../shared/test-temp-dir";
12
+ import { isCommitRetentionPaused } from "./commit-retention-recovery";
12
13
  import { loadPluginConfigDetailed } from "./index";
13
- import { dreamerRunConfig, historianRunConfig } from "./live-run-config";
14
+ import { changedLiveKeys, dreamerRunConfig, historianRunConfig } from "./live-run-config";
14
15
  import { LiveConfigReader } from "./live-snapshot";
15
16
 
17
+ test("dreamer retains the recovered-cap pause through live copies and clears it when the same cap is fixed", () => {
18
+ const root = createTestTempDirFromPath(join(tmpdir(), "mc-live-retention-"));
19
+ const previous = process.env.XDG_CONFIG_HOME;
20
+ process.env.XDG_CONFIG_HOME = root;
21
+ const directory = join(root, "project");
22
+ const file = join(root, "cortexkit", "magic-context.jsonc");
23
+ mkdirSync(join(root, "cortexkit"), { recursive: true });
24
+ mkdirSync(directory, { recursive: true });
25
+ const writeCap = (max_commits: number) =>
26
+ writeFileSync(
27
+ file,
28
+ JSON.stringify({ memory: { git_commit_indexing: { enabled: true, max_commits } } }),
29
+ );
30
+ try {
31
+ writeCap(0);
32
+ const boot = loadPluginConfigDetailed(directory, false).config;
33
+ const reader = new LiveConfigReader(
34
+ directory,
35
+ boot,
36
+ () => {
37
+ const loaded = loadPluginConfigDetailed(directory, false);
38
+ if (loaded.loadOutcome === "schema-recovery") throw new Error("invalid config");
39
+ return loaded.config;
40
+ },
41
+ () => {},
42
+ changedLiveKeys,
43
+ );
44
+ const initial = dreamerRunConfig(boot, reader.poll().effective);
45
+ expect(initial.memory.git_commit_indexing.max_commits).toBe(2000);
46
+ expect(isCommitRetentionPaused(initial.memory.git_commit_indexing)).toBe(true);
47
+ writeCap(2000);
48
+ const fixed = dreamerRunConfig(boot, reader.poll().effective);
49
+ expect(fixed.memory.git_commit_indexing.max_commits).toBe(2000);
50
+ expect(isCommitRetentionPaused(fixed.memory.git_commit_indexing)).toBe(false);
51
+ expect(isCommitRetentionPaused(initial.memory.git_commit_indexing)).toBe(true);
52
+ } finally {
53
+ if (previous === undefined) delete process.env.XDG_CONFIG_HOME;
54
+ else process.env.XDG_CONFIG_HOME = previous;
55
+ rmSync(root, { recursive: true, force: true });
56
+ }
57
+ });
58
+
16
59
  for (const host of ["OC1", "OC2", "Pi", "OMP"] as const) {
17
60
  test(`${host} historian samples the next generation while an existing run keeps its model and fallback`, () => {
18
61
  const root = createTestTempDirFromPath(join(tmpdir(), "mc-live-historian-"));
@@ -1,4 +1,5 @@
1
1
  import { log } from "../shared/logger";
2
+ import { copyCommitRetentionRecovery } from "./commit-retention-recovery";
2
3
  import { loadPluginConfigDetailed, type MagicContextPluginConfig } from "./index";
3
4
  import { LiveConfigReader } from "./live-snapshot";
4
5
  import { LIVE_RELOAD_CONFIG_PATHS, type MagicContextConfig } from "./schema/magic-context";
@@ -32,7 +33,13 @@ export function sampleLiveConfig<T extends MagicContextConfig>(boot: T, fresh: T
32
33
  const leaf = parts.at(-1);
33
34
  if (leaf !== undefined) target[leaf] = value;
34
35
  }
35
- return result as T;
36
+ const sampled = result as T;
37
+ const freshCommitIndexing = fresh.memory?.git_commit_indexing;
38
+ const sampledCommitIndexing = sampled.memory?.git_commit_indexing;
39
+ if (freshCommitIndexing && sampledCommitIndexing) {
40
+ copyCommitRetentionRecovery(freshCommitIndexing, sampledCommitIndexing);
41
+ }
42
+ return sampled;
36
43
  }
37
44
 
38
45
  function removeLeaf(root: Record<string, unknown>, parts: string[]): void {
@@ -2,7 +2,9 @@ import { describe, expect, it } from "bun:test";
2
2
  import { mkdirSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
+ import { isConfigLoadUntrusted } from "../plugin/embedding-bootstrap-helpers";
5
6
  import { createTestTempDirFromPath } from "../shared/test-temp-dir";
7
+ import { isCommitRetentionPaused } from "./commit-retention-recovery";
6
8
  import { loadPluginConfigDetailed } from "./index";
7
9
 
8
10
  /**
@@ -38,6 +40,61 @@ function loadUserAndProject(user: unknown, project: unknown) {
38
40
  }
39
41
 
40
42
  describe("project config cannot reset user settings with a wrong-typed parent block", () => {
43
+ it("pauses retention eviction for a recovered commit cap without making memory untrusted", () => {
44
+ const loaded = loadUserAndProject(
45
+ { memory: { git_commit_indexing: { enabled: true, max_commits: 0 } } },
46
+ {},
47
+ );
48
+ expect(loaded.config.memory.git_commit_indexing.max_commits).toBe(2000);
49
+ expect(isCommitRetentionPaused(loaded.config.memory.git_commit_indexing)).toBe(true);
50
+ expect(isConfigLoadUntrusted(loaded)).toBe(false);
51
+ expect(
52
+ loaded.config.configWarnings?.filter((warning) =>
53
+ warning.includes("retention eviction is paused"),
54
+ ),
55
+ ).toHaveLength(1);
56
+ expect(loaded.config.configWarnings?.join("\n")).toContain(
57
+ "memory.git_commit_indexing.max_commits",
58
+ );
59
+ expect(
60
+ loaded.warningDetails.filter((warning) =>
61
+ warning.message.includes("retention eviction is paused"),
62
+ ),
63
+ ).toHaveLength(1);
64
+ const valid = loadUserAndProject(
65
+ { memory: { git_commit_indexing: { enabled: true, max_commits: 100 } } },
66
+ {},
67
+ );
68
+ expect(isCommitRetentionPaused(valid.config.memory.git_commit_indexing)).toBe(false);
69
+ });
70
+
71
+ it("recovers an out-of-range injection budget without making memory untrusted", () => {
72
+ const loaded = loadUserAndProject({}, { memory: { injection_budget_tokens: 20001 } });
73
+ expect(loaded.config.memory.enabled).toBe(true);
74
+ expect(loaded.config.memory.injection_budget_tokens).toBe(4000);
75
+ expect(loaded.recoveredFieldPaths).toEqual(["memory.injection_budget_tokens"]);
76
+ expect(loaded.config.configWarnings?.join("\n")).toContain(
77
+ "memory.injection_budget_tokens",
78
+ );
79
+ expect(isConfigLoadUntrusted(loaded)).toBe(false);
80
+ });
81
+
82
+ it("records the exact restored project leaf without distrusting a plain threshold", () => {
83
+ const loaded = loadUserAndProject(
84
+ { memory: { auto_search: { score_threshold: 0.7 } } },
85
+ { memory: { auto_search: { score_threshold: "invalid" } } },
86
+ );
87
+ expect(loaded.config.memory.auto_search.score_threshold).toBe(0.7);
88
+ expect(loaded.recoveredFieldPaths).toEqual(["memory.auto_search.score_threshold"]);
89
+ expect(isConfigLoadUntrusted(loaded)).toBe(false);
90
+ });
91
+
92
+ it("keeps an invalid embedding model untrusted after field recovery", () => {
93
+ const loaded = loadUserAndProject({ embedding: { model: 123 } }, {});
94
+ expect(loaded.recoveredFieldPaths).toEqual(["embedding.model"]);
95
+ expect(isConfigLoadUntrusted(loaded)).toBe(true);
96
+ });
97
+
41
98
  it("keeps the user's compaction block when the project sets compaction:false", () => {
42
99
  const loaded = loadUserAndProject(
43
100
  { compaction: { enabled: false } },
@@ -973,7 +973,7 @@ export function restoreTrustedValuesOverInvalidProjectValues(args: {
973
973
  trustedRaw: Record<string, unknown>;
974
974
  projectRaw: Record<string, unknown>;
975
975
  collectIssuePaths: (raw: Record<string, unknown>) => readonly ConfigIssuePath[];
976
- }): { warnings: string[]; restoredTopLevelKeys: string[] } {
976
+ }): { warnings: string[]; restoredTopLevelKeys: string[]; restoredFieldPaths: string[] } {
977
977
  const warnings: string[] = [];
978
978
  const restoredTopLevelKeys = new Set<string>();
979
979
  const restored = new Set<string>();
@@ -1000,7 +1000,11 @@ export function restoreTrustedValuesOverInvalidProjectValues(args: {
1000
1000
  if (!changed) break;
1001
1001
  }
1002
1002
 
1003
- return { warnings, restoredTopLevelKeys: [...restoredTopLevelKeys] };
1003
+ return {
1004
+ warnings,
1005
+ restoredTopLevelKeys: [...restoredTopLevelKeys],
1006
+ restoredFieldPaths: [...restored],
1007
+ };
1004
1008
  }
1005
1009
 
1006
1010
  const PROJECT_COMMAND_STRING_FIELDS = ["description", "agent", "model"] as const;
@@ -0,0 +1,8 @@
1
+ // Models sometimes imitate dropped placeholders with a blank or bare numeric tag.
2
+ // Match only complete marker-only text, never prose that mentions a placeholder.
3
+ const MARKER_ONLY_PATTERN = /^(?:(?:§\d+§|\[dropped\s*(?:\d+|§\d*§)?\]|\[cleared\])\s*)+$/;
4
+
5
+ export function isMarkerOnlyText(text: string): boolean {
6
+ const trimmed = text.trim();
7
+ return trimmed.length > 0 && MARKER_ONLY_PATTERN.test(trimmed);
8
+ }
@@ -30,21 +30,28 @@ function sourceFiles(dir: string): string[] {
30
30
  function missingHiddenOptions(path: string, text = readFileSync(path, "utf8")): string[] {
31
31
  if (!/node:child_process|Bun\.spawn/.test(text)) return [];
32
32
  const source = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true);
33
- const imports = new Set<string>();
33
+ const imports = new Map<string, string>();
34
+ const promisifyImports = new Set<string>();
35
+ const utilImports = new Set<string>();
34
36
  const variables = new Map<string, ts.Expression>();
35
37
  for (const statement of source.statements) {
36
- if (
37
- ts.isImportDeclaration(statement) &&
38
- statement.moduleSpecifier.getText(source).includes("node:child_process")
39
- ) {
40
- const bindings = statement.importClause?.namedBindings;
41
- if (bindings && ts.isNamedImports(bindings)) {
42
- for (const element of bindings.elements) {
43
- if (subprocessNames.has(element.propertyName?.text ?? element.name.text))
44
- imports.add(element.name.text);
45
- }
38
+ if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier))
39
+ continue;
40
+ const module = statement.moduleSpecifier.text;
41
+ const bindings = statement.importClause?.namedBindings;
42
+ if (bindings && ts.isNamedImports(bindings)) {
43
+ for (const element of bindings.elements) {
44
+ const imported = element.propertyName?.text ?? element.name.text;
45
+ if (module === "node:child_process" && subprocessNames.has(imported))
46
+ imports.set(element.name.text, imported);
47
+ if ((module === "node:util" || module === "util") && imported === "promisify")
48
+ promisifyImports.add(element.name.text);
46
49
  }
47
50
  }
51
+ if (module === "node:util" || module === "util") {
52
+ if (bindings && ts.isNamespaceImport(bindings)) utilImports.add(bindings.name.text);
53
+ if (statement.importClause?.name) utilImports.add(statement.importClause.name.text);
54
+ }
48
55
  }
49
56
  function collect(node: ts.Node): void {
50
57
  if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer)
@@ -52,6 +59,44 @@ function missingHiddenOptions(path: string, text = readFileSync(path, "utf8")):
52
59
  ts.forEachChild(node, collect);
53
60
  }
54
61
  collect(source);
62
+ function isUtil(node: ts.Expression, seen = new Set<string>()): boolean {
63
+ if (ts.isParenthesizedExpression(node)) return isUtil(node.expression, seen);
64
+ if (!ts.isIdentifier(node)) return false;
65
+ if (utilImports.has(node.text)) return true;
66
+ if (seen.has(node.text)) return false;
67
+ seen.add(node.text);
68
+ const initializer = variables.get(node.text);
69
+ return initializer ? isUtil(initializer, seen) : false;
70
+ }
71
+ function isPromisify(node: ts.Expression, seen = new Set<string>()): boolean {
72
+ if (ts.isParenthesizedExpression(node)) return isPromisify(node.expression, seen);
73
+ if (ts.isIdentifier(node)) {
74
+ if (promisifyImports.has(node.text)) return true;
75
+ if (seen.has(node.text)) return false;
76
+ seen.add(node.text);
77
+ const initializer = variables.get(node.text);
78
+ return initializer ? isPromisify(initializer, seen) : false;
79
+ }
80
+ return (
81
+ ts.isPropertyAccessExpression(node) &&
82
+ isUtil(node.expression) &&
83
+ node.name.text === "promisify"
84
+ );
85
+ }
86
+ function subprocess(node: ts.Expression, seen = new Set<string>()): string | undefined {
87
+ if (ts.isParenthesizedExpression(node)) return subprocess(node.expression, seen);
88
+ if (ts.isIdentifier(node)) {
89
+ const imported = imports.get(node.text);
90
+ if (imported) return imported;
91
+ if (seen.has(node.text)) return undefined;
92
+ seen.add(node.text);
93
+ const initializer = variables.get(node.text);
94
+ return initializer ? subprocess(initializer, seen) : undefined;
95
+ }
96
+ if (ts.isCallExpression(node) && isPromisify(node.expression) && node.arguments[0])
97
+ return subprocess(node.arguments[0], seen);
98
+ return undefined;
99
+ }
55
100
  function hidden(node: ts.Expression | undefined, seen = new Set<string>()): boolean {
56
101
  if (!node) return false;
57
102
  if (ts.isIdentifier(node)) {
@@ -90,39 +135,49 @@ function missingHiddenOptions(path: string, text = readFileSync(path, "utf8")):
90
135
  if (ts.isCallExpression(node)) {
91
136
  const expression = node.expression;
92
137
  let name: string | undefined;
138
+ // Track the original API, not the alias name: exec takes options at index 1,
139
+ // while execFile and spawn take them at index 2, even after promisify.
140
+ let kind = subprocess(expression);
141
+ if (kind) name = expression.getText(source);
93
142
  if (
143
+ !kind &&
94
144
  ts.isIdentifier(expression) &&
95
- (imports.has(expression.text) ||
96
- [
97
- "execFileAsync",
98
- "execFileSyncForIdentity",
99
- "rpcIdentityExecFileSync",
100
- "execFileForVerificationPaths",
101
- ].includes(expression.text) ||
145
+ ([
146
+ "execFileAsync",
147
+ "execFileSyncForIdentity",
148
+ "rpcIdentityExecFileSync",
149
+ "execFileForVerificationPaths",
150
+ ].includes(expression.text) ||
102
151
  (expression.text === "exec" && path.endsWith("rpc-utils.ts")))
103
- )
152
+ ) {
104
153
  name = expression.text;
154
+ kind = "execFile";
155
+ }
105
156
  if (ts.isPropertyAccessExpression(expression)) {
106
157
  const owner = expression.expression.getText(source);
107
158
  if (
108
159
  owner === "Bun" &&
109
160
  (expression.name.text === "spawn" || expression.name.text === "spawnSync")
110
- )
161
+ ) {
111
162
  name = "Bun.spawn";
163
+ kind = "Bun.spawn";
164
+ }
112
165
  if (
113
166
  (owner === "deps" || owner === "this") &&
114
167
  (subprocessNames.has(expression.name.text) ||
115
168
  expression.name.text === "spawnImpl")
116
- )
169
+ ) {
117
170
  name = expression.name.text;
171
+ kind = expression.name.text;
172
+ }
118
173
  }
119
174
  if (name) {
120
175
  const index =
121
- name === "Bun.spawn"
176
+ kind === "Bun.spawn"
122
177
  ? node.arguments.length === 1
123
178
  ? 0
124
179
  : 1
125
- : name === "execSync" || (name === "exec" && !path.endsWith("rpc-utils.ts"))
180
+ : kind === "execSync" || kind === "exec"
126
181
  ? 1
127
182
  : 2;
128
183
  // A single-argument Bun.spawnSync({cmd, ...}) passes options as its first argument.
@@ -158,6 +213,61 @@ describe("Windows child processes", () => {
158
213
  expect(failures).toEqual([]);
159
214
  });
160
215
 
216
+ test.each([
217
+ ["execFile", "[],"],
218
+ ["exec", ""],
219
+ ["spawn", "[],"],
220
+ ])("follows promisified %s aliases and checks the original options position", (api, args) => {
221
+ const path = join(root, "packages/plugin/src/shared/promisified-fixture.ts");
222
+ const text = `import { ${api} as childProcess } from "node:child_process";
223
+ import { promisify as toPromise } from "node:util";
224
+ const rawAlias = childProcess;
225
+ const promiseAlias = toPromise;
226
+ const exec = promiseAlias(rawAlias);
227
+ const run = exec;
228
+ run("git", ${args} { windowsHide: true });
229
+ run("git", ${args} { windowsHide: false });
230
+ run("git", ${args} {});`;
231
+ expect(missingHiddenOptions(path, text)).toEqual([
232
+ "packages/plugin/src/shared/promisified-fixture.ts:8 run",
233
+ "packages/plugin/src/shared/promisified-fixture.ts:9 run",
234
+ ]);
235
+ });
236
+
237
+ test.each([
238
+ 'import { promisify } from "node:util";',
239
+ 'import * as util from "node:util"; const promisify = util.promisify;',
240
+ 'import util from "util"; const promisify = util.promisify;',
241
+ 'import * as util from "node:util"; const u = util; const promisify = u.promisify;',
242
+ ])("recognizes promisify bindings: %s", (utilImport) => {
243
+ const path = join(root, "packages/plugin/src/shared/promisified-fixture.ts");
244
+ const text = `import { execFile } from "node:child_process";
245
+ ${utilImport}
246
+ const exec = promisify(execFile);
247
+ exec("git", [], { windowsHide: true });
248
+ exec("git", [], {});`;
249
+ expect(missingHiddenOptions(path, text)).toEqual([
250
+ "packages/plugin/src/shared/promisified-fixture.ts:5 exec",
251
+ ]);
252
+ });
253
+
254
+ test("recognizes direct util.promisify calls and does not trust options in the wrong slot", () => {
255
+ const path = join(root, "packages/plugin/src/shared/promisified-fixture.ts");
256
+ const text = `import { execFile, exec, spawn } from "node:child_process";
257
+ import * as util from "node:util";
258
+ const file = util.promisify(execFile);
259
+ const shell = util.promisify(exec);
260
+ const child = util.promisify(spawn);
261
+ file("git", { windowsHide: true }, {});
262
+ shell("git", {}, { windowsHide: true });
263
+ child("git", { windowsHide: true }, {});`;
264
+ expect(missingHiddenOptions(path, text)).toEqual([
265
+ "packages/plugin/src/shared/promisified-fixture.ts:6 file",
266
+ "packages/plugin/src/shared/promisified-fixture.ts:7 shell",
267
+ "packages/plugin/src/shared/promisified-fixture.ts:8 child",
268
+ ]);
269
+ });
270
+
161
271
  test("another dashboard subprocess still requires windowsHide true", () => {
162
272
  const path = join(root, "packages/cli/src/lib/dashboard-install.ts");
163
273
  const text = `import { execFile } from "node:child_process";