@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.
- package/dist/auto-search-worker.js +3 -3
- package/dist/chunk-embedding-write-worker.js +1 -1
- package/dist/config/commit-retention-recovery.d.ts +9 -0
- package/dist/config/commit-retention-recovery.d.ts.map +1 -0
- package/dist/config/index.d.ts +2 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/live-child-output-cap.d.ts.map +1 -1
- package/dist/config/live-run-config.d.ts.map +1 -1
- package/dist/config/project-security.d.ts +1 -0
- package/dist/config/project-security.d.ts.map +1 -1
- package/dist/{conflict-warning-hook-tgnw019g.js → conflict-warning-hook-xgskea1v.js} +3 -3
- package/dist/{context-dxfztzsd.js → context-vnfr6d7t.js} +210 -50
- package/dist/{embedding-backfill-cwnyr4fj.js → embedding-backfill-mms9nb0h.js} +2 -2
- package/dist/features/magic-context/dreamer/evaluate-smart-notes.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/hidden-single-shot.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/verify-diff.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
- package/dist/features/magic-context/git-commits/indexer.d.ts +2 -0
- package/dist/features/magic-context/git-commits/indexer.d.ts.map +1 -1
- package/dist/features/magic-context/project-embedding-registry.d.ts +3 -1
- package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/compiler.d.ts.map +1 -1
- package/dist/features/magic-context/work-metrics.d.ts +5 -3
- package/dist/features/magic-context/work-metrics.d.ts.map +1 -1
- package/dist/hooks/magic-context/apply-operations.d.ts +2 -0
- package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-types.d.ts +5 -1
- package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
- package/dist/hooks/magic-context/history-boundary-repair.d.ts +1 -1
- package/dist/hooks/magic-context/history-boundary-repair.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-recovery-prefix.d.ts +3 -1
- package/dist/hooks/magic-context/lkg-recovery-prefix.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-replay.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-slot.d.ts +7 -0
- package/dist/hooks/magic-context/lkg-slot.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-db.d.ts +3 -2
- package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
- package/dist/hooks/magic-context/replay-committed-drops.d.ts.map +1 -1
- package/dist/hooks/magic-context/strip-content.d.ts +1 -1
- package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/{index-yzjhx10y.js → index-0fe4q77z.js} +12 -6
- package/dist/{index-89k1w6x2.js → index-3jmx3fae.js} +2 -2
- package/dist/{index-7gbcg8xv.js → index-4s4zq97n.js} +1 -1
- package/dist/{index-31y1mznz.js → index-69gd2scx.js} +4 -4
- package/dist/{index-n8qmmd7f.js → index-adr4vfys.js} +3 -3
- package/dist/{index-k9jx7ma9.js → index-ekzzw66a.js} +328 -249
- package/dist/{index-tvzvjggy.js → index-gkee6p4g.js} +1 -1
- package/dist/{index-a2fz9hpa.js → index-gxkhzmjq.js} +4 -2
- package/dist/{index-4cpc59jv.js → index-hbt0sbv2.js} +1 -1
- package/dist/{index-dcqk0y27.js → index-k0dpkyn7.js} +139 -28
- package/dist/{index-t3rbb8wr.js → index-pe6b4bhs.js} +1 -1
- package/dist/{index-4ypqzvhf.js → index-vnn5405n.js} +1 -1
- package/dist/{index-dzfevacx.js → index-vtz789ex.js} +10 -6
- package/dist/{index-fq2p158w.js → index-vzz570j8.js} +13 -7
- package/dist/{index-077ht5cq.js → index-waktfbjy.js} +1 -1
- package/dist/{index-v0dhhxsq.js → index-y1qwzvxn.js} +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -30
- package/dist/{message-index-bounded-7ayzb2ah.js → message-index-bounded-zq7a5nm6.js} +3 -3
- package/dist/migration-worker.js +1 -1
- package/dist/plugin/dream-timer.d.ts.map +1 -1
- package/dist/plugin/embedding-bootstrap-helpers.d.ts +5 -1
- package/dist/plugin/embedding-bootstrap-helpers.d.ts.map +1 -1
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/{project-embedding-registry-94nncs5g.js → project-embedding-registry-cg281d9j.js} +1 -1
- package/dist/raw-ordinal-warmup-worker.js +2 -2
- package/dist/{read-session-chunk-jzb6w9w5.js → read-session-chunk-1ndphmb8.js} +2 -2
- package/dist/{recomp-orchestrator-02sa2ngr.js → recomp-orchestrator-w3pjp33d.js} +10 -10
- package/dist/{search-archive-q1vw7zg4.js → search-archive-sr07c6aj.js} +3 -3
- package/dist/{search-spq6nfyf.js → search-fbaz1m8r.js} +7 -7
- package/dist/{send-session-notification-afzzqy5b.js → send-session-notification-vz86nt79.js} +2 -2
- package/dist/shared/marker-only-text.d.ts +2 -0
- package/dist/shared/marker-only-text.d.ts.map +1 -0
- package/dist/{transform-postprocess-phase-6menyyxc.js → transform-postprocess-phase-6ccazq85.js} +10 -10
- package/dist/v2/auto-search-worker.js +3 -3
- package/dist/v2/chunk-embedding-write-worker.js +1 -1
- package/dist/v2/{context-0qhz1nnj.js → context-wx4sfhgr.js} +345 -102
- package/dist/v2/{embedding-backfill-rh5dgsva.js → embedding-backfill-35k6czrr.js} +2 -2
- package/dist/v2/hidden-child-cleanup.d.ts.map +1 -1
- package/dist/v2/hidden-child-native.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/dream-timer.d.ts.map +1 -1
- package/dist/v2/hooks/store.d.ts.map +1 -1
- package/dist/v2/{message-index-bounded-n09b74vr.js → message-index-bounded-jvgz7vk9.js} +3 -3
- package/dist/v2/message-times.d.ts +17 -0
- package/dist/v2/message-times.d.ts.map +1 -0
- package/dist/v2/migration-worker.js +1 -1
- package/dist/v2/{project-embedding-registry-6pak822h.js → project-embedding-registry-p1p10ks7.js} +1 -1
- package/dist/v2/raw-ordinal-warmup-worker.js +2 -2
- package/dist/v2/{read-session-chunk-vpt69tw9.js → read-session-chunk-djzgm4jg.js} +2 -2
- package/dist/v2/{recomp-orchestrator-txsmbnvw.js → recomp-orchestrator-wb28kr9k.js} +10 -10
- package/dist/v2/{search-archive-t9bcz4ht.js → search-archive-n3dhns4r.js} +3 -3
- package/dist/v2/{search-dcy9b6gd.js → search-q3jjw6zp.js} +7 -7
- package/dist/v2/{send-session-notification-vdsgnf4c.js → send-session-notification-x5nzy34v.js} +2 -2
- package/dist/v2/{server-txbc3czn.js → server-42nebt63.js} +10 -6
- package/dist/v2/{server-94wk1ey3.js → server-835q39kd.js} +45 -5
- package/dist/v2/{server-3vhxy2k5.js → server-a7x00tw8.js} +5 -5
- package/dist/v2/{server-e3hc93wk.js → server-d01pe8q5.js} +1 -1
- package/dist/v2/{server-z2n2xaeh.js → server-hwag0khr.js} +1 -1
- package/dist/v2/{server-jj9qye8y.js → server-k33t88h3.js} +97 -25
- package/dist/v2/{server-6avtp9jp.js → server-p0snacz3.js} +1 -1
- package/dist/v2/{server-z3mk9jdc.js → server-ph300d6m.js} +1 -1
- package/dist/v2/{server-6v3cdzsx.js → server-qc915kw2.js} +1 -1
- package/dist/v2/{server-xkvxss5g.js → server-rb8m4vry.js} +12 -6
- package/dist/v2/{server-zmcrvpm3.js → server-rr4cbkr0.js} +2 -2
- package/dist/v2/{server-yqek3s4k.js → server-zh3esb0z.js} +4 -2
- package/dist/v2/server.js +3 -3
- package/dist/v2/store-reader.d.ts +14 -2
- package/dist/v2/store-reader.d.ts.map +1 -1
- package/dist/v2/{wrapup-orchestrator-gjkszser.js → wrapup-orchestrator-2t0pdqzm.js} +10 -10
- package/dist/{wrapup-orchestrator-n5h5sr1x.js → wrapup-orchestrator-kjmkms5g.js} +11 -11
- package/package.json +1 -1
- package/src/config/commit-retention-recovery.ts +34 -0
- package/src/config/index.ts +22 -2
- package/src/config/live-child-output-cap.test.ts +28 -0
- package/src/config/live-child-output-cap.ts +2 -0
- package/src/config/live-run-config.test.ts +44 -1
- package/src/config/live-run-config.ts +8 -1
- package/src/config/project-config-trust-boundary.test.ts +57 -0
- package/src/config/project-security.ts +6 -2
- package/src/shared/marker-only-text.ts +8 -0
- 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
|
+
}
|
package/src/config/index.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
176
|
+
kind === "Bun.spawn"
|
|
122
177
|
? node.arguments.length === 1
|
|
123
178
|
? 0
|
|
124
179
|
: 1
|
|
125
|
-
:
|
|
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";
|