@cortexkit/opencode-magic-context 0.36.1 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/project-security.d.ts +2 -1
- package/dist/config/project-security.d.ts.map +1 -1
- package/dist/config/schema/magic-context.d.ts +10 -0
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/{conflict-warning-hook-mt0ttc4x.js → conflict-warning-hook-an4fxhwf.js} +3 -3
- package/dist/{derive-budgets-88mxk2w1.js → derive-budgets-da4yg3tz.js} +3 -2
- package/dist/features/magic-context/context-authority.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/classify-prompt.d.ts +5 -1
- package/dist/features/magic-context/dreamer/classify-prompt.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/classify.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/evaluate-smart-notes.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/manifest-parser.d.ts +8 -0
- package/dist/features/magic-context/dreamer/manifest-parser.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts +7 -1
- package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/map-memories.d.ts +18 -0
- package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts +5 -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/verify-prompt.d.ts +5 -1
- package/dist/features/magic-context/dreamer/verify-prompt.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
- package/dist/features/magic-context/fail-closed-block.d.ts +6 -1
- package/dist/features/magic-context/fail-closed-block.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-openai.d.ts +3 -1
- package/dist/features/magic-context/memory/embedding-openai.d.ts.map +1 -1
- package/dist/features/magic-context/memory/project-identity.d.ts +6 -0
- package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
- package/dist/features/magic-context/migrations.d.ts.map +1 -1
- package/dist/features/magic-context/overflow-detection.d.ts.map +1 -1
- package/dist/features/magic-context/reclaim-protection.d.ts +13 -0
- package/dist/features/magic-context/reclaim-protection.d.ts.map +1 -0
- package/dist/features/magic-context/smart-notes/runner.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/wake-plane.d.ts +21 -0
- package/dist/features/magic-context/smart-notes/wake-plane.d.ts.map +1 -0
- package/dist/features/magic-context/storage-db.d.ts +19 -3
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-persisted.d.ts +23 -0
- package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-shared.d.ts.map +1 -1
- package/dist/features/magic-context/storage-primers.d.ts +11 -0
- package/dist/features/magic-context/storage-primers.d.ts.map +1 -1
- package/dist/features/magic-context/storage-schema-helpers.d.ts.map +1 -1
- package/dist/features/magic-context/storage-tags.d.ts +13 -5
- package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
- package/dist/features/magic-context/user-memory/storage-user-memory.d.ts +7 -0
- package/dist/features/magic-context/user-memory/storage-user-memory.d.ts.map +1 -1
- package/dist/features/magic-context/window-report-ledger.d.ts +70 -0
- package/dist/features/magic-context/window-report-ledger.d.ts.map +1 -0
- package/dist/hooks/auto-update-checker/checker.d.ts +8 -1
- package/dist/hooks/auto-update-checker/checker.d.ts.map +1 -1
- package/dist/hooks/auto-update-checker/index.d.ts.map +1 -1
- package/dist/hooks/magic-context/channel2-cycle.d.ts +16 -0
- package/dist/hooks/magic-context/channel2-cycle.d.ts.map +1 -0
- package/dist/hooks/magic-context/channel2-delivery.d.ts +3 -9
- package/dist/hooks/magic-context/channel2-delivery.d.ts.map +1 -1
- package/dist/hooks/magic-context/command-handler.d.ts +2 -0
- package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts +33 -103
- package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts.map +1 -1
- package/dist/hooks/magic-context/emergency-drop.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-resolvers.d.ts +4 -0
- package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
- package/dist/hooks/magic-context/execute-status.d.ts +3 -1
- package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
- package/dist/hooks/magic-context/fold-execution-gate.d.ts +2 -0
- package/dist/hooks/magic-context/fold-execution-gate.d.ts.map +1 -0
- package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook.d.ts.map +1 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
- package/dist/hooks/magic-context/live-session-state.d.ts +3 -0
- package/dist/hooks/magic-context/live-session-state.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 +20 -0
- package/dist/hooks/magic-context/lkg-slot.d.ts.map +1 -1
- package/dist/hooks/magic-context/module-state-sync.d.ts +8 -0
- package/dist/hooks/magic-context/module-state-sync.d.ts.map +1 -1
- package/dist/hooks/magic-context/module-transport.d.ts.map +1 -1
- package/dist/hooks/magic-context/module-wire.d.ts +6 -1
- package/dist/hooks/magic-context/module-wire.d.ts.map +1 -1
- package/dist/hooks/magic-context/rust-mode-transform.d.ts +21 -2
- package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/strip-content.d.ts +27 -0
- package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
- package/dist/hooks/magic-context/supersession-reclaim.d.ts.map +1 -1
- package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
- package/dist/hooks/magic-context/tail-hygiene-walk.d.ts +78 -0
- package/dist/hooks/magic-context/tail-hygiene-walk.d.ts.map +1 -0
- package/dist/hooks/magic-context/tool-reclaim.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +14 -3
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +10 -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-nsx2n9eg.js → index-4cz5nfxf.js} +3 -3
- package/dist/{index-f1tnb3vm.js → index-8pqfcghm.js} +21 -9
- package/dist/index-9zhhvr4w.js +733 -0
- package/dist/{index-zp6x6s6a.js → index-cae89eas.js} +25 -6
- package/dist/index-cgyfn1s2.js +53 -0
- package/dist/{index-3zqcdwqr.js → index-crke4z5j.js} +7 -4
- package/dist/{index-1skzp4dc.js → index-mmvhrqqb.js} +1991 -274
- package/dist/{index-tsgs7td8.js → index-pck9vadr.js} +1 -1
- package/dist/{index-3tnkymva.js → index-sw8gnq34.js} +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1714 -988
- 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/messages-transform.d.ts.map +1 -1
- package/dist/plugin/rpc-handlers.d.ts +2 -0
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/{recomp-orchestrator-a0g9b52q.js → recomp-orchestrator-jsde4ygw.js} +7 -6
- package/dist/{safe-notification-target-c11fdda2.js → safe-notification-target-b693f9x3.js} +2 -2
- package/dist/{send-session-notification-bgtf4853.js → send-session-notification-dxske5zm.js} +3 -3
- package/dist/shared/conflict-detector.d.ts +63 -0
- package/dist/shared/conflict-detector.d.ts.map +1 -1
- package/dist/shared/harness-provider-map.d.ts +8 -0
- package/dist/shared/harness-provider-map.d.ts.map +1 -1
- package/dist/shared/logger.d.ts +8 -0
- package/dist/shared/logger.d.ts.map +1 -1
- package/dist/shared/models-dev-cache.d.ts +7 -0
- package/dist/shared/models-dev-cache.d.ts.map +1 -1
- package/dist/shared/prompt-surface-runtime.d.ts.map +1 -1
- package/dist/shared/prompt-surface.d.ts +2 -0
- package/dist/shared/prompt-surface.d.ts.map +1 -1
- package/dist/shared/rpc-client.d.ts.map +1 -1
- package/dist/shared/rpc-server.d.ts.map +1 -1
- package/dist/shared/rpc-types.d.ts +29 -0
- package/dist/shared/rpc-types.d.ts.map +1 -1
- package/dist/shared/rpc-utils.d.ts +28 -9
- package/dist/shared/rpc-utils.d.ts.map +1 -1
- package/dist/shared/tail-hygiene-status.d.ts +15 -0
- package/dist/shared/tail-hygiene-status.d.ts.map +1 -0
- package/dist/shared/window-geometry.d.ts +90 -0
- package/dist/shared/window-geometry.d.ts.map +1 -0
- package/dist/{storage-meta-session-df9tstzr.js → storage-meta-session-mk4gsnke.js} +4 -3
- package/dist/tools/ctx-memory/tools.d.ts.map +1 -1
- package/dist/tools/ctx-note/tools.d.ts.map +1 -1
- package/dist/tui/data/context-db.d.ts.map +1 -1
- package/dist/tui-compiled/data/context-db.d.ts.map +1 -1
- package/package.json +3 -1
- package/src/config/index.ts +2 -0
- package/src/config/latch-permanence-guard.test.ts +218 -0
- package/src/config/project-security.ts +10 -1
- package/src/config/schema/magic-context.ts +12 -2
- package/src/shared/__fixtures__/window-overlay.v1.json +337 -0
- package/src/shared/conflict-detector.test.ts +176 -1
- package/src/shared/conflict-detector.ts +126 -3
- package/src/shared/harness-provider-map.test.ts +20 -0
- package/src/shared/harness-provider-map.ts +19 -0
- package/src/shared/logger.test.ts +119 -0
- package/src/shared/logger.ts +33 -15
- package/src/shared/models-dev-cache.test.ts +42 -0
- package/src/shared/models-dev-cache.ts +62 -9
- package/src/shared/prompt-surface-runtime.ts +5 -3
- package/src/shared/prompt-surface.test.ts +38 -0
- package/src/shared/prompt-surface.ts +15 -2
- package/src/shared/rpc-client.ts +4 -2
- package/src/shared/rpc-server.ts +1 -0
- package/src/shared/rpc-types.ts +30 -0
- package/src/shared/rpc-utils.test.ts +162 -13
- package/src/shared/rpc-utils.ts +224 -31
- package/src/shared/tail-hygiene-status.ts +57 -0
- package/src/shared/window-geometry.test.ts +342 -0
- package/src/shared/window-geometry.ts +626 -0
- package/src/tui/data/context-db.ts +5 -0
- package/src/tui/index.tsx +33 -1
- package/src/tui/slots/sidebar-content.tsx +17 -5
- package/src/tui-compiled/data/context-db.ts +5 -0
- package/src/tui-compiled/index.tsx +359 -273
- package/src/tui-compiled/slots/sidebar-content.tsx +316 -287
- package/dist/index-wgvxkqpx.js +0 -342
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { readJsoncFile } from "./jsonc-parser";
|
|
4
|
+
import { log } from "./logger";
|
|
4
5
|
import { getOpenCodeConfigPaths } from "./opencode-config-dir";
|
|
5
6
|
|
|
6
7
|
interface OpenCodeConfig {
|
|
@@ -51,6 +52,18 @@ export interface ConflictResult {
|
|
|
51
52
|
};
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Resolved native compaction state, as reported by the host's own config
|
|
57
|
+
* resolution (ctx.client.config.get() — the same object `opencode debug
|
|
58
|
+
* config` prints). `auto`/`prune` are booleans; the OpenCode schema annotates
|
|
59
|
+
* `auto` with default `true` and `prune` with default `false`, so an absent
|
|
60
|
+
* compaction block resolves to `{ auto: true, prune: false }`.
|
|
61
|
+
*/
|
|
62
|
+
export interface ResolvedCompaction {
|
|
63
|
+
auto: boolean;
|
|
64
|
+
prune: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
54
67
|
/**
|
|
55
68
|
* Options for {@link detectConflicts}.
|
|
56
69
|
*
|
|
@@ -63,6 +76,13 @@ export interface ConflictResult {
|
|
|
63
76
|
* omit it and accept the default `true` (mode-on) behavior, which preserves
|
|
64
77
|
* today's conflict semantics; it must never silently skip the check.
|
|
65
78
|
*
|
|
79
|
+
* `resolvedCompaction` is the host's RESOLVED native compaction state
|
|
80
|
+
* (fetched via {@link resolveCompactionForBoot}). When present, the
|
|
81
|
+
* file-based {@link checkCompaction} is NOT called — the resolved values are
|
|
82
|
+
* used directly. Absent means the file-based fallback is used. The
|
|
83
|
+
* OPENCODE_DISABLE_AUTOCOMPACT env short-circuit is applied on top of
|
|
84
|
+
* whichever arm produced the value.
|
|
85
|
+
*
|
|
66
86
|
* When `compactionEnabled` is `false` (compaction-off mode), OpenCode
|
|
67
87
|
* `compaction.auto=true` / `compaction.prune=true` are NOT plugin-disabling
|
|
68
88
|
* conflicts — native compaction is the user's chosen window manager. DCP
|
|
@@ -71,6 +91,7 @@ export interface ConflictResult {
|
|
|
71
91
|
*/
|
|
72
92
|
export interface DetectConflictsOptions {
|
|
73
93
|
compactionEnabled?: boolean;
|
|
94
|
+
resolvedCompaction?: ResolvedCompaction;
|
|
74
95
|
}
|
|
75
96
|
|
|
76
97
|
/**
|
|
@@ -81,6 +102,11 @@ export interface DetectConflictsOptions {
|
|
|
81
102
|
* When `false` (compaction-off mode), native `compaction.auto`/`prune` are
|
|
82
103
|
* reported in {@link ConflictResult.nativeCompaction} but are NOT flagged as
|
|
83
104
|
* conflicts — native compaction is the intended window manager in that mode.
|
|
105
|
+
*
|
|
106
|
+
* `resolvedCompaction` (optional) is the host's RESOLVED native compaction
|
|
107
|
+
* state from {@link resolveCompactionForBoot}. When present it is used
|
|
108
|
+
* directly and the file-based {@link checkCompaction} is skipped; when absent
|
|
109
|
+
* the file-based fallback runs unchanged.
|
|
84
110
|
*/
|
|
85
111
|
export function detectConflicts(
|
|
86
112
|
directory: string,
|
|
@@ -98,18 +124,38 @@ export function detectConflicts(
|
|
|
98
124
|
const reasons: string[] = [];
|
|
99
125
|
|
|
100
126
|
// --- Check OpenCode compaction config ---
|
|
101
|
-
|
|
127
|
+
// The host's resolved config is the authority when available (issue #309:
|
|
128
|
+
// the file-based re-derivation defaults to auto=true when no file resolves,
|
|
129
|
+
// wrongly disabling the plugin for users whose auto=false lives in a layer
|
|
130
|
+
// the file reader cannot see). When the resolved fetch failed, fall back to
|
|
131
|
+
// the file-based check unchanged.
|
|
132
|
+
let compactionResult = options?.resolvedCompaction ?? checkCompaction(directory);
|
|
133
|
+
// OPENCODE_DISABLE_AUTOCOMPACT short-circuits BOTH arms: it is the first,
|
|
134
|
+
// cheapest check and is correct regardless of which arm produced the value.
|
|
135
|
+
// (checkCompaction already applies it internally for the file arm; this
|
|
136
|
+
// covers the resolved arm, which skips checkCompaction entirely.)
|
|
137
|
+
if (process.env.OPENCODE_DISABLE_AUTOCOMPACT) {
|
|
138
|
+
compactionResult = { auto: false, prune: false };
|
|
139
|
+
}
|
|
102
140
|
// Native compaction is a conflict ONLY when MC compaction is ON. In
|
|
103
141
|
// compaction-off mode the user has explicitly handed the window to native
|
|
104
142
|
// compaction (or nothing), so compaction.auto=true / prune=true are the
|
|
105
143
|
// intended state, not a plugin-disabling conflict.
|
|
106
144
|
if (compactionEnabled && compactionResult.auto) {
|
|
107
145
|
conflicts.compactionAuto = true;
|
|
108
|
-
reasons.push(
|
|
146
|
+
reasons.push(
|
|
147
|
+
options?.resolvedCompaction
|
|
148
|
+
? "OpenCode auto-compaction is enabled (compaction.auto=true) (resolved config)"
|
|
149
|
+
: "OpenCode auto-compaction is enabled (compaction.auto=true)",
|
|
150
|
+
);
|
|
109
151
|
}
|
|
110
152
|
if (compactionEnabled && compactionResult.prune) {
|
|
111
153
|
conflicts.compactionPrune = true;
|
|
112
|
-
reasons.push(
|
|
154
|
+
reasons.push(
|
|
155
|
+
options?.resolvedCompaction
|
|
156
|
+
? "OpenCode prune is enabled (compaction.prune=true) (resolved config)"
|
|
157
|
+
: "OpenCode prune is enabled (compaction.prune=true)",
|
|
158
|
+
);
|
|
113
159
|
}
|
|
114
160
|
|
|
115
161
|
// --- Check for DCP plugin ---
|
|
@@ -152,6 +198,83 @@ export function detectConflicts(
|
|
|
152
198
|
|
|
153
199
|
// --- Compaction detection (extracted from opencode-compaction-detector.ts) ---
|
|
154
200
|
|
|
201
|
+
/**
|
|
202
|
+
* Minimal shape of the OpenCode SDK client's `config.get()` response. The
|
|
203
|
+
* SDK's generated `Config` type does not declare a `compaction` field (the
|
|
204
|
+
* schema lives in OpenCode's core config, not the SDK surface), so we read it
|
|
205
|
+
* defensively at runtime. `config.get()` returns a `RequestResult` whose
|
|
206
|
+
* `data` is the resolved config object — the same object `opencode debug
|
|
207
|
+
* config` prints. The `data` is typed as `unknown` here so the real SDK client
|
|
208
|
+
* (whose `Config` has no `compaction` key) is structurally assignable.
|
|
209
|
+
*/
|
|
210
|
+
export interface OpencodeConfigClientLike {
|
|
211
|
+
config: {
|
|
212
|
+
get: () => Promise<{ data?: unknown }>;
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Shape of the `compaction` block inside the resolved config, read defensively
|
|
218
|
+
* from the SDK response at runtime.
|
|
219
|
+
*/
|
|
220
|
+
interface ResolvedCompactionBlock {
|
|
221
|
+
compaction?: {
|
|
222
|
+
auto?: boolean;
|
|
223
|
+
prune?: boolean;
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Fetch the host's RESOLVED native compaction state from the OpenCode SDK
|
|
229
|
+
* client (`ctx.client.config.get()`). This is the authority for the plugin's
|
|
230
|
+
* conflict decision (issue #309): the file-based re-derivation cannot see
|
|
231
|
+
* every layer OpenCode folds in (env-var config path, managed configs,
|
|
232
|
+
* multi-file merge), so any user whose `auto=false` lives in a layer we don't
|
|
233
|
+
* read would be wrongly flagged. We never re-derive what the host will tell
|
|
234
|
+
* us.
|
|
235
|
+
*
|
|
236
|
+
* A response WITHOUT a compaction block is INCONCLUSIVE, not "host defaults
|
|
237
|
+
* apply": a server whose `/config` shape drifted (OpenCode Desktop bundles
|
|
238
|
+
* its own server version) or a fetch racing boot returns data with no
|
|
239
|
+
* `compaction` key, and reading that absence as `auto=true` disables the
|
|
240
|
+
* plugin — the one wrong direction, because a false disable leaves NOTHING
|
|
241
|
+
* managing the window and every long session overflows (issue #309, second
|
|
242
|
+
* arm). Only an explicit boolean from the host resolves this arm; anything
|
|
243
|
+
* else returns `null` so the caller falls back to the file-based check,
|
|
244
|
+
* which reads the layers the user actually wrote.
|
|
245
|
+
*
|
|
246
|
+
* Returns `null` when the fetch fails, times out (bounded to `timeoutMs` so
|
|
247
|
+
* boot never hangs), or serves no explicit compaction block.
|
|
248
|
+
*/
|
|
249
|
+
export async function resolveCompactionForBoot(
|
|
250
|
+
client: OpencodeConfigClientLike,
|
|
251
|
+
timeoutMs = 2_000,
|
|
252
|
+
): Promise<ResolvedCompaction | null> {
|
|
253
|
+
try {
|
|
254
|
+
const result = await Promise.race([
|
|
255
|
+
client.config.get(),
|
|
256
|
+
new Promise<never>((_, reject) =>
|
|
257
|
+
setTimeout(() => reject(new Error("config.get() timed out")), timeoutMs),
|
|
258
|
+
),
|
|
259
|
+
]);
|
|
260
|
+
// The SDK's generated `Config` type has no `compaction` key, so read it
|
|
261
|
+
// defensively from the runtime response.
|
|
262
|
+
const compaction = (result?.data as ResolvedCompactionBlock | undefined)?.compaction;
|
|
263
|
+
// Explicit booleans only. An absent block (or non-boolean values) means
|
|
264
|
+
// the response shape did not carry the resolved state — fall back to the
|
|
265
|
+
// file arm rather than resolving to the plugin-disabling default.
|
|
266
|
+
if (typeof compaction?.auto !== "boolean" || typeof compaction?.prune !== "boolean") {
|
|
267
|
+
log(
|
|
268
|
+
`[magic-context] conflict-detector: resolved config carried no explicit compaction block (${JSON.stringify(compaction) ?? "absent"}); falling back to file-based detection`,
|
|
269
|
+
);
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
return { auto: compaction.auto, prune: compaction.prune };
|
|
273
|
+
} catch {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
155
278
|
function checkCompaction(directory: string): { auto: boolean; prune: boolean } {
|
|
156
279
|
if (process.env.OPENCODE_DISABLE_AUTOCOMPACT) {
|
|
157
280
|
return { auto: false, prune: false };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import {
|
|
3
|
+
modelRefLookupOrder,
|
|
3
4
|
ompModelRefToCanonical,
|
|
4
5
|
piModelRefToCanonical,
|
|
5
6
|
resolveModelRefForOmp,
|
|
@@ -64,6 +65,25 @@ describe("harness-provider-map", () => {
|
|
|
64
65
|
const piForm = "openai-codex/gpt-5.5";
|
|
65
66
|
expect(resolveModelRefForPi(piModelRefToCanonical(piForm))).toBe(piForm);
|
|
66
67
|
});
|
|
68
|
+
|
|
69
|
+
describe("modelRefLookupOrder (config read edge)", () => {
|
|
70
|
+
it("tries canonical before the Pi-native spelling", () => {
|
|
71
|
+
expect(modelRefLookupOrder("openai-codex/gpt-5.6-sol")).toEqual([
|
|
72
|
+
"openai/gpt-5.6-sol",
|
|
73
|
+
"openai-codex/gpt-5.6-sol",
|
|
74
|
+
]);
|
|
75
|
+
expect(modelRefLookupOrder("openai/gpt-5.6-sol")).toEqual([
|
|
76
|
+
"openai/gpt-5.6-sol",
|
|
77
|
+
"openai-codex/gpt-5.6-sol",
|
|
78
|
+
]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("keeps unknown provider prefixes as a single passthrough key", () => {
|
|
82
|
+
expect(modelRefLookupOrder("custom-provider/model")).toEqual([
|
|
83
|
+
"custom-provider/model",
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
67
87
|
});
|
|
68
88
|
});
|
|
69
89
|
|
|
@@ -77,6 +77,25 @@ export function resolveModelRefForPi(ref: string): string {
|
|
|
77
77
|
return remapProviderPrefix(piModelRefToCanonical(ref), CANONICAL_TO_PI_PROVIDER);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Return every known spelling of a model reference with the canonical shared
|
|
82
|
+
* form first. The raw input remains the first fallback, so a single config file
|
|
83
|
+
* works on every harness: canonical wins when both spellings are present, while
|
|
84
|
+
* Pi/OMP-native provider ids are still accepted at the read edge. Unknown
|
|
85
|
+
* providers pass through unchanged and therefore produce one candidate.
|
|
86
|
+
*/
|
|
87
|
+
export function modelRefLookupOrder(ref: string): string[] {
|
|
88
|
+
const canonical = piModelRefToCanonical(ompModelRefToCanonical(ref));
|
|
89
|
+
return [
|
|
90
|
+
...new Set([
|
|
91
|
+
canonical,
|
|
92
|
+
ref,
|
|
93
|
+
resolveModelRefForPi(canonical),
|
|
94
|
+
resolveModelRefForOmp(canonical),
|
|
95
|
+
]),
|
|
96
|
+
];
|
|
97
|
+
}
|
|
98
|
+
|
|
80
99
|
/** OMP-native selector -> canonical shared-config model reference. */
|
|
81
100
|
export function ompModelRefToCanonical(ref: string): string {
|
|
82
101
|
return remapProviderPrefix(ref, OMP_TO_CANONICAL_PROVIDER);
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
|
|
6
|
+
type LoggerScenarioResult = {
|
|
7
|
+
exists: boolean;
|
|
8
|
+
content: string;
|
|
9
|
+
healthyDiagnostics: {
|
|
10
|
+
swallowedWriteCount: number;
|
|
11
|
+
lastErrorMessage: string | null;
|
|
12
|
+
lastErrorTime: string | null;
|
|
13
|
+
};
|
|
14
|
+
failedDiagnostics: {
|
|
15
|
+
swallowedWriteCount: number;
|
|
16
|
+
lastErrorMessage: string | null;
|
|
17
|
+
lastErrorTime: string | null;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const loggerScenario = `
|
|
22
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
23
|
+
import * as path from "node:path";
|
|
24
|
+
|
|
25
|
+
const root = process.env.LOGGER_SCENARIO_ROOT;
|
|
26
|
+
const loggerModuleUrl = process.env.LOGGER_MODULE_URL;
|
|
27
|
+
if (!root || !loggerModuleUrl) throw new Error("logger scenario environment is incomplete");
|
|
28
|
+
|
|
29
|
+
const logger = await import(loggerModuleUrl);
|
|
30
|
+
const logPath = path.join(root, "nested", "magic-context.log");
|
|
31
|
+
process.env.MAGIC_CONTEXT_LOG_PATH = logPath;
|
|
32
|
+
logger.log("first");
|
|
33
|
+
logger.flushLogger();
|
|
34
|
+
const healthyDiagnostics = logger.getLoggerDiagnostics();
|
|
35
|
+
|
|
36
|
+
const logDirectory = path.dirname(logPath);
|
|
37
|
+
if (process.env.LOGGER_SCENARIO === "recovery") {
|
|
38
|
+
const { rmSync } = await import("node:fs");
|
|
39
|
+
rmSync(logDirectory, { recursive: true, force: true });
|
|
40
|
+
logger.log("second");
|
|
41
|
+
logger.flushLogger();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// A directory used as the file target makes append fail deterministically on every platform.
|
|
45
|
+
const failedPath = path.join(root, "unwritable-log-target");
|
|
46
|
+
const { mkdirSync } = await import("node:fs");
|
|
47
|
+
mkdirSync(failedPath, { recursive: true });
|
|
48
|
+
process.env.MAGIC_CONTEXT_LOG_PATH = failedPath;
|
|
49
|
+
logger.log("failed write");
|
|
50
|
+
logger.flushLogger();
|
|
51
|
+
const failedDiagnostics = logger.getLoggerDiagnostics();
|
|
52
|
+
|
|
53
|
+
const content = existsSync(logPath) ? readFileSync(logPath, "utf8") : "";
|
|
54
|
+
console.log(JSON.stringify({
|
|
55
|
+
exists: existsSync(logPath),
|
|
56
|
+
content,
|
|
57
|
+
healthyDiagnostics,
|
|
58
|
+
failedDiagnostics,
|
|
59
|
+
}));
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
const scenarioRoots: string[] = [];
|
|
63
|
+
|
|
64
|
+
afterEach(() => {
|
|
65
|
+
for (const root of scenarioRoots.splice(0)) {
|
|
66
|
+
rmSync(root, { recursive: true, force: true });
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
async function runLoggerScenario(
|
|
71
|
+
scenario: "recovery" | "diagnostics",
|
|
72
|
+
): Promise<LoggerScenarioResult> {
|
|
73
|
+
const root = mkdtempSync(path.join(os.tmpdir(), "magic-context-logger-test-"));
|
|
74
|
+
scenarioRoots.push(root);
|
|
75
|
+
const child = Bun.spawn({
|
|
76
|
+
cmd: ["bun", "--eval", loggerScenario],
|
|
77
|
+
cwd: import.meta.dir,
|
|
78
|
+
env: {
|
|
79
|
+
...process.env,
|
|
80
|
+
NODE_ENV: "production",
|
|
81
|
+
LOGGER_MODULE_URL: new URL("./logger.ts", import.meta.url).href,
|
|
82
|
+
LOGGER_SCENARIO: scenario,
|
|
83
|
+
LOGGER_SCENARIO_ROOT: root,
|
|
84
|
+
},
|
|
85
|
+
stdout: "pipe",
|
|
86
|
+
stderr: "pipe",
|
|
87
|
+
});
|
|
88
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
89
|
+
child.exited,
|
|
90
|
+
new Response(child.stdout).text(),
|
|
91
|
+
new Response(child.stderr).text(),
|
|
92
|
+
]);
|
|
93
|
+
expect(exitCode, stderr).toBe(0);
|
|
94
|
+
return JSON.parse(stdout.trim()) as LoggerScenarioResult;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
describe("logger", () => {
|
|
98
|
+
test("recreates a log directory removed while the process is running", async () => {
|
|
99
|
+
const result = await runLoggerScenario("recovery");
|
|
100
|
+
|
|
101
|
+
expect(result.exists).toBe(true);
|
|
102
|
+
expect(result.content).toContain("second");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("reports swallowed writes while healthy writes leave the counter at zero", async () => {
|
|
106
|
+
const result = await runLoggerScenario("diagnostics");
|
|
107
|
+
|
|
108
|
+
expect(result.healthyDiagnostics).toEqual({
|
|
109
|
+
swallowedWriteCount: 0,
|
|
110
|
+
lastErrorMessage: null,
|
|
111
|
+
lastErrorTime: null,
|
|
112
|
+
});
|
|
113
|
+
expect(result.failedDiagnostics.swallowedWriteCount).toBe(1);
|
|
114
|
+
expect(result.failedDiagnostics.lastErrorMessage).toBeTruthy();
|
|
115
|
+
expect(result.failedDiagnostics.lastErrorTime).toMatch(
|
|
116
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/,
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
});
|
package/src/shared/logger.ts
CHANGED
|
@@ -9,25 +9,30 @@ let flushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
9
9
|
const FLUSH_INTERVAL_MS = 500;
|
|
10
10
|
const BUFFER_SIZE_LIMIT = 50;
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
let lastEnsuredDir: string | null = null;
|
|
12
|
+
export interface LoggerDiagnostics {
|
|
13
|
+
swallowedWriteCount: number;
|
|
14
|
+
lastErrorMessage: string | null;
|
|
15
|
+
lastErrorTime: string | null;
|
|
16
|
+
}
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
let swallowedWriteCount = 0;
|
|
19
|
+
let lastErrorMessage: string | null = null;
|
|
20
|
+
let lastErrorTime: string | null = null;
|
|
21
|
+
|
|
22
|
+
function recordSwallowedWrite(error: unknown): void {
|
|
22
23
|
try {
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
swallowedWriteCount++;
|
|
25
|
+
lastErrorMessage = error instanceof Error ? error.message : String(error);
|
|
26
|
+
lastErrorTime = new Date().toISOString();
|
|
25
27
|
} catch {
|
|
26
|
-
//
|
|
27
|
-
// try the append; failure there is also swallowed.
|
|
28
|
+
// Diagnostics must not make the logger throw either.
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
function ensureDir(filePath: string): void {
|
|
33
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
|
|
31
36
|
function flush(): void {
|
|
32
37
|
if (flushTimer) {
|
|
33
38
|
clearTimeout(flushTimer);
|
|
@@ -40,8 +45,8 @@ function flush(): void {
|
|
|
40
45
|
const logFile = getMagicContextLogPath();
|
|
41
46
|
ensureDir(logFile);
|
|
42
47
|
fs.appendFileSync(logFile, data);
|
|
43
|
-
} catch {
|
|
44
|
-
|
|
48
|
+
} catch (error) {
|
|
49
|
+
recordSwallowedWrite(error);
|
|
45
50
|
}
|
|
46
51
|
}
|
|
47
52
|
|
|
@@ -78,6 +83,19 @@ export function sessionLog(sessionId: string, message: string, data?: unknown):
|
|
|
78
83
|
log(`[magic-context][${sessionId}] ${message}`, data);
|
|
79
84
|
}
|
|
80
85
|
|
|
86
|
+
export function getLoggerDiagnostics(): LoggerDiagnostics {
|
|
87
|
+
return {
|
|
88
|
+
swallowedWriteCount,
|
|
89
|
+
lastErrorMessage,
|
|
90
|
+
lastErrorTime,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Flush buffered log entries immediately. Primarily useful to diagnostic readers and tests. */
|
|
95
|
+
export function flushLogger(): void {
|
|
96
|
+
flush();
|
|
97
|
+
}
|
|
98
|
+
|
|
81
99
|
/**
|
|
82
100
|
* Resolve the current log file path. The path is harness-aware (see
|
|
83
101
|
* {@link getMagicContextLogPath}) and re-evaluated on every call, so callers
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
getModelsDevCacheState,
|
|
8
8
|
getSdkContextLimit,
|
|
9
9
|
getSdkInputLimit,
|
|
10
|
+
getSdkWindowGeometry,
|
|
10
11
|
refreshModelLimitsAfterAuthOnce,
|
|
11
12
|
refreshModelLimitsFromApi,
|
|
12
13
|
resetAuthRewarmLatchForTest,
|
|
@@ -78,6 +79,23 @@ describe("output-token reservation", () => {
|
|
|
78
79
|
).toBe(1_048_576);
|
|
79
80
|
});
|
|
80
81
|
|
|
82
|
+
test("output_reserve accepts both harness provider spellings with canonical precedence", () => {
|
|
83
|
+
const limit = { context: 100_000, output: 20_000 };
|
|
84
|
+
expect(
|
|
85
|
+
resolveLimit(limit, "openai-codex", "gpt-5.6-sol", {
|
|
86
|
+
default: 0,
|
|
87
|
+
"openai-codex/gpt-5.6-sol": 8_000,
|
|
88
|
+
}),
|
|
89
|
+
).toBe(92_000);
|
|
90
|
+
expect(
|
|
91
|
+
resolveLimit(limit, "openai-codex", "gpt-5.6-sol", {
|
|
92
|
+
default: 0,
|
|
93
|
+
"openai-codex/gpt-5.6-sol": 8_000,
|
|
94
|
+
"openai/gpt-5.6-sol": 4_000,
|
|
95
|
+
}),
|
|
96
|
+
).toBe(96_000);
|
|
97
|
+
});
|
|
98
|
+
|
|
81
99
|
test("output_reserve overrides shared and separate quota defaults", () => {
|
|
82
100
|
expect(resolveLimit({ context: 100_000, output: 20_000 }, "anthropic", "claude", 0)).toBe(
|
|
83
101
|
100_000,
|
|
@@ -131,6 +149,30 @@ describe("models-dev-cache (SDK-only)", () => {
|
|
|
131
149
|
clearModelsDevCache();
|
|
132
150
|
});
|
|
133
151
|
|
|
152
|
+
test("honors the reporter's default output_reserve on the SDK geometry path", async () => {
|
|
153
|
+
await refreshModelLimitsFromApi(
|
|
154
|
+
makeClient([
|
|
155
|
+
{
|
|
156
|
+
id: "openai-codex",
|
|
157
|
+
models: {
|
|
158
|
+
"gpt-5.6-sol": {
|
|
159
|
+
limit: { context: 400_000, input: 272_000, output: 128_000 },
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
]),
|
|
164
|
+
);
|
|
165
|
+
setOutputReserveConfig({ default: 16_384 });
|
|
166
|
+
|
|
167
|
+
const geometry = getSdkWindowGeometry("openai-codex", "gpt-5.6-sol");
|
|
168
|
+
expect(geometry?.usableSoft).toBe(272_000 - 16_384);
|
|
169
|
+
expect(geometry?.derivation).toMatchObject({
|
|
170
|
+
window: 272_000,
|
|
171
|
+
reserve: 16_384,
|
|
172
|
+
reserveSource: "output_config",
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
134
176
|
test("resolves from the SDK and prefers limit.input over limit.context", async () => {
|
|
135
177
|
// github-copilot / codex shape: input is the max prompt, context the total
|
|
136
178
|
// window. Pressure math must use the input cap (OpenCode's own overflow.ts
|
|
@@ -30,9 +30,15 @@ import { join } from "node:path";
|
|
|
30
30
|
import type { ContextLimitProvenance } from "./context-limit-provenance";
|
|
31
31
|
import { getMagicContextStorageDir } from "./data-path";
|
|
32
32
|
import { getHarness } from "./harness";
|
|
33
|
-
import {
|
|
33
|
+
import { modelRefLookupOrder } from "./harness-provider-map";
|
|
34
34
|
import { sessionLog } from "./logger";
|
|
35
35
|
import { shouldEnforcePrivateStoragePermissions } from "./storage-permissions";
|
|
36
|
+
import {
|
|
37
|
+
deriveWindowGeometry,
|
|
38
|
+
getWindowOverlay,
|
|
39
|
+
resolveWindowOverlayFacts,
|
|
40
|
+
type WindowGeometryResult,
|
|
41
|
+
} from "./window-geometry";
|
|
36
42
|
|
|
37
43
|
interface OpencodeClientLike {
|
|
38
44
|
config: {
|
|
@@ -196,22 +202,20 @@ function isFinitePositive(value: number | undefined): value is number {
|
|
|
196
202
|
}
|
|
197
203
|
|
|
198
204
|
function modelKeyLookupOrder(providerID: string, modelID: string): string[] {
|
|
199
|
-
const
|
|
200
|
-
const canonicalFull = piModelRefToCanonical(full);
|
|
201
|
-
const candidates = [full, canonicalFull, modelID];
|
|
205
|
+
const candidates = [...modelRefLookupOrder(`${providerID}/${modelID}`), modelID];
|
|
202
206
|
const colon = modelID.lastIndexOf(":");
|
|
203
207
|
if (colon > 0) {
|
|
204
208
|
const bareModel = modelID.slice(0, colon);
|
|
205
|
-
|
|
206
|
-
candidates.push(providerBare, piModelRefToCanonical(providerBare), bareModel);
|
|
209
|
+
candidates.push(...modelRefLookupOrder(`${providerID}/${bareModel}`), bareModel);
|
|
207
210
|
}
|
|
208
211
|
return [...new Set(candidates)];
|
|
209
212
|
}
|
|
210
213
|
|
|
211
|
-
|
|
212
|
-
|
|
214
|
+
/** Resolve the user-tier output reservation for one runtime model. */
|
|
215
|
+
export function resolveOutputReserve(
|
|
213
216
|
providerID: string,
|
|
214
217
|
modelID: string,
|
|
218
|
+
config: OutputReserveConfig | undefined = outputReserveConfig,
|
|
215
219
|
): number | undefined {
|
|
216
220
|
if (typeof config === "number")
|
|
217
221
|
return Number.isFinite(config) && config >= 0 ? config : undefined;
|
|
@@ -256,7 +260,7 @@ export function resolveLimit(
|
|
|
256
260
|
if (input !== undefined && (context === undefined || input < context)) return input;
|
|
257
261
|
if (context === undefined) return undefined;
|
|
258
262
|
|
|
259
|
-
const configuredReserve =
|
|
263
|
+
const configuredReserve = resolveOutputReserve(providerID, modelID, reserveConfig);
|
|
260
264
|
let reserve: number;
|
|
261
265
|
if (configuredReserve !== undefined) {
|
|
262
266
|
reserve = configuredReserve;
|
|
@@ -493,6 +497,50 @@ async function refreshModelLimitsOnce(client: OpencodeClientLike): Promise<boole
|
|
|
493
497
|
* `ctx.model.contextWindow`), so for Pi this returns `undefined` and Pi's
|
|
494
498
|
* own resolution path is used.
|
|
495
499
|
*/
|
|
500
|
+
export function getSdkWindowGeometry(
|
|
501
|
+
providerID: string,
|
|
502
|
+
modelID: string,
|
|
503
|
+
detectedContextLimit?: number,
|
|
504
|
+
options?: {
|
|
505
|
+
detectedLimitProvenance?: ContextLimitProvenance;
|
|
506
|
+
harness?: "opencode" | "pi";
|
|
507
|
+
},
|
|
508
|
+
): WindowGeometryResult | undefined {
|
|
509
|
+
loadPersistedApiCacheOnce();
|
|
510
|
+
const metadata = lookupMetadataWithTagFallback(apiCache, providerID, modelID);
|
|
511
|
+
if (!metadata) return undefined;
|
|
512
|
+
const rawContext = metadata.contextLimit ?? metadata.limit;
|
|
513
|
+
const promptOnlyDetected =
|
|
514
|
+
options?.detectedLimitProvenance === "prompt_only" && isFinitePositive(detectedContextLimit)
|
|
515
|
+
? detectedContextLimit
|
|
516
|
+
: undefined;
|
|
517
|
+
const result = deriveWindowGeometry(
|
|
518
|
+
providerID,
|
|
519
|
+
modelID,
|
|
520
|
+
{
|
|
521
|
+
context: rawContext,
|
|
522
|
+
input: metadata.inputLimit,
|
|
523
|
+
output: metadata.outputLimit,
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
overlay: resolveWindowOverlayFacts(providerID, modelID, getWindowOverlay()),
|
|
527
|
+
outputReserveOverride: resolveOutputReserve(providerID, modelID),
|
|
528
|
+
harness: options?.harness ?? "opencode",
|
|
529
|
+
contextCap:
|
|
530
|
+
promptOnlyDetected === undefined && isFinitePositive(detectedContextLimit)
|
|
531
|
+
? detectedContextLimit
|
|
532
|
+
: undefined,
|
|
533
|
+
},
|
|
534
|
+
);
|
|
535
|
+
if (!result || promptOnlyDetected === undefined) return result;
|
|
536
|
+
const usableSoft = promptOnlyDetected;
|
|
537
|
+
return {
|
|
538
|
+
...result,
|
|
539
|
+
usableSoft,
|
|
540
|
+
usableHard: Math.max(usableSoft, Math.min(result.usableHard, promptOnlyDetected)),
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
496
544
|
export function getSdkContextLimit(
|
|
497
545
|
providerID: string,
|
|
498
546
|
modelID: string,
|
|
@@ -502,6 +550,11 @@ export function getSdkContextLimit(
|
|
|
502
550
|
detectedLimitProvenance?: ContextLimitProvenance;
|
|
503
551
|
},
|
|
504
552
|
): number | undefined {
|
|
553
|
+
if (options?.reservation !== "none") {
|
|
554
|
+
return getSdkWindowGeometry(providerID, modelID, detectedContextLimit, {
|
|
555
|
+
detectedLimitProvenance: options?.detectedLimitProvenance,
|
|
556
|
+
})?.usableSoft;
|
|
557
|
+
}
|
|
505
558
|
loadPersistedApiCacheOnce();
|
|
506
559
|
const metadata = lookupMetadataWithTagFallback(apiCache, providerID, modelID);
|
|
507
560
|
if (!metadata) return undefined;
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
CTX_REDUCE_LIGHT_DESCRIPTION,
|
|
14
14
|
CTX_SEARCH_LIGHT_DESCRIPTION,
|
|
15
15
|
} from "../tools/light-descriptions";
|
|
16
|
+
import { piModelRefToCanonical } from "./harness-provider-map";
|
|
16
17
|
import { detectConfigFile } from "./jsonc-parser";
|
|
17
18
|
import {
|
|
18
19
|
type PromptSurfaceConfig,
|
|
@@ -242,13 +243,14 @@ export function createPromptSurfaceGuidanceEpochCache(runtime: PromptSurfaceRunt
|
|
|
242
243
|
|
|
243
244
|
return {
|
|
244
245
|
resolve(sessionId, config, modelKey) {
|
|
246
|
+
const canonicalModelKey = modelKey ? piModelRefToCanonical(modelKey) : undefined;
|
|
245
247
|
const cached = epochs.get(sessionId);
|
|
246
|
-
if (cached && cached.config === config && cached.modelKey ===
|
|
248
|
+
if (cached && cached.config === config && cached.modelKey === canonicalModelKey) {
|
|
247
249
|
return cached.selection;
|
|
248
250
|
}
|
|
249
251
|
|
|
250
|
-
const selection = runtime.resolveGuidance(config,
|
|
251
|
-
epochs.set(sessionId, { config, modelKey, selection });
|
|
252
|
+
const selection = runtime.resolveGuidance(config, canonicalModelKey);
|
|
253
|
+
epochs.set(sessionId, { config, modelKey: canonicalModelKey, selection });
|
|
252
254
|
return selection;
|
|
253
255
|
},
|
|
254
256
|
clear(sessionId) {
|