@cortexkit/opencode-magic-context 0.36.0 → 0.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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-dw9fs934.js → conflict-warning-hook-an4fxhwf.js} +4 -4
- package/dist/{derive-budgets-88mxk2w1.js → derive-budgets-0f8xpyw2.js} +3 -2
- package/dist/features/magic-context/context-authority.d.ts +12 -0
- package/dist/features/magic-context/context-authority.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/classify.d.ts +2 -0
- package/dist/features/magic-context/dreamer/classify.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/evaluate-smart-notes.d.ts +2 -0
- package/dist/features/magic-context/dreamer/evaluate-smart-notes.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/lease.d.ts +10 -3
- package/dist/features/magic-context/dreamer/lease.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/map-memories.d.ts +2 -0
- package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/module-apply.d.ts +5 -0
- package/dist/features/magic-context/dreamer/module-apply.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/promote-primers.d.ts +2 -0
- package/dist/features/magic-context/dreamer/promote-primers.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/refresh-primers.d.ts +2 -0
- package/dist/features/magic-context/dreamer/refresh-primers.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 +5 -2
- package/dist/features/magic-context/dreamer/task-scheduler.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/verify.d.ts +2 -0
- package/dist/features/magic-context/dreamer/verify.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/message-index-async.d.ts.map +1 -1
- package/dist/features/magic-context/migrations.d.ts.map +1 -1
- package/dist/features/magic-context/mural/compress-cues.d.ts +2 -0
- package/dist/features/magic-context/mural/compress-cues.d.ts.map +1 -1
- package/dist/features/magic-context/overflow-detection.d.ts.map +1 -1
- package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
- package/dist/features/magic-context/schema-fence-probe.d.ts +1 -0
- package/dist/features/magic-context/schema-fence-probe.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/condition-compiler.d.ts +1 -0
- package/dist/features/magic-context/smart-notes/condition-compiler.d.ts.map +1 -1
- 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-clone.d.ts +6 -0
- package/dist/features/magic-context/storage-clone.d.ts.map +1 -1
- package/dist/features/magic-context/storage-db.d.ts +14 -3
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage-identity-merge.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-persisted.d.ts +22 -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 +2 -0
- package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
- package/dist/features/magic-context/storage.d.ts +1 -1
- package/dist/features/magic-context/storage.d.ts.map +1 -1
- package/dist/features/magic-context/user-memory/review-user-memories.d.ts +2 -0
- package/dist/features/magic-context/user-memory/review-user-memories.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/magic-context/child-session-spawn.d.ts +1 -0
- package/dist/hooks/magic-context/child-session-spawn.d.ts.map +1 -1
- package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/compaction-off-transition.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-render-epoch.d.ts +5 -3
- package/dist/hooks/magic-context/compartment-render-epoch.d.ts.map +1 -1
- package/dist/hooks/magic-context/embed-session-state.d.ts +1 -1
- package/dist/hooks/magic-context/embed-session-state.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 +2 -1
- package/dist/hooks/magic-context/execute-status.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 +8 -0
- package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-replay.d.ts +5 -2
- 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 +9 -0
- package/dist/hooks/magic-context/module-state-sync.d.ts.map +1 -1
- package/dist/hooks/magic-context/module-transport.d.ts +20 -3
- 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/read-session-formatting.d.ts +1 -0
- package/dist/hooks/magic-context/read-session-formatting.d.ts.map +1 -1
- package/dist/hooks/magic-context/rust-mode-transform.d.ts +20 -0
- package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/strip-content.d.ts +31 -1
- package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
- package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +12 -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-3nfgbsp4.js → index-0ze9p983.js} +897 -114
- package/dist/{index-nsx2n9eg.js → index-4cz5nfxf.js} +3 -3
- package/dist/{index-0c9dmw9m.js → index-8mtsfhyg.js} +33 -3
- package/dist/{index-f1tnb3vm.js → index-8pqfcghm.js} +21 -9
- package/dist/{index-37rt2bka.js → index-cae89eas.js} +26 -7
- package/dist/index-cgyfn1s2.js +53 -0
- package/dist/{index-z8v31d85.js → index-crke4z5j.js} +144 -14
- package/dist/{index-tsgs7td8.js → index-pck9vadr.js} +1 -1
- package/dist/index-q0t189ay.js +741 -0
- package/dist/{index-3tnkymva.js → index-sw8gnq34.js} +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1952 -795
- 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/plugin/rust-tool-backends.d.ts +4 -0
- package/dist/plugin/rust-tool-backends.d.ts.map +1 -1
- package/dist/{recomp-orchestrator-7wbbvf87.js → recomp-orchestrator-c3ff16k7.js} +8 -7
- 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/conflict-fixer.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/jsonc-edit.d.ts +18 -0
- package/dist/shared/jsonc-edit.d.ts.map +1 -0
- package/dist/shared/jsonc-parser.d.ts +11 -1
- package/dist/shared/jsonc-parser.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 +5 -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 +7 -3
- package/dist/shared/prompt-surface.d.ts.map +1 -1
- package/dist/shared/rpc-client.d.ts.map +1 -1
- package/dist/shared/rpc-types.d.ts +14 -0
- package/dist/shared/rpc-types.d.ts.map +1 -1
- package/dist/shared/rpc-utils.d.ts +32 -6
- package/dist/shared/rpc-utils.d.ts.map +1 -1
- package/dist/shared/tui-preferences.d.ts +9 -0
- package/dist/shared/tui-preferences.d.ts.map +1 -1
- package/dist/shared/window-geometry.d.ts +89 -0
- package/dist/shared/window-geometry.d.ts.map +1 -0
- package/dist/{storage-meta-session-kh5egr11.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 +2 -1
- package/src/config/index.test.ts +23 -0
- package/src/config/index.ts +43 -9
- package/src/config/latch-permanence-guard.test.ts +218 -0
- package/src/config/project-security.ts +10 -1
- package/src/config/schema/magic-context.test.ts +2 -1
- package/src/config/schema/magic-context.ts +13 -3
- 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/conflict-fixer.test.ts +174 -0
- package/src/shared/conflict-fixer.ts +66 -70
- package/src/shared/harness-provider-map.test.ts +20 -0
- package/src/shared/harness-provider-map.ts +19 -0
- package/src/shared/jsonc-edit.ts +295 -0
- package/src/shared/jsonc-parser.test.ts +33 -0
- package/src/shared/jsonc-parser.ts +54 -2
- package/src/shared/logger.test.ts +119 -0
- package/src/shared/logger.ts +33 -15
- package/src/shared/models-dev-cache.test.ts +17 -0
- package/src/shared/models-dev-cache.ts +58 -6
- package/src/shared/prompt-surface-runtime.ts +5 -3
- package/src/shared/prompt-surface.test.ts +91 -2
- package/src/shared/prompt-surface.ts +35 -11
- package/src/shared/rpc-client.ts +4 -2
- package/src/shared/rpc-types.ts +14 -0
- package/src/shared/rpc-utils.test.ts +173 -13
- package/src/shared/rpc-utils.ts +202 -18
- package/src/shared/tui-preferences.test.ts +52 -2
- package/src/shared/tui-preferences.ts +42 -17
- package/src/shared/window-geometry.test.ts +309 -0
- package/src/shared/window-geometry.ts +629 -0
- package/src/tui/data/context-db.ts +5 -0
- package/src/tui/index.tsx +22 -0
- package/src/tui-compiled/data/context-db.ts +5 -0
- package/src/tui-compiled/index.tsx +319 -255
- 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 };
|
|
@@ -434,4 +434,178 @@ describe("fixConflicts", () => {
|
|
|
434
434
|
expect(updated.compaction).toEqual({ auto: false, prune: false });
|
|
435
435
|
});
|
|
436
436
|
});
|
|
437
|
+
|
|
438
|
+
describe("JSONC byte preservation", () => {
|
|
439
|
+
it("removes DCP and disables compaction without changing comments or formatting elsewhere", () => {
|
|
440
|
+
const configPath = join(projectDir, "opencode.jsonc");
|
|
441
|
+
const original =
|
|
442
|
+
"// leading file comment\r\n" +
|
|
443
|
+
"{\r\n" +
|
|
444
|
+
'\t"plugin": [\r\n' +
|
|
445
|
+
"\t\t// first plugin comment\r\n" +
|
|
446
|
+
'\t\t"@keep/first",\r\n' +
|
|
447
|
+
"\t\t// removed DCP comment\r\n" +
|
|
448
|
+
'\t\t"@tarquinen/opencode-dcp@latest",\r\n' +
|
|
449
|
+
"\t\t// second plugin comment\r\n" +
|
|
450
|
+
'\t\t"@keep/second",\r\n' +
|
|
451
|
+
"\t], // trailing array comment\r\n" +
|
|
452
|
+
'\t"compaction": {\r\n' +
|
|
453
|
+
"\t\t// nested compaction comment\r\n" +
|
|
454
|
+
'\t\t"auto": true,\r\n' +
|
|
455
|
+
'\t\t"prune": true,\r\n' +
|
|
456
|
+
"\t},\r\n" +
|
|
457
|
+
'\t"theme": "dark",\r\n' +
|
|
458
|
+
"}\r\n";
|
|
459
|
+
const expected =
|
|
460
|
+
"// leading file comment\r\n" +
|
|
461
|
+
"{\r\n" +
|
|
462
|
+
'\t"plugin": [\r\n' +
|
|
463
|
+
"\t\t// first plugin comment\r\n" +
|
|
464
|
+
'\t\t"@keep/first",\r\n' +
|
|
465
|
+
"\t\t// second plugin comment\r\n" +
|
|
466
|
+
'\t\t"@keep/second",\r\n' +
|
|
467
|
+
"\t], // trailing array comment\r\n" +
|
|
468
|
+
'\t"compaction": {\r\n' +
|
|
469
|
+
"\t\t// nested compaction comment\r\n" +
|
|
470
|
+
'\t\t"auto": false,\r\n' +
|
|
471
|
+
'\t\t"prune": false,\r\n' +
|
|
472
|
+
"\t},\r\n" +
|
|
473
|
+
'\t"theme": "dark",\r\n' +
|
|
474
|
+
"}\r\n";
|
|
475
|
+
writeFileSync(configPath, original);
|
|
476
|
+
|
|
477
|
+
const actions = fixConflicts(projectDir, {
|
|
478
|
+
compactionAuto: true,
|
|
479
|
+
compactionPrune: true,
|
|
480
|
+
dcpPlugin: true,
|
|
481
|
+
...noOmoConflicts,
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
expect(actions).toEqual(["Disabled auto-compaction", "Removed opencode-dcp plugin"]);
|
|
485
|
+
expect(readFileSync(configPath, "utf-8")).toBe(expected);
|
|
486
|
+
expect(readFileSync(configPath, "utf-8")).not.toContain("removed DCP comment");
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it("keeps each survivor's own comment when removing a middle plugin entry", () => {
|
|
490
|
+
const configPath = join(projectDir, "opencode.jsonc");
|
|
491
|
+
const original = `{
|
|
492
|
+
"plugin": [
|
|
493
|
+
// first plugin
|
|
494
|
+
"@keep/first",
|
|
495
|
+
// removed plugin
|
|
496
|
+
"@tarquinen/opencode-dcp@latest",
|
|
497
|
+
// second plugin
|
|
498
|
+
"@keep/second",
|
|
499
|
+
],
|
|
500
|
+
"theme": "dark",
|
|
501
|
+
}
|
|
502
|
+
`;
|
|
503
|
+
const expected = `{
|
|
504
|
+
"plugin": [
|
|
505
|
+
// first plugin
|
|
506
|
+
"@keep/first",
|
|
507
|
+
// second plugin
|
|
508
|
+
"@keep/second",
|
|
509
|
+
],
|
|
510
|
+
"theme": "dark",
|
|
511
|
+
}
|
|
512
|
+
`;
|
|
513
|
+
writeFileSync(configPath, original);
|
|
514
|
+
|
|
515
|
+
fixConflicts(projectDir, {
|
|
516
|
+
compactionAuto: false,
|
|
517
|
+
compactionPrune: false,
|
|
518
|
+
dcpPlugin: true,
|
|
519
|
+
...noOmoConflicts,
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
expect(readFileSync(configPath, "utf-8")).toBe(expected);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it("preserves CRLF and the array's trailing comment when DCP is the final entry", () => {
|
|
526
|
+
const configPath = join(projectDir, "opencode.jsonc");
|
|
527
|
+
const original =
|
|
528
|
+
"{\r\n" +
|
|
529
|
+
'\t"plugin": [\r\n' +
|
|
530
|
+
"\t\t// surviving plugin\r\n" +
|
|
531
|
+
'\t\t"@keep/first",\r\n' +
|
|
532
|
+
"\t\t// removed final plugin\r\n" +
|
|
533
|
+
'\t\t"@tarquinen/opencode-dcp@latest",\r\n' +
|
|
534
|
+
"\t], // array trailing comment\r\n" +
|
|
535
|
+
"}\r\n";
|
|
536
|
+
const expected =
|
|
537
|
+
"{\r\n" +
|
|
538
|
+
'\t"plugin": [\r\n' +
|
|
539
|
+
"\t\t// surviving plugin\r\n" +
|
|
540
|
+
'\t\t"@keep/first"\r\n' +
|
|
541
|
+
"\t], // array trailing comment\r\n" +
|
|
542
|
+
"}\r\n";
|
|
543
|
+
writeFileSync(configPath, original);
|
|
544
|
+
|
|
545
|
+
fixConflicts(projectDir, {
|
|
546
|
+
compactionAuto: false,
|
|
547
|
+
compactionPrune: false,
|
|
548
|
+
dcpPlugin: true,
|
|
549
|
+
...noOmoConflicts,
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
expect(readFileSync(configPath, "utf-8")).toBe(expected);
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it("leaves an already conflict-free config byte-identical", () => {
|
|
556
|
+
const configPath = join(projectDir, "opencode.jsonc");
|
|
557
|
+
const original = '// preserve every byte\r\n{\r\n\t"plugin": ["@keep/one",],\r\n}\r\n';
|
|
558
|
+
writeFileSync(configPath, original);
|
|
559
|
+
|
|
560
|
+
const actions = fixConflicts(projectDir, {
|
|
561
|
+
compactionAuto: false,
|
|
562
|
+
compactionPrune: false,
|
|
563
|
+
dcpPlugin: true,
|
|
564
|
+
...noOmoConflicts,
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
expect(actions).toEqual([]);
|
|
568
|
+
expect(readFileSync(configPath, "utf-8")).toBe(original);
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
it("appends OMO hooks without reformatting the existing JSONC", () => {
|
|
572
|
+
const omoDir = join(homeDir, ".omo");
|
|
573
|
+
mkdirSync(omoDir, { recursive: true });
|
|
574
|
+
const configPath = join(omoDir, "omo.jsonc");
|
|
575
|
+
const original = `{
|
|
576
|
+
// preserve this leading comment
|
|
577
|
+
"[opencode]": {
|
|
578
|
+
"disabled_hooks": [
|
|
579
|
+
// custom hook comment
|
|
580
|
+
"custom-hook",
|
|
581
|
+
], // preserve this array comment
|
|
582
|
+
},
|
|
583
|
+
}
|
|
584
|
+
`;
|
|
585
|
+
const expected = `{
|
|
586
|
+
// preserve this leading comment
|
|
587
|
+
"[opencode]": {
|
|
588
|
+
"disabled_hooks": [
|
|
589
|
+
// custom hook comment
|
|
590
|
+
"custom-hook",
|
|
591
|
+
"context-window-monitor",
|
|
592
|
+
], // preserve this array comment
|
|
593
|
+
},
|
|
594
|
+
}
|
|
595
|
+
`;
|
|
596
|
+
writeFileSync(configPath, original);
|
|
597
|
+
|
|
598
|
+
const actions = fixConflicts(projectDir, {
|
|
599
|
+
compactionAuto: false,
|
|
600
|
+
compactionPrune: false,
|
|
601
|
+
dcpPlugin: false,
|
|
602
|
+
omoPreemptiveCompaction: false,
|
|
603
|
+
omoContextWindowMonitor: true,
|
|
604
|
+
omoAnthropicRecovery: false,
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
expect(actions).toEqual(["Disabled conflicting oh-my-opencode hooks"]);
|
|
608
|
+
expect(readFileSync(configPath, "utf-8")).toBe(expected);
|
|
609
|
+
});
|
|
610
|
+
});
|
|
437
611
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { parse
|
|
4
|
+
import { parse } from "comment-json";
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
7
|
type ConflictResult,
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
extractPluginName,
|
|
10
10
|
matchesPackageName,
|
|
11
11
|
} from "./conflict-detector";
|
|
12
|
+
import { appendJsoncArrayValues, removeJsoncArrayEntries, setJsoncValue } from "./jsonc-edit";
|
|
12
13
|
import { getOpenCodeConfigPaths } from "./opencode-config-dir";
|
|
13
14
|
|
|
14
15
|
type JsonObject = Record<string, unknown>;
|
|
@@ -39,21 +40,27 @@ function asStringArray(value: unknown): string[] {
|
|
|
39
40
|
: [];
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
|
|
43
|
+
interface JsonConfigDocument {
|
|
44
|
+
config: JsonObject;
|
|
45
|
+
text: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readConfig(filePath: string): JsonConfigDocument | null {
|
|
43
49
|
if (!existsSync(filePath)) {
|
|
44
50
|
return null;
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
try {
|
|
48
|
-
const
|
|
49
|
-
|
|
54
|
+
const text = readFileSync(filePath, "utf-8");
|
|
55
|
+
const parsed = parse(text);
|
|
56
|
+
return isRecord(parsed) ? { config: parsed, text } : null;
|
|
50
57
|
} catch {
|
|
51
58
|
return null;
|
|
52
59
|
}
|
|
53
60
|
}
|
|
54
61
|
|
|
55
|
-
function writeConfig(filePath: string,
|
|
56
|
-
writeFileSync(filePath,
|
|
62
|
+
function writeConfig(filePath: string, text: string): void {
|
|
63
|
+
writeFileSync(filePath, text);
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
function resolveUserOpenCodeConfigPath(): string {
|
|
@@ -130,26 +137,28 @@ function isUnifiedOmoPath(configPath: string): boolean {
|
|
|
130
137
|
return basename === "omo.jsonc" || basename === "omo.json";
|
|
131
138
|
}
|
|
132
139
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
140
|
+
function disableCompactionFlags(
|
|
141
|
+
text: string,
|
|
142
|
+
config: JsonObject,
|
|
143
|
+
): { text: string; changed: boolean } {
|
|
144
|
+
if (!isRecord(config.compaction)) {
|
|
145
|
+
return {
|
|
146
|
+
text: setJsoncValue(text, ["compaction"], { auto: false, prune: false }),
|
|
147
|
+
changed: true,
|
|
148
|
+
};
|
|
141
149
|
}
|
|
142
|
-
const newBlock: JsonObject = {};
|
|
143
|
-
config["[opencode]"] = newBlock;
|
|
144
|
-
return newBlock;
|
|
145
|
-
}
|
|
146
150
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
151
|
+
let updated = text;
|
|
152
|
+
let changed = false;
|
|
153
|
+
if (config.compaction.auto !== false) {
|
|
154
|
+
updated = setJsoncValue(updated, ["compaction", "auto"], false);
|
|
155
|
+
changed = true;
|
|
156
|
+
}
|
|
157
|
+
if (config.compaction.prune !== false) {
|
|
158
|
+
updated = setJsoncValue(updated, ["compaction", "prune"], false);
|
|
159
|
+
changed = true;
|
|
160
|
+
}
|
|
161
|
+
return { text: updated, changed };
|
|
153
162
|
}
|
|
154
163
|
|
|
155
164
|
/**
|
|
@@ -190,44 +199,37 @@ export function fixConflicts(
|
|
|
190
199
|
|
|
191
200
|
if (repairCompaction || conflicts.dcpPlugin) {
|
|
192
201
|
for (const configPath of collectOpenCodeConfigPaths(directory)) {
|
|
193
|
-
const
|
|
194
|
-
if (!
|
|
202
|
+
const document = readConfig(configPath);
|
|
203
|
+
if (!document) {
|
|
195
204
|
continue;
|
|
196
205
|
}
|
|
197
206
|
|
|
207
|
+
let text = document.text;
|
|
198
208
|
let changed = false;
|
|
199
209
|
|
|
200
210
|
if (repairCompaction) {
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
compaction.auto = false;
|
|
205
|
-
changed = true;
|
|
206
|
-
updatedCompaction = true;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
if (compaction.prune !== false) {
|
|
210
|
-
compaction.prune = false;
|
|
211
|
+
const result = disableCompactionFlags(text, document.config);
|
|
212
|
+
if (result.changed) {
|
|
213
|
+
text = result.text;
|
|
211
214
|
changed = true;
|
|
212
215
|
updatedCompaction = true;
|
|
213
216
|
}
|
|
214
|
-
|
|
215
|
-
config.compaction = compaction;
|
|
216
217
|
}
|
|
217
218
|
|
|
218
219
|
if (conflicts.dcpPlugin) {
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
220
|
+
const result = removeJsoncArrayEntries(text, ["plugin"], (entry) => {
|
|
221
|
+
const name = extractPluginName(entry);
|
|
222
|
+
return name ? matchesPackageName(name, DCP_PACKAGE_NAMES) : false;
|
|
223
|
+
});
|
|
224
|
+
if (result.removed) {
|
|
225
|
+
text = result.text;
|
|
224
226
|
changed = true;
|
|
225
227
|
removedDcpPlugin = true;
|
|
226
228
|
}
|
|
227
229
|
}
|
|
228
230
|
|
|
229
231
|
if (changed) {
|
|
230
|
-
writeConfig(configPath,
|
|
232
|
+
writeConfig(configPath, text);
|
|
231
233
|
}
|
|
232
234
|
}
|
|
233
235
|
}
|
|
@@ -249,36 +251,30 @@ export function fixConflicts(
|
|
|
249
251
|
}
|
|
250
252
|
|
|
251
253
|
for (const configPath of collectOmoConfigPaths(directory)) {
|
|
252
|
-
const
|
|
253
|
-
if (!
|
|
254
|
+
const document = readConfig(configPath);
|
|
255
|
+
if (!document) {
|
|
254
256
|
continue;
|
|
255
257
|
}
|
|
256
258
|
|
|
257
|
-
|
|
258
|
-
const target =
|
|
259
|
-
|
|
259
|
+
const unifiedPath = isUnifiedOmoPath(configPath);
|
|
260
|
+
const target =
|
|
261
|
+
unifiedPath && isRecord(document.config["[opencode]"])
|
|
262
|
+
? document.config["[opencode]"]
|
|
263
|
+
: unifiedPath
|
|
264
|
+
? {}
|
|
265
|
+
: document.config;
|
|
260
266
|
const disabledHooks = new Set(asStringArray(target.disabled_hooks));
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
target.disabled_hooks = [
|
|
273
|
-
...CONFLICTING_OMO_HOOKS.filter((hook) => disabledHooks.has(hook)),
|
|
274
|
-
...[...disabledHooks].filter(
|
|
275
|
-
(hook) =>
|
|
276
|
-
!CONFLICTING_OMO_HOOKS.includes(
|
|
277
|
-
hook as (typeof CONFLICTING_OMO_HOOKS)[number],
|
|
278
|
-
),
|
|
279
|
-
),
|
|
280
|
-
];
|
|
281
|
-
writeConfig(configPath, config);
|
|
267
|
+
const hooksToAdd = CONFLICTING_OMO_HOOKS.filter(
|
|
268
|
+
(hook) => hooksToDisable.has(hook) && !disabledHooks.has(hook),
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
if (hooksToAdd.length > 0) {
|
|
272
|
+
const path = unifiedPath ? ["[opencode]", "disabled_hooks"] : ["disabled_hooks"];
|
|
273
|
+
const text = Array.isArray(target.disabled_hooks)
|
|
274
|
+
? appendJsoncArrayValues(document.text, path, hooksToAdd)
|
|
275
|
+
: setJsoncValue(document.text, path, hooksToAdd);
|
|
276
|
+
writeConfig(configPath, text);
|
|
277
|
+
disabledOmoHooks = true;
|
|
282
278
|
}
|
|
283
279
|
}
|
|
284
280
|
}
|
|
@@ -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);
|