@cortexkit/opencode-magic-context 0.41.3 → 0.41.4
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/config/index.d.ts +9 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/{conflict-warning-hook-z5sd697e.js → conflict-warning-hook-ejkc9sjn.js} +2 -2
- package/dist/{derive-budgets-901a0pk0.js → derive-budgets-bc366cz8.js} +4 -2
- package/dist/features/magic-context/dreamer/classify.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts +13 -0
- package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-scheduler.d.ts +3 -0
- package/dist/features/magic-context/dreamer/task-scheduler.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-persisted.d.ts +2 -2
- package/dist/hooks/magic-context/command-handler.d.ts +5 -1
- package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-types.d.ts +4 -0
- package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
- package/dist/hooks/magic-context/derive-budgets.d.ts +1 -0
- package/dist/hooks/magic-context/derive-budgets.d.ts.map +1 -1
- package/dist/hooks/magic-context/emergency-drop.d.ts +3 -1
- package/dist/hooks/magic-context/emergency-drop.d.ts.map +1 -1
- package/dist/hooks/magic-context/execute-status.d.ts +7 -1
- package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
- package/dist/hooks/magic-context/heuristic-cleanup.d.ts +4 -3
- package/dist/hooks/magic-context/heuristic-cleanup.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook-handlers.d.ts +2 -0
- package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook.d.ts +5 -2
- package/dist/hooks/magic-context/hook.d.ts.map +1 -1
- package/dist/hooks/magic-context/issue-423-fixture.d.ts +37 -0
- package/dist/hooks/magic-context/issue-423-fixture.d.ts.map +1 -0
- package/dist/hooks/magic-context/issue-424-fixture.d.ts +11 -0
- package/dist/hooks/magic-context/issue-424-fixture.d.ts.map +1 -0
- package/dist/hooks/magic-context/producer-window-guard.d.ts +8 -0
- package/dist/hooks/magic-context/producer-window-guard.d.ts.map +1 -0
- package/dist/hooks/magic-context/protected-tail-boundary.d.ts +59 -0
- package/dist/hooks/magic-context/protected-tail-boundary.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-chunk.d.ts +2 -0
- package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts +4 -1
- package/dist/hooks/magic-context/read-session-true-raw-tokens.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/rust-mode-transform.d.ts +5 -2
- package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/sentinel.d.ts +3 -3
- package/dist/hooks/magic-context/sentinel.d.ts.map +1 -1
- package/dist/hooks/magic-context/tag-messages.d.ts +2 -0
- package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
- package/dist/hooks/magic-context/tail-hygiene-walk.d.ts +8 -3
- package/dist/hooks/magic-context/tail-hygiene-walk.d.ts.map +1 -1
- package/dist/hooks/magic-context/tool-drop-target.d.ts +1 -0
- package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts +2 -0
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +4 -0
- 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-vmmjdpc0.js → index-0y2mcg5y.js} +282 -41
- package/dist/{index-npx8vhmy.js → index-4avmwfp5.js} +1 -1
- package/dist/{index-kby1sng5.js → index-avwt563y.js} +8 -1
- package/dist/{index-0bycg0np.js → index-fz104x1j.js} +1 -1
- package/dist/{index-dn1rwpd8.js → index-mjv9je4e.js} +14 -10
- package/dist/index-sd3egckz.js +1583 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +455 -1400
- package/dist/plugin/hooks/create-session-hooks.d.ts +3 -0
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/{recomp-orchestrator-f5m2wk74.js → recomp-orchestrator-q98r6rga.js} +4 -4
- package/dist/shared/cache-ttl-display.d.ts +23 -0
- package/dist/shared/cache-ttl-display.d.ts.map +1 -0
- package/dist/shared/cache-ttl-seed.d.ts +10 -0
- package/dist/shared/cache-ttl-seed.d.ts.map +1 -0
- package/dist/shared/config-diagnostics.d.ts +30 -0
- package/dist/shared/config-diagnostics.d.ts.map +1 -0
- package/dist/shared/config-warning-surface.d.ts +3 -0
- package/dist/shared/config-warning-surface.d.ts.map +1 -0
- package/dist/shared/data-path.d.ts +10 -9
- package/dist/shared/data-path.d.ts.map +1 -1
- package/dist/shared/harness-provider-map.d.ts +7 -0
- package/dist/shared/harness-provider-map.d.ts.map +1 -1
- package/dist/shared/jsonc-parser.d.ts +12 -0
- package/dist/shared/jsonc-parser.d.ts.map +1 -1
- package/dist/shared/model-suggestion-retry.d.ts +14 -10
- package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
- package/dist/shared/pi-executable.d.ts +6 -0
- package/dist/shared/pi-executable.d.ts.map +1 -0
- package/dist/shared/rpc-types.d.ts +6 -0
- package/dist/shared/rpc-types.d.ts.map +1 -1
- package/dist/shared/rpc-utils.d.ts.map +1 -1
- package/dist/shared/status-detail-text.d.ts.map +1 -1
- package/dist/shared/tag-transcript.d.ts.map +1 -1
- package/dist/{storage-meta-session-3rxx9ekm.js → storage-meta-session-dvwfw778.js} +1 -1
- package/package.json +1 -1
- package/src/config/index.test.ts +44 -0
- package/src/config/index.ts +96 -15
- package/src/shared/cache-ttl-display.test.ts +84 -0
- package/src/shared/cache-ttl-display.ts +64 -0
- package/src/shared/cache-ttl-seed.ts +23 -0
- package/src/shared/config-diagnostics.test.ts +29 -0
- package/src/shared/config-diagnostics.ts +64 -0
- package/src/shared/config-warning-surface.ts +28 -0
- package/src/shared/data-path.ts +10 -9
- package/src/shared/harness-provider-map.test.ts +16 -0
- package/src/shared/harness-provider-map.ts +15 -0
- package/src/shared/jsonc-parser.test.ts +14 -1
- package/src/shared/jsonc-parser.ts +93 -0
- package/src/shared/model-suggestion-retry.ts +143 -28
- package/src/shared/pi-executable.ts +20 -0
- package/src/shared/rpc-types.ts +6 -0
- package/src/shared/rpc-utils.ts +2 -3
- package/src/shared/status-detail-text.test.ts +2 -1
- package/src/shared/status-detail-text.ts +5 -1
- package/src/shared/tag-transcript.ts +21 -2
- package/src/tui/index.tsx +15 -1
- package/src/tui-compiled/index.tsx +234 -222
- package/dist/index-jfpk81yt.js +0 -146
package/src/config/index.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
CONFIG_WARNING_CLASS,
|
|
5
|
+
type ConfigParseFailure,
|
|
6
|
+
type ConfigWarningDetail,
|
|
7
|
+
} from "../shared/config-diagnostics";
|
|
8
|
+
import {
|
|
9
|
+
detectConfigFile,
|
|
10
|
+
isPrototypePollutionKey,
|
|
11
|
+
parseJsoncRecovering,
|
|
12
|
+
} from "../shared/jsonc-parser";
|
|
4
13
|
import { setOutputReserveConfig } from "../shared/models-dev-cache";
|
|
5
14
|
import type { PromptSurfaceConfig } from "../shared/prompt-surface";
|
|
6
15
|
import { setWindowOverlayPath } from "../shared/window-geometry";
|
|
@@ -28,6 +37,11 @@ import { substituteConfigVariables } from "./variable";
|
|
|
28
37
|
|
|
29
38
|
export interface MagicContextPluginConfig extends MagicContextConfig {
|
|
30
39
|
disabled_hooks?: string[];
|
|
40
|
+
/** Runtime-only diagnostics; never read from user config. */
|
|
41
|
+
configParseFailures?: ConfigParseFailure[];
|
|
42
|
+
configWarningDetails?: ConfigWarningDetail[];
|
|
43
|
+
/** Whether cache_ttl existed in resolved raw config rather than coming from Zod defaults. */
|
|
44
|
+
cacheTtlConfigured?: boolean;
|
|
31
45
|
command?: Record<
|
|
32
46
|
string,
|
|
33
47
|
{
|
|
@@ -74,6 +88,8 @@ interface LoadedConfigFile {
|
|
|
74
88
|
config: Record<string, unknown>;
|
|
75
89
|
/** Warnings from {env:} / {file:} substitution, with config-path prefix applied. */
|
|
76
90
|
warnings: string[];
|
|
91
|
+
parseFailures: ConfigParseFailure[];
|
|
92
|
+
warningDetails: ConfigWarningDetail[];
|
|
77
93
|
}
|
|
78
94
|
|
|
79
95
|
export type LoadOutcome =
|
|
@@ -95,6 +111,9 @@ export interface LoadResultDetailed {
|
|
|
95
111
|
};
|
|
96
112
|
substitutionFailures: Array<{ keyPath: string; source: "user" | "project"; message: string }>;
|
|
97
113
|
recoveredTopLevelKeys: string[];
|
|
114
|
+
configParseFailures: ConfigParseFailure[];
|
|
115
|
+
warningDetails: ConfigWarningDetail[];
|
|
116
|
+
cacheTtlConfigured: boolean;
|
|
98
117
|
}
|
|
99
118
|
|
|
100
119
|
interface LoadedConfigFileDetailed extends LoadedConfigFile {
|
|
@@ -118,10 +137,13 @@ function loadConfigFileDetailed(
|
|
|
118
137
|
rawText = raw.text;
|
|
119
138
|
rawWarnings = raw.warnings;
|
|
120
139
|
} catch (error) {
|
|
140
|
+
const message = `failed to read config: ${error instanceof Error ? error.message : String(error)}`;
|
|
121
141
|
return {
|
|
122
142
|
config: {},
|
|
123
|
-
warnings: [
|
|
124
|
-
|
|
143
|
+
warnings: [`${configPath}: ${message}`],
|
|
144
|
+
parseFailures: [],
|
|
145
|
+
warningDetails: [
|
|
146
|
+
{ warningClass: CONFIG_WARNING_CLASS.FILE_IO, source, path: configPath, message },
|
|
125
147
|
],
|
|
126
148
|
outcome: "project-file-io-error",
|
|
127
149
|
source,
|
|
@@ -135,32 +157,71 @@ function loadConfigFileDetailed(
|
|
|
135
157
|
isProjectConfig: source === "project",
|
|
136
158
|
});
|
|
137
159
|
const rejectedKeyPaths: string[] = [];
|
|
138
|
-
const
|
|
160
|
+
const parsed = parseJsoncRecovering<Record<string, unknown>>(substituted.text, {
|
|
139
161
|
onRejectedKey: (path) => rejectedKeyPaths.push(path.join(".")),
|
|
140
162
|
});
|
|
163
|
+
const config =
|
|
164
|
+
parsed.value && typeof parsed.value === "object" && !Array.isArray(parsed.value)
|
|
165
|
+
? parsed.value
|
|
166
|
+
: {};
|
|
141
167
|
const unsafeKeyWarnings = rejectedKeyPaths.map(
|
|
142
168
|
(path) =>
|
|
143
169
|
`Ignored unsafe config key "${path}" (security: prototype-pollution keys are not allowed).`,
|
|
144
170
|
);
|
|
171
|
+
const firstIssue = parsed.issues[0];
|
|
172
|
+
const recovered = firstIssue !== undefined && Object.keys(config).length > 0;
|
|
173
|
+
const parseFailures: ConfigParseFailure[] = firstIssue
|
|
174
|
+
? [
|
|
175
|
+
{
|
|
176
|
+
warningClass: CONFIG_WARNING_CLASS.FILE_PARSE,
|
|
177
|
+
source,
|
|
178
|
+
path: configPath,
|
|
179
|
+
line: firstIssue.line,
|
|
180
|
+
column: firstIssue.column,
|
|
181
|
+
message: firstIssue.message,
|
|
182
|
+
recovered,
|
|
183
|
+
warning: `${configPath}:${firstIssue.line}:${firstIssue.column}: ${firstIssue.message}; ${recovered ? "recovered values were applied, but the file must be fixed." : "using defaults for this file."}`,
|
|
184
|
+
},
|
|
185
|
+
]
|
|
186
|
+
: [];
|
|
145
187
|
return {
|
|
146
188
|
config,
|
|
147
|
-
warnings: [
|
|
148
|
-
(
|
|
149
|
-
|
|
189
|
+
warnings: [
|
|
190
|
+
...parseFailures.map((failure) => failure.warning),
|
|
191
|
+
...rawWarnings.map((warning) => `${configPath}: ${warning}`),
|
|
192
|
+
...substituted.warnings.map((warning) => `${configPath}: ${warning}`),
|
|
193
|
+
...unsafeKeyWarnings.map((warning) => `${configPath}: ${warning}`),
|
|
194
|
+
],
|
|
195
|
+
parseFailures,
|
|
196
|
+
warningDetails: parseFailures,
|
|
150
197
|
outcome:
|
|
151
|
-
|
|
152
|
-
? "
|
|
153
|
-
:
|
|
154
|
-
? "
|
|
155
|
-
:
|
|
198
|
+
parseFailures.length > 0
|
|
199
|
+
? "project-file-parse-error"
|
|
200
|
+
: rejectedKeyPaths.length > 0
|
|
201
|
+
? "schema-recovery"
|
|
202
|
+
: substituted.warnings.length > 0
|
|
203
|
+
? "substitution-failure"
|
|
204
|
+
: "ok",
|
|
156
205
|
source,
|
|
157
206
|
};
|
|
158
207
|
} catch (error) {
|
|
208
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
209
|
+
const warning = `${configPath}:1:1: ${message}; using defaults for this file.`;
|
|
210
|
+
const failure: ConfigParseFailure = {
|
|
211
|
+
warningClass: CONFIG_WARNING_CLASS.FILE_PARSE,
|
|
212
|
+
source,
|
|
213
|
+
path: configPath,
|
|
214
|
+
line: 1,
|
|
215
|
+
column: 1,
|
|
216
|
+
message,
|
|
217
|
+
recovered: false,
|
|
218
|
+
warning,
|
|
219
|
+
};
|
|
159
220
|
return {
|
|
160
221
|
config: {},
|
|
161
|
-
warnings: [
|
|
162
|
-
|
|
163
|
-
],
|
|
222
|
+
warnings: [warning],
|
|
223
|
+
parseFailures: [failure],
|
|
224
|
+
warningDetails: [failure],
|
|
164
225
|
outcome: "project-file-parse-error",
|
|
165
226
|
source,
|
|
166
227
|
};
|
|
@@ -621,10 +682,12 @@ export function loadPluginConfigDetailed(directory: string): LoadResultDetailed
|
|
|
621
682
|
}
|
|
622
683
|
|
|
623
684
|
const recoveredTopLevelKeys: string[] = [];
|
|
685
|
+
const cacheTtlConfigured = Object.hasOwn(mergedRaw, "cache_ttl");
|
|
624
686
|
const config = parsePluginConfig(mergedRaw, recoveredTopLevelKeys);
|
|
625
687
|
if (profileResolution.activeProfile) config.profile = profileResolution.activeProfile;
|
|
626
688
|
setOutputReserveConfig(config.output_reserve);
|
|
627
689
|
setWindowOverlayPath(config.models?.window_overlay_path);
|
|
690
|
+
const leafValidationWarnings = [...(config.configWarnings ?? [])];
|
|
628
691
|
if (config.configWarnings?.length) {
|
|
629
692
|
allWarnings.push(
|
|
630
693
|
...config.configWarnings.map((w) => {
|
|
@@ -653,6 +716,21 @@ export function loadPluginConfigDetailed(directory: string): LoadResultDetailed
|
|
|
653
716
|
...bindSubstitutionFailures(userLoaded),
|
|
654
717
|
...bindSubstitutionFailures(projectLoaded),
|
|
655
718
|
];
|
|
719
|
+
const configParseFailures = [
|
|
720
|
+
...(userLoaded?.parseFailures ?? []),
|
|
721
|
+
...(projectLoaded?.parseFailures ?? []),
|
|
722
|
+
];
|
|
723
|
+
const warningDetails: ConfigWarningDetail[] = [
|
|
724
|
+
...(userLoaded?.warningDetails ?? []),
|
|
725
|
+
...(projectLoaded?.warningDetails ?? []),
|
|
726
|
+
...leafValidationWarnings.map((message) => ({
|
|
727
|
+
warningClass: CONFIG_WARNING_CLASS.INVALID_LEAF,
|
|
728
|
+
message,
|
|
729
|
+
})),
|
|
730
|
+
];
|
|
731
|
+
config.configParseFailures = configParseFailures;
|
|
732
|
+
config.configWarningDetails = warningDetails;
|
|
733
|
+
config.cacheTtlConfigured = cacheTtlConfigured;
|
|
656
734
|
const sources = {
|
|
657
735
|
userConfig:
|
|
658
736
|
userLoaded?.outcome ??
|
|
@@ -669,5 +747,8 @@ export function loadPluginConfigDetailed(directory: string): LoadResultDetailed
|
|
|
669
747
|
sources,
|
|
670
748
|
substitutionFailures,
|
|
671
749
|
recoveredTopLevelKeys,
|
|
750
|
+
configParseFailures,
|
|
751
|
+
warningDetails,
|
|
752
|
+
cacheTtlConfigured,
|
|
672
753
|
};
|
|
673
754
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { initializeDatabase } from "../features/magic-context/storage-db";
|
|
4
|
+
import { getOrCreateSessionMeta, updateSessionMeta } from "../features/magic-context/storage-meta";
|
|
5
|
+
import { resolveCacheTtlDisplay } from "./cache-ttl-display";
|
|
6
|
+
import { seedSessionCacheTtlIfUnsynced } from "./cache-ttl-seed";
|
|
7
|
+
import { Database } from "./sqlite";
|
|
8
|
+
|
|
9
|
+
const modelKey = "anthropic/claude-opus-5";
|
|
10
|
+
|
|
11
|
+
describe("resolveCacheTtlDisplay", () => {
|
|
12
|
+
it("shows config before the first message_end sync", () => {
|
|
13
|
+
expect(
|
|
14
|
+
resolveCacheTtlDisplay({
|
|
15
|
+
configured: { default: "5m", [modelKey]: "1h" },
|
|
16
|
+
configuredExplicitly: true,
|
|
17
|
+
modelKey,
|
|
18
|
+
sessionValue: "5m",
|
|
19
|
+
sessionModelKey: null,
|
|
20
|
+
}),
|
|
21
|
+
).toEqual({ value: "1h", source: "config", modelKey });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("discriminates config, session, and guessed default provenance", () => {
|
|
25
|
+
const configured = resolveCacheTtlDisplay({
|
|
26
|
+
configured: "1h",
|
|
27
|
+
configuredExplicitly: true,
|
|
28
|
+
modelKey,
|
|
29
|
+
sessionValue: "5m",
|
|
30
|
+
sessionModelKey: null,
|
|
31
|
+
});
|
|
32
|
+
const session = resolveCacheTtlDisplay({
|
|
33
|
+
configured: "1h",
|
|
34
|
+
configuredExplicitly: true,
|
|
35
|
+
modelKey,
|
|
36
|
+
sessionValue: "45m",
|
|
37
|
+
sessionModelKey: modelKey,
|
|
38
|
+
});
|
|
39
|
+
const fallback = resolveCacheTtlDisplay({
|
|
40
|
+
configured: "5m",
|
|
41
|
+
configuredExplicitly: false,
|
|
42
|
+
modelKey,
|
|
43
|
+
sessionValue: "5m",
|
|
44
|
+
sessionModelKey: null,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(configured.source).toBe("config");
|
|
48
|
+
expect(session).toEqual({ value: "45m", source: "session", modelKey });
|
|
49
|
+
expect(fallback).toEqual({ value: "5m", source: "default", modelKey });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("seeds only an unsynced row and leaves a scheduler-owned row unchanged", () => {
|
|
53
|
+
const db = new Database(":memory:");
|
|
54
|
+
initializeDatabase(db);
|
|
55
|
+
try {
|
|
56
|
+
expect(
|
|
57
|
+
seedSessionCacheTtlIfUnsynced({
|
|
58
|
+
db,
|
|
59
|
+
sessionId: "ses-seed",
|
|
60
|
+
configured: { default: "5m", [modelKey]: "1h" },
|
|
61
|
+
modelKey,
|
|
62
|
+
}),
|
|
63
|
+
).toBe(true);
|
|
64
|
+
expect(getOrCreateSessionMeta(db, "ses-seed").cacheTtl).toBe("1h");
|
|
65
|
+
|
|
66
|
+
updateSessionMeta(db, "ses-seed", {
|
|
67
|
+
cacheTtl: "45m",
|
|
68
|
+
lastObservedModelKey: modelKey,
|
|
69
|
+
lastResponseTime: 1,
|
|
70
|
+
});
|
|
71
|
+
expect(
|
|
72
|
+
seedSessionCacheTtlIfUnsynced({
|
|
73
|
+
db,
|
|
74
|
+
sessionId: "ses-seed",
|
|
75
|
+
configured: "2h",
|
|
76
|
+
modelKey,
|
|
77
|
+
}),
|
|
78
|
+
).toBe(false);
|
|
79
|
+
expect(getOrCreateSessionMeta(db, "ses-seed").cacheTtl).toBe("45m");
|
|
80
|
+
} finally {
|
|
81
|
+
db.close();
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { MagicContextConfig } from "../config/schema/magic-context";
|
|
2
|
+
import { resolveModelConfigValue } from "./prompt-surface";
|
|
3
|
+
|
|
4
|
+
export type CacheTtlDisplaySource = "config" | "session" | "default";
|
|
5
|
+
|
|
6
|
+
export interface CacheTtlDisplay {
|
|
7
|
+
value: string;
|
|
8
|
+
source: CacheTtlDisplaySource;
|
|
9
|
+
modelKey: string | undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ResolveCacheTtlDisplayArgs {
|
|
13
|
+
configured: MagicContextConfig["cache_ttl"];
|
|
14
|
+
configuredExplicitly: boolean;
|
|
15
|
+
modelKey: string | undefined;
|
|
16
|
+
sessionValue: string;
|
|
17
|
+
/** Model key persisted with the last completed assistant response. */
|
|
18
|
+
sessionModelKey: string | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolve status-only TTL text without changing the scheduler's persisted truth.
|
|
23
|
+
* Use the session value only when its persisted model key matches the model being
|
|
24
|
+
* displayed; otherwise use live config because the session value may be for another model.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveCacheTtlDisplay(args: ResolveCacheTtlDisplayArgs): CacheTtlDisplay {
|
|
27
|
+
if (
|
|
28
|
+
(args.sessionModelKey && (!args.modelKey || args.sessionModelKey === args.modelKey)) ||
|
|
29
|
+
(!args.modelKey && !args.sessionModelKey && args.sessionValue !== "5m")
|
|
30
|
+
) {
|
|
31
|
+
return {
|
|
32
|
+
value: args.sessionValue || "5m",
|
|
33
|
+
source: "session",
|
|
34
|
+
modelKey: args.modelKey ?? args.sessionModelKey ?? undefined,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (typeof args.configured === "string") {
|
|
39
|
+
return {
|
|
40
|
+
value: args.configured,
|
|
41
|
+
source: args.configuredExplicitly ? "config" : "default",
|
|
42
|
+
modelKey: args.modelKey,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const matched = resolveModelConfigValue(args.configured, args.modelKey);
|
|
47
|
+
if (matched) {
|
|
48
|
+
return { value: matched.value, source: "config", modelKey: args.modelKey };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
value: args.configured.default ?? "5m",
|
|
53
|
+
source: "default",
|
|
54
|
+
modelKey: args.modelKey,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function formatCacheTtlDisplay(display: CacheTtlDisplay): string {
|
|
59
|
+
if (display.source === "session") return `Cache TTL: ${display.value} (session)`;
|
|
60
|
+
if (display.source === "config") {
|
|
61
|
+
return `Cache TTL: ${display.value} (config for ${display.modelKey ?? "current model"})`;
|
|
62
|
+
}
|
|
63
|
+
return `Cache TTL: ${display.value} (default — no cache_ttl for ${display.modelKey ?? "unknown model"})`;
|
|
64
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { MagicContextConfig } from "../config/schema/magic-context";
|
|
2
|
+
import { getOrCreateSessionMeta, updateSessionMeta } from "../features/magic-context/storage-meta";
|
|
3
|
+
import { resolveModelConfigValue } from "./prompt-surface";
|
|
4
|
+
import type { Database } from "./sqlite";
|
|
5
|
+
|
|
6
|
+
/** Seed only before a completed assistant response has made the session row authoritative. */
|
|
7
|
+
export function seedSessionCacheTtlIfUnsynced(args: {
|
|
8
|
+
db: Database;
|
|
9
|
+
sessionId: string;
|
|
10
|
+
configured: MagicContextConfig["cache_ttl"];
|
|
11
|
+
modelKey: string;
|
|
12
|
+
}): boolean {
|
|
13
|
+
const meta = getOrCreateSessionMeta(args.db, args.sessionId);
|
|
14
|
+
if (meta.lastResponseTime !== 0 || meta.lastObservedModelKey !== null) return false;
|
|
15
|
+
const cacheTtl =
|
|
16
|
+
typeof args.configured === "string"
|
|
17
|
+
? args.configured
|
|
18
|
+
: (resolveModelConfigValue(args.configured, args.modelKey)?.value ??
|
|
19
|
+
args.configured.default ??
|
|
20
|
+
"5m");
|
|
21
|
+
updateSessionMeta(args.db, args.sessionId, { cacheTtl });
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type ConfigParseFailure,
|
|
5
|
+
claimConfigParseFailuresOnce,
|
|
6
|
+
resetClaimedConfigParseFailuresForTesting,
|
|
7
|
+
} from "./config-diagnostics";
|
|
8
|
+
|
|
9
|
+
const failure: ConfigParseFailure = {
|
|
10
|
+
warningClass: "file-parse",
|
|
11
|
+
source: "user",
|
|
12
|
+
path: "/tmp/magic-context.jsonc",
|
|
13
|
+
line: 1,
|
|
14
|
+
column: 1,
|
|
15
|
+
message: "invalid symbol",
|
|
16
|
+
recovered: true,
|
|
17
|
+
warning: "/tmp/magic-context.jsonc:1:1: invalid symbol",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
describe("config parse warning notice claims", () => {
|
|
21
|
+
beforeEach(resetClaimedConfigParseFailuresForTesting);
|
|
22
|
+
|
|
23
|
+
it("emits each parse failure once per harness surface", () => {
|
|
24
|
+
expect(claimConfigParseFailuresOnce("opencode", [failure])).toEqual([failure]);
|
|
25
|
+
expect(claimConfigParseFailuresOnce("opencode", [failure])).toEqual([]);
|
|
26
|
+
expect(claimConfigParseFailuresOnce("pi", [failure])).toEqual([failure]);
|
|
27
|
+
expect(claimConfigParseFailuresOnce("pi", [failure])).toEqual([]);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export const CONFIG_WARNING_CLASS = {
|
|
2
|
+
FILE_PARSE: "file-parse",
|
|
3
|
+
FILE_IO: "file-io",
|
|
4
|
+
INVALID_LEAF: "invalid-leaf",
|
|
5
|
+
} as const;
|
|
6
|
+
|
|
7
|
+
export type ConfigWarningClass = (typeof CONFIG_WARNING_CLASS)[keyof typeof CONFIG_WARNING_CLASS];
|
|
8
|
+
|
|
9
|
+
export interface ConfigParseFailure {
|
|
10
|
+
warningClass: typeof CONFIG_WARNING_CLASS.FILE_PARSE;
|
|
11
|
+
source: "user" | "project";
|
|
12
|
+
path: string;
|
|
13
|
+
line: number;
|
|
14
|
+
column: number;
|
|
15
|
+
message: string;
|
|
16
|
+
recovered: boolean;
|
|
17
|
+
warning: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ConfigWarningDetail {
|
|
21
|
+
warningClass: ConfigWarningClass;
|
|
22
|
+
source?: "user" | "project";
|
|
23
|
+
path?: string;
|
|
24
|
+
line?: number;
|
|
25
|
+
column?: number;
|
|
26
|
+
message: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function formatConfigParseStatusLine(failure: ConfigParseFailure): string {
|
|
30
|
+
const disposition = failure.recovered
|
|
31
|
+
? "recovered values applied; fix the file"
|
|
32
|
+
: "running on defaults";
|
|
33
|
+
return `Config: PARSE FAILED (${failure.path}:${failure.line}:${failure.column}) — ${disposition}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function formatConfigParseNotice(failures: readonly ConfigParseFailure[]): string {
|
|
37
|
+
const lines = failures.map(
|
|
38
|
+
(failure) => `${formatConfigParseStatusLine(failure)}\n${failure.message}`,
|
|
39
|
+
);
|
|
40
|
+
return ["⚠️ Magic Context config parse failure", ...lines].join("\n\n");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const claimedFailures = new Set<string>();
|
|
44
|
+
|
|
45
|
+
function failureKey(surface: string, failure: ConfigParseFailure): string {
|
|
46
|
+
return `${surface}\0${failure.path}\0${failure.line}\0${failure.column}\0${failure.message}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Claim parse-failure notices once for each runtime surface in this process. */
|
|
50
|
+
export function claimConfigParseFailuresOnce(
|
|
51
|
+
surface: "opencode" | "pi",
|
|
52
|
+
failures: readonly ConfigParseFailure[],
|
|
53
|
+
): ConfigParseFailure[] {
|
|
54
|
+
return failures.filter((failure) => {
|
|
55
|
+
const key = failureKey(surface, failure);
|
|
56
|
+
if (claimedFailures.has(key)) return false;
|
|
57
|
+
claimedFailures.add(key);
|
|
58
|
+
return true;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function resetClaimedConfigParseFailuresForTesting(): void {
|
|
63
|
+
claimedFailures.clear();
|
|
64
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ConfigParseFailure } from "./config-diagnostics";
|
|
2
|
+
import { formatConfigParseStatusLine } from "./config-diagnostics";
|
|
3
|
+
|
|
4
|
+
export function buildOpenCodeConfigWarningBanner(
|
|
5
|
+
warnings: readonly string[],
|
|
6
|
+
parseFailures: readonly ConfigParseFailure[],
|
|
7
|
+
allParseFailures: readonly ConfigParseFailure[] = parseFailures,
|
|
8
|
+
): string {
|
|
9
|
+
const parseWarningStrings = new Set(allParseFailures.map((failure) => failure.warning));
|
|
10
|
+
const ordinaryWarnings = warnings.filter(
|
|
11
|
+
(warning) =>
|
|
12
|
+
![...parseWarningStrings].some((parseWarning) => warning.includes(parseWarning)),
|
|
13
|
+
);
|
|
14
|
+
const entries = [
|
|
15
|
+
...parseFailures.map(
|
|
16
|
+
(failure) =>
|
|
17
|
+
`- **TOP SEVERITY:** ${formatConfigParseStatusLine(failure)} — ${failure.message}`,
|
|
18
|
+
),
|
|
19
|
+
...ordinaryWarnings.map((warning) => `- ${warning}`),
|
|
20
|
+
];
|
|
21
|
+
return [
|
|
22
|
+
"## ⚠️ Magic Context Config Warning",
|
|
23
|
+
"",
|
|
24
|
+
...entries,
|
|
25
|
+
"",
|
|
26
|
+
"Fix the reported `magic-context.jsonc` file; Magic Context remains enabled.",
|
|
27
|
+
].join("\n");
|
|
28
|
+
}
|
package/src/shared/data-path.ts
CHANGED
|
@@ -13,16 +13,17 @@ export function getDataDir(): string {
|
|
|
13
13
|
* Layout:
|
|
14
14
|
* - OpenCode: `${os.tmpdir()}/opencode/magic-context/`
|
|
15
15
|
* - Pi: `${os.tmpdir()}/pi/magic-context/`
|
|
16
|
+
* - OMP: `${os.tmpdir()}/omp/magic-context/`
|
|
16
17
|
*
|
|
17
18
|
* Why a per-harness subtree of `os.tmpdir()`:
|
|
18
19
|
* 1. OpenCode Desktop runs as an Electron app with a permission sandbox.
|
|
19
20
|
* Writing to arbitrary tmp paths can trigger user-visible permission
|
|
20
21
|
* prompts; the `${tmpdir}/opencode/` subtree is allow-listed by
|
|
21
22
|
* OpenCode, so anything we put under it never asks for permission.
|
|
22
|
-
* 2. Splitting
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* 2. Splitting harnesses keeps their logs and historian dump directories
|
|
24
|
+
* cleanly separated. `doctor --issue` for each harness reports diagnostics
|
|
25
|
+
* from the matching subtree, so issue reports do not include another
|
|
26
|
+
* harness's log noise.
|
|
26
27
|
* 3. Pi has no permission sandbox, so the path choice is purely
|
|
27
28
|
* cosmetic for Pi — it just keeps the layout symmetric.
|
|
28
29
|
*
|
|
@@ -37,13 +38,13 @@ export function getMagicContextTempDir(harness: HarnessId = getHarness()): strin
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
|
-
* Standard log file path the plugin writes to.
|
|
41
|
-
*
|
|
42
|
-
*
|
|
41
|
+
* Standard log file path the plugin writes to. Each harness writes to a
|
|
42
|
+
* separate log under its own subtree so a machine running multiple harnesses
|
|
43
|
+
* does not interleave session traces.
|
|
43
44
|
*
|
|
44
45
|
* The plugin's buffered logger calls this on every flush rather than
|
|
45
|
-
* caching, so `setHarness(
|
|
46
|
-
*
|
|
46
|
+
* caching, so the boot-time `setHarness()` call is reflected in the next
|
|
47
|
+
* flush.
|
|
47
48
|
*/
|
|
48
49
|
export function getMagicContextLogPath(harness: HarnessId = getHarness()): string {
|
|
49
50
|
// An explicit override wins over the harness temp-dir default, so users on
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import {
|
|
3
|
+
canonicalModelIdentity,
|
|
3
4
|
modelRefLookupOrder,
|
|
4
5
|
ompModelRefToCanonical,
|
|
5
6
|
piModelRefToCanonical,
|
|
@@ -8,6 +9,21 @@ import {
|
|
|
8
9
|
} from "./harness-provider-map";
|
|
9
10
|
|
|
10
11
|
describe("harness-provider-map", () => {
|
|
12
|
+
describe("canonicalModelIdentity", () => {
|
|
13
|
+
it("collapses Astra route aliases without changing unrelated Copilot models", () => {
|
|
14
|
+
for (const ref of [
|
|
15
|
+
"openai/gpt-6-astra",
|
|
16
|
+
"openai-codex/gpt-6-astra",
|
|
17
|
+
"github-copilot/gpt-6-astra",
|
|
18
|
+
]) {
|
|
19
|
+
expect(canonicalModelIdentity(ref)).toBe("openai/gpt-6-astra");
|
|
20
|
+
}
|
|
21
|
+
expect(canonicalModelIdentity("github-copilot/claude-opus-5")).toBe(
|
|
22
|
+
"github-copilot/claude-opus-5",
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
11
27
|
describe("resolveModelRefForPi (canonical -> Pi, used when spawning)", () => {
|
|
12
28
|
it("maps the diverging auth-plugin providers, preserving the model id", () => {
|
|
13
29
|
expect(resolveModelRefForPi("openai/gpt-5.5")).toBe("openai-codex/gpt-5.5");
|
|
@@ -58,6 +58,10 @@ const OMP_TO_CANONICAL_PROVIDER: Readonly<Record<string, string>> = {
|
|
|
58
58
|
"opencode-zen": "opencode",
|
|
59
59
|
};
|
|
60
60
|
|
|
61
|
+
const CANONICAL_MODEL_IDENTITY_ALIASES: Readonly<Record<string, string>> = {
|
|
62
|
+
"github-copilot/gpt-6-astra": "openai/gpt-6-astra",
|
|
63
|
+
};
|
|
64
|
+
|
|
61
65
|
/** Remap only the provider prefix (text before the first "/"), preserving the
|
|
62
66
|
* model id verbatim. No "/", empty provider, or unmapped provider -> unchanged.
|
|
63
67
|
* Lookups are own-property only, so provider ids that collide with
|
|
@@ -72,6 +76,17 @@ function remapProviderPrefix(ref: string, map: Readonly<Record<string, string>>)
|
|
|
72
76
|
return `${map[provider]}${ref.slice(slash)}`;
|
|
73
77
|
}
|
|
74
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Canonicalize only for identity comparisons and cache policy; callers must not
|
|
81
|
+
* use the result as a provider spawn route. Provider-wide Pi/OMP aliases are
|
|
82
|
+
* normalized first; model-specific aliases stay explicit because GitHub Copilot
|
|
83
|
+
* also serves non-OpenAI model families.
|
|
84
|
+
*/
|
|
85
|
+
export function canonicalModelIdentity(ref: string): string {
|
|
86
|
+
const harnessCanonical = piModelRefToCanonical(ompModelRefToCanonical(ref));
|
|
87
|
+
return CANONICAL_MODEL_IDENTITY_ALIASES[harnessCanonical.toLowerCase()] ?? harnessCanonical;
|
|
88
|
+
}
|
|
89
|
+
|
|
75
90
|
/** Pi-native `provider/model` -> canonical (OpenCode). Identity when unmapped.
|
|
76
91
|
* Used by the Pi setup wizard so configs it writes stay OpenCode-readable. */
|
|
77
92
|
export function piModelRefToCanonical(ref: string): string {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
2
|
|
|
3
|
-
import { parseJsonc } from "./jsonc-parser";
|
|
3
|
+
import { parseJsonc, parseJsoncRecovering } from "./jsonc-parser";
|
|
4
4
|
|
|
5
5
|
describe("parseJsonc prototype-pollution hardening", () => {
|
|
6
6
|
it("rejects dangerous keys recursively, including inside arrays", () => {
|
|
@@ -30,4 +30,17 @@ describe("parseJsonc prototype-pollution hardening", () => {
|
|
|
30
30
|
expect(Object.getPrototypeOf(items[0])).toBe(Object.prototype);
|
|
31
31
|
expect("polluted" in items[0]).toBe(false);
|
|
32
32
|
});
|
|
33
|
+
|
|
34
|
+
it("preserves hardening when tolerant recovery encounters __proto__", () => {
|
|
35
|
+
const rejected: string[] = [];
|
|
36
|
+
const parsed = parseJsoncRecovering<Record<string, unknown>>(
|
|
37
|
+
'\\{\n"__proto__":{"polluted":true},"cache_ttl":"1h"}',
|
|
38
|
+
{ onRejectedKey: (path) => rejected.push(path.join(".")) },
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
expect(parsed.value).toEqual({ cache_ttl: "1h" });
|
|
42
|
+
expect(parsed.issues[0]).toMatchObject({ line: 1, column: 1 });
|
|
43
|
+
expect(rejected).toContain("__proto__");
|
|
44
|
+
expect(({} as { polluted?: boolean }).polluted).toBeUndefined();
|
|
45
|
+
});
|
|
33
46
|
});
|