@cortexkit/opencode-magic-context 0.33.1 → 0.34.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/agent-disable.d.ts +22 -0
- package/dist/config/agent-disable.d.ts.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/project-security.d.ts +7 -0
- package/dist/config/project-security.d.ts.map +1 -1
- package/dist/config/schema/magic-context.d.ts +82 -2
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/config/transform-mode.d.ts +2 -0
- package/dist/config/transform-mode.d.ts.map +1 -1
- package/dist/{conflict-warning-hook-n48c783v.js → conflict-warning-hook-fvpfvt0w.js} +1 -1
- package/dist/{derive-budgets-e9j09sjr.js → derive-budgets-9crp0cvd.js} +1 -1
- package/dist/features/builtin-commands/commands.d.ts +1 -1
- package/dist/features/builtin-commands/commands.d.ts.map +1 -1
- package/dist/features/magic-context/compaction-marker.d.ts +57 -0
- package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
- package/dist/features/magic-context/compartment-chunk-embedding.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/map-memories.d.ts.map +1 -1
- 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/verify.d.ts.map +1 -1
- package/dist/features/magic-context/fail-closed-block.d.ts +10 -0
- package/dist/features/magic-context/fail-closed-block.d.ts.map +1 -1
- package/dist/features/magic-context/git-commits/storage-git-commit-embeddings.d.ts.map +1 -1
- package/dist/features/magic-context/git-commits/storage-git-commits.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-identity.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-local.d.ts +7 -1
- package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding.d.ts.map +1 -1
- package/dist/features/magic-context/memory/memory-migration.d.ts.map +1 -1
- package/dist/features/magic-context/memory/project-identity.d.ts +2 -6
- package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
- package/dist/features/magic-context/memory/storage-memory-embeddings.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.map +1 -1
- package/dist/features/magic-context/mural/render-mural.d.ts.map +1 -1
- package/dist/features/magic-context/overflow-detection.d.ts +8 -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/scheduler.d.ts.map +1 -1
- package/dist/features/magic-context/schema-fence-probe.d.ts +28 -0
- package/dist/features/magic-context/schema-fence-probe.d.ts.map +1 -0
- package/dist/features/magic-context/sidekick/agent.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/compiler.d.ts.map +1 -1
- package/dist/features/magic-context/storage-db.d.ts +35 -1
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-persisted.d.ts +38 -4
- package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta.d.ts +1 -1
- package/dist/features/magic-context/storage-meta.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.map +1 -1
- package/dist/hooks/magic-context/boundary-execution.d.ts +1 -1
- package/dist/hooks/magic-context/boundary-execution.d.ts.map +1 -1
- package/dist/hooks/magic-context/cache-busting-signals.d.ts +1 -0
- package/dist/hooks/magic-context/cache-busting-signals.d.ts.map +1 -1
- package/dist/hooks/magic-context/child-session-spawn.d.ts +26 -0
- package/dist/hooks/magic-context/child-session-spawn.d.ts.map +1 -0
- 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/compaction-off-transition.d.ts +96 -0
- package/dist/hooks/magic-context/compaction-off-transition.d.ts.map +1 -0
- package/dist/hooks/magic-context/compartment-runner-historian.d.ts +2 -0
- package/dist/hooks/magic-context/compartment-runner-historian.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-mapping.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-partial-recomp.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-trigger.d.ts +12 -5
- package/dist/hooks/magic-context/compartment-trigger.d.ts.map +1 -1
- package/dist/hooks/magic-context/ctx-reduce-availability.d.ts +47 -1
- package/dist/hooks/magic-context/ctx-reduce-availability.d.ts.map +1 -1
- package/dist/hooks/magic-context/emergency-drop.d.ts +2 -2
- package/dist/hooks/magic-context/event-handler.d.ts +7 -0
- package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-resolvers.d.ts +13 -21
- package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
- package/dist/hooks/magic-context/final-wire-token-estimate.d.ts +2 -0
- package/dist/hooks/magic-context/final-wire-token-estimate.d.ts.map +1 -1
- package/dist/hooks/magic-context/heuristic-cleanup.d.ts +1 -1
- package/dist/hooks/magic-context/hook-handlers.d.ts +1 -0
- package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook.d.ts +11 -1
- package/dist/hooks/magic-context/hook.d.ts.map +1 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts +9 -0
- package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
- package/dist/hooks/magic-context/protected-tail-boundary.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts.map +1 -1
- package/dist/hooks/magic-context/rust-mode-transform.d.ts +3 -0
- package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/send-session-notification.d.ts +20 -1
- package/dist/hooks/magic-context/send-session-notification.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts +3 -0
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +30 -0
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +27 -0
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/{index-ey1bc14z.js → index-b563sga5.js} +7 -264
- package/dist/{index-y1c9tcm3.js → index-cn960ffd.js} +101 -20
- package/dist/{index-xyaekybw.js → index-dtas91s5.js} +6 -4
- package/dist/index-fcyvznch.js +279 -0
- package/dist/{index-63dq2g4d.js → index-ry8rh8cv.js} +138 -24
- package/dist/{index-c2gahrks.js → index-s0mdn690.js} +1599 -892
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1292 -623
- package/dist/plugin/embedding-bootstrap.d.ts.map +1 -1
- package/dist/plugin/hooks/create-session-hooks.d.ts +11 -0
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/messages-transform.d.ts +10 -0
- package/dist/plugin/messages-transform.d.ts.map +1 -1
- package/dist/plugin/rpc-handlers.d.ts +3 -7
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/plugin/tool-registry.d.ts +8 -0
- package/dist/plugin/tool-registry.d.ts.map +1 -1
- package/dist/{recomp-orchestrator-2yb2hfh2.js → recomp-orchestrator-8e82vmvb.js} +6 -4
- package/dist/send-session-notification-6njqewzk.js +20 -0
- package/dist/shared/conflict-detector.d.ts +38 -1
- package/dist/shared/conflict-detector.d.ts.map +1 -1
- package/dist/shared/conflict-fixer.d.ts +18 -1
- package/dist/shared/conflict-fixer.d.ts.map +1 -1
- package/dist/shared/context-limit-provenance.d.ts +3 -0
- package/dist/shared/context-limit-provenance.d.ts.map +1 -0
- package/dist/shared/escalation-bands.d.ts +9 -0
- package/dist/shared/escalation-bands.d.ts.map +1 -0
- package/dist/shared/format-threshold.d.ts +1 -1
- package/dist/shared/models-dev-cache.d.ts +29 -3
- package/dist/shared/models-dev-cache.d.ts.map +1 -1
- package/dist/shared/rpc-server.d.ts.map +1 -1
- package/dist/shared/rpc-types.d.ts +46 -1
- package/dist/shared/rpc-types.d.ts.map +1 -1
- package/dist/shared/storage-permissions.d.ts +5 -0
- package/dist/shared/storage-permissions.d.ts.map +1 -0
- package/dist/{storage-meta-session-td6bwtky.js → storage-meta-session-xt41zbw4.js} +2 -1
- package/dist/tui/compaction-off.d.ts +13 -0
- package/dist/tui/compaction-off.d.ts.map +1 -0
- package/dist/tui/data/context-db.d.ts.map +1 -1
- package/dist/tui-compiled/compaction-off.d.ts +13 -0
- package/dist/tui-compiled/compaction-off.d.ts.map +1 -0
- package/dist/tui-compiled/data/context-db.d.ts.map +1 -1
- package/package.json +7 -2
- package/src/agents/dreamer.ts +68 -0
- package/src/agents/hidden-agent-registrations.ts +331 -0
- package/src/agents/historian.ts +3 -0
- package/src/agents/language-directive.test.ts +138 -0
- package/src/agents/language-directive.ts +121 -0
- package/src/agents/magic-context-prompt.language.test.ts +35 -0
- package/src/agents/magic-context-prompt.test.ts +292 -0
- package/src/agents/magic-context-prompt.ts +182 -0
- package/src/agents/permissions.test.ts +252 -0
- package/src/agents/permissions.ts +186 -0
- package/src/agents/sidekick.ts +1 -0
- package/src/agents/smart-note-compiler.ts +1 -0
- package/src/config/agent-disable.ts +106 -0
- package/src/config/compaction-accessor-guard.test.ts +82 -0
- package/src/config/compaction-backcompat.test.ts +52 -0
- package/src/config/index.test.ts +904 -0
- package/src/config/index.ts +601 -0
- package/src/config/migrate-config-location.test.ts +332 -0
- package/src/config/migrate-config-location.ts +635 -0
- package/src/config/migrate-dreamer-v2.test.ts +287 -0
- package/src/config/migrate-dreamer-v2.ts +336 -0
- package/src/config/migrate-experimental.ts +159 -0
- package/src/config/project-security.test.ts +366 -0
- package/src/config/project-security.ts +588 -0
- package/src/config/prune-config-leaf.test.ts +78 -0
- package/src/config/prune-config-leaf.ts +68 -0
- package/src/config/removed-config-keys.test.ts +116 -0
- package/src/config/schema/agent-overrides.ts +45 -0
- package/src/config/schema/magic-context.test.ts +358 -0
- package/src/config/schema/magic-context.ts +1002 -0
- package/src/config/transform-mode.test.ts +62 -0
- package/src/config/transform-mode.ts +34 -0
- package/src/config/variable.test.ts +323 -0
- package/src/config/variable.ts +192 -0
- package/src/features/magic-context/defaults.ts +1 -0
- package/src/features/magic-context/dreamer/cron.ts +243 -0
- package/src/features/magic-context/dreamer/task-registry.ts +104 -0
- package/src/shared/conflict-detector.test.ts +121 -0
- package/src/shared/conflict-detector.ts +50 -3
- package/src/shared/conflict-fixer.test.ts +309 -0
- package/src/shared/conflict-fixer.ts +82 -5
- package/src/shared/context-limit-provenance.ts +6 -0
- package/src/shared/escalation-bands.test.ts +23 -0
- package/src/shared/escalation-bands.ts +18 -0
- package/src/shared/format-threshold.ts +1 -1
- package/src/shared/models-dev-cache.test.ts +171 -0
- package/src/shared/models-dev-cache.ts +195 -38
- package/src/shared/rpc-server.ts +27 -18
- package/src/shared/rpc-types.ts +46 -1
- package/src/shared/storage-permissions.ts +20 -0
- package/src/tui/compaction-off.test.ts +56 -0
- package/src/tui/compaction-off.ts +29 -0
- package/src/tui/data/context-db.ts +6 -0
- package/src/tui/index.tsx +113 -65
- package/src/tui/slots/sidebar-content.tsx +139 -83
- package/src/tui-compiled/compaction-off.ts +29 -0
- package/src/tui-compiled/data/context-db.ts +6 -0
- package/src/tui-compiled/index.tsx +558 -455
- package/src/tui-compiled/slots/sidebar-content.tsx +237 -161
- package/dist/send-session-notification-tghq9gnh.js +0 -11
- /package/dist/{index-v7fmc1s2.js → index-ygcng9t8.js} +0 -0
|
@@ -5,6 +5,7 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync
|
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { parse as parseJsonc } from "comment-json";
|
|
8
|
+
import { detectConflicts } from "./conflict-detector";
|
|
8
9
|
import { fixConflicts } from "./conflict-fixer";
|
|
9
10
|
|
|
10
11
|
const noOmoConflicts = {
|
|
@@ -17,19 +18,24 @@ describe("fixConflicts", () => {
|
|
|
17
18
|
let root: string;
|
|
18
19
|
let projectDir: string;
|
|
19
20
|
let userConfigDir: string;
|
|
21
|
+
let homeDir: string;
|
|
20
22
|
let originalEnv: Record<string, string | undefined>;
|
|
21
23
|
|
|
22
24
|
beforeEach(() => {
|
|
23
25
|
root = mkdtempSync(join(tmpdir(), "mc-conflict-fixer-"));
|
|
24
26
|
projectDir = join(root, "project");
|
|
25
27
|
userConfigDir = join(root, "user-config", "opencode");
|
|
28
|
+
homeDir = join(root, "home");
|
|
26
29
|
mkdirSync(projectDir, { recursive: true });
|
|
27
30
|
mkdirSync(userConfigDir, { recursive: true });
|
|
31
|
+
mkdirSync(homeDir, { recursive: true });
|
|
28
32
|
originalEnv = {
|
|
29
33
|
OPENCODE_CONFIG_DIR: process.env.OPENCODE_CONFIG_DIR,
|
|
30
34
|
XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME,
|
|
35
|
+
HOME: process.env.HOME,
|
|
31
36
|
};
|
|
32
37
|
process.env.OPENCODE_CONFIG_DIR = userConfigDir;
|
|
38
|
+
process.env.HOME = homeDir;
|
|
33
39
|
delete process.env.XDG_CONFIG_HOME;
|
|
34
40
|
});
|
|
35
41
|
|
|
@@ -125,4 +131,307 @@ describe("fixConflicts", () => {
|
|
|
125
131
|
["@other/opencode-dcp-slim@latest", { enabled: true }],
|
|
126
132
|
]);
|
|
127
133
|
});
|
|
134
|
+
|
|
135
|
+
// --- Unified OMO config (oh-my-openagent >= 4.19.0) ---
|
|
136
|
+
|
|
137
|
+
describe("unified OMO config paths", () => {
|
|
138
|
+
const omoConflicts = {
|
|
139
|
+
compactionAuto: false,
|
|
140
|
+
compactionPrune: false,
|
|
141
|
+
dcpPlugin: false,
|
|
142
|
+
omoPreemptiveCompaction: true,
|
|
143
|
+
omoContextWindowMonitor: true,
|
|
144
|
+
omoAnthropicRecovery: true,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
it("disables hooks inside [opencode] block in ~/.omo/omo.jsonc, comments survive, detector confirms", () => {
|
|
148
|
+
const omoDir = join(homeDir, ".omo");
|
|
149
|
+
mkdirSync(omoDir, { recursive: true });
|
|
150
|
+
const configPath = join(omoDir, "omo.jsonc");
|
|
151
|
+
writeFileSync(
|
|
152
|
+
configPath,
|
|
153
|
+
`{
|
|
154
|
+
// top-level comment
|
|
155
|
+
"some-omo-setting": true,
|
|
156
|
+
"[opencode]": {
|
|
157
|
+
// opencode block comment
|
|
158
|
+
"other_setting": "value"
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
`,
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const actions = fixConflicts(projectDir, omoConflicts);
|
|
165
|
+
|
|
166
|
+
expect(actions).toEqual(["Disabled conflicting oh-my-opencode hooks"]);
|
|
167
|
+
|
|
168
|
+
const updatedText = readFileSync(configPath, "utf-8");
|
|
169
|
+
expect(updatedText).toContain("top-level comment");
|
|
170
|
+
expect(updatedText).toContain("opencode block comment");
|
|
171
|
+
expect(updatedText).toContain("some-omo-setting");
|
|
172
|
+
|
|
173
|
+
const updated = parseJsonc(updatedText) as Record<string, unknown>;
|
|
174
|
+
const opencodeBlock = updated["[opencode]"] as Record<string, unknown>;
|
|
175
|
+
expect(opencodeBlock.disabled_hooks).toEqual([
|
|
176
|
+
"context-window-monitor",
|
|
177
|
+
"preemptive-compaction",
|
|
178
|
+
"anthropic-context-window-limit-recovery",
|
|
179
|
+
]);
|
|
180
|
+
expect(opencodeBlock.other_setting).toBe("value");
|
|
181
|
+
|
|
182
|
+
// Round-trip: detector should now report no OMO conflicts
|
|
183
|
+
// Need a project-level opencode.json with OMO plugin for detection
|
|
184
|
+
writeFileSync(
|
|
185
|
+
join(projectDir, "opencode.json"),
|
|
186
|
+
JSON.stringify({ plugin: ["oh-my-opencode"] }),
|
|
187
|
+
);
|
|
188
|
+
const result = detectConflicts(projectDir);
|
|
189
|
+
expect(result.conflicts.omoPreemptiveCompaction).toBe(false);
|
|
190
|
+
expect(result.conflicts.omoContextWindowMonitor).toBe(false);
|
|
191
|
+
expect(result.conflicts.omoAnthropicRecovery).toBe(false);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("creates [opencode] block when missing in unified omo.jsonc", () => {
|
|
195
|
+
const omoDir = join(homeDir, ".omo");
|
|
196
|
+
mkdirSync(omoDir, { recursive: true });
|
|
197
|
+
const configPath = join(omoDir, "omo.jsonc");
|
|
198
|
+
writeFileSync(
|
|
199
|
+
configPath,
|
|
200
|
+
`{
|
|
201
|
+
// just a top-level setting, no [opencode] block
|
|
202
|
+
"some-omo-setting": true
|
|
203
|
+
}
|
|
204
|
+
`,
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const actions = fixConflicts(projectDir, omoConflicts);
|
|
208
|
+
|
|
209
|
+
expect(actions).toEqual(["Disabled conflicting oh-my-opencode hooks"]);
|
|
210
|
+
|
|
211
|
+
const updatedText = readFileSync(configPath, "utf-8");
|
|
212
|
+
expect(updatedText).toContain("top-level setting");
|
|
213
|
+
|
|
214
|
+
const updated = parseJsonc(updatedText) as Record<string, unknown>;
|
|
215
|
+
const opencodeBlock = updated["[opencode]"] as Record<string, unknown>;
|
|
216
|
+
expect(opencodeBlock.disabled_hooks).toEqual([
|
|
217
|
+
"context-window-monitor",
|
|
218
|
+
"preemptive-compaction",
|
|
219
|
+
"anthropic-context-window-limit-recovery",
|
|
220
|
+
]);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("writes to project-level .omo/omo.jsonc", () => {
|
|
224
|
+
const omoDir = join(projectDir, ".omo");
|
|
225
|
+
mkdirSync(omoDir, { recursive: true });
|
|
226
|
+
const configPath = join(omoDir, "omo.jsonc");
|
|
227
|
+
writeFileSync(
|
|
228
|
+
configPath,
|
|
229
|
+
JSON.stringify({
|
|
230
|
+
"[opencode]": {},
|
|
231
|
+
}),
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
const actions = fixConflicts(projectDir, omoConflicts);
|
|
235
|
+
|
|
236
|
+
expect(actions).toEqual(["Disabled conflicting oh-my-opencode hooks"]);
|
|
237
|
+
|
|
238
|
+
const updated = parseJsonc(readFileSync(configPath, "utf-8")) as Record<
|
|
239
|
+
string,
|
|
240
|
+
unknown
|
|
241
|
+
>;
|
|
242
|
+
const opencodeBlock = updated["[opencode]"] as Record<string, unknown>;
|
|
243
|
+
expect(opencodeBlock.disabled_hooks).toEqual([
|
|
244
|
+
"context-window-monitor",
|
|
245
|
+
"preemptive-compaction",
|
|
246
|
+
"anthropic-context-window-limit-recovery",
|
|
247
|
+
]);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("reads omo.json (fallback) when omo.jsonc does not exist", () => {
|
|
251
|
+
const omoDir = join(homeDir, ".omo");
|
|
252
|
+
mkdirSync(omoDir, { recursive: true });
|
|
253
|
+
const configPath = join(omoDir, "omo.json");
|
|
254
|
+
writeFileSync(
|
|
255
|
+
configPath,
|
|
256
|
+
JSON.stringify({
|
|
257
|
+
"[opencode]": {},
|
|
258
|
+
}),
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
const actions = fixConflicts(projectDir, omoConflicts);
|
|
262
|
+
|
|
263
|
+
expect(actions).toEqual(["Disabled conflicting oh-my-opencode hooks"]);
|
|
264
|
+
|
|
265
|
+
const updated = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
266
|
+
expect(updated["[opencode]"].disabled_hooks).toEqual([
|
|
267
|
+
"context-window-monitor",
|
|
268
|
+
"preemptive-compaction",
|
|
269
|
+
"anthropic-context-window-limit-recovery",
|
|
270
|
+
]);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("updates both legacy and unified config when both exist", () => {
|
|
274
|
+
// Legacy: project-level oh-my-opencode.json
|
|
275
|
+
const legacyPath = join(projectDir, "oh-my-opencode.json");
|
|
276
|
+
writeFileSync(legacyPath, JSON.stringify({ disabled_hooks: [] }));
|
|
277
|
+
|
|
278
|
+
// Unified: ~/.omo/omo.jsonc
|
|
279
|
+
const omoDir = join(homeDir, ".omo");
|
|
280
|
+
mkdirSync(omoDir, { recursive: true });
|
|
281
|
+
const unifiedPath = join(omoDir, "omo.jsonc");
|
|
282
|
+
writeFileSync(
|
|
283
|
+
unifiedPath,
|
|
284
|
+
JSON.stringify({
|
|
285
|
+
"[opencode]": {},
|
|
286
|
+
}),
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
const actions = fixConflicts(projectDir, omoConflicts);
|
|
290
|
+
|
|
291
|
+
expect(actions).toEqual(["Disabled conflicting oh-my-opencode hooks"]);
|
|
292
|
+
|
|
293
|
+
// Legacy: top-level disabled_hooks
|
|
294
|
+
const legacy = JSON.parse(readFileSync(legacyPath, "utf-8"));
|
|
295
|
+
expect(legacy.disabled_hooks).toEqual([
|
|
296
|
+
"context-window-monitor",
|
|
297
|
+
"preemptive-compaction",
|
|
298
|
+
"anthropic-context-window-limit-recovery",
|
|
299
|
+
]);
|
|
300
|
+
|
|
301
|
+
// Unified: inside [opencode] block
|
|
302
|
+
const unified = JSON.parse(readFileSync(unifiedPath, "utf-8"));
|
|
303
|
+
expect(unified["[opencode]"].disabled_hooks).toEqual([
|
|
304
|
+
"context-window-monitor",
|
|
305
|
+
"preemptive-compaction",
|
|
306
|
+
"anthropic-context-window-limit-recovery",
|
|
307
|
+
]);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("skips non-existent unified paths (no create)", () => {
|
|
311
|
+
// No .omo directory at all — fixer should find no targets
|
|
312
|
+
const actions = fixConflicts(projectDir, omoConflicts);
|
|
313
|
+
expect(actions).toEqual([]);
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// --- Compaction-off mode parity (issue #266 S2) ---
|
|
318
|
+
// In compaction-off mode the fixer MUST NOT flip compaction.auto/prune to
|
|
319
|
+
// false — native compaction fields are left byte-for-byte as found.
|
|
320
|
+
// DCP and OMO hook fixes keep their existing policy in BOTH modes.
|
|
321
|
+
describe("compaction-off mode parity (issue #266)", () => {
|
|
322
|
+
it("does NOT flip compaction.auto to false when compaction-off", () => {
|
|
323
|
+
const configPath = join(projectDir, "opencode.jsonc");
|
|
324
|
+
const original = `{
|
|
325
|
+
// keep comment
|
|
326
|
+
"compaction": {
|
|
327
|
+
"auto": true,
|
|
328
|
+
"prune": true
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
`;
|
|
332
|
+
writeFileSync(configPath, original);
|
|
333
|
+
|
|
334
|
+
const actions = fixConflicts(
|
|
335
|
+
projectDir,
|
|
336
|
+
{
|
|
337
|
+
compactionAuto: true,
|
|
338
|
+
compactionPrune: true,
|
|
339
|
+
dcpPlugin: false,
|
|
340
|
+
...noOmoConflicts,
|
|
341
|
+
},
|
|
342
|
+
{ compactionEnabled: false },
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
// No compaction repair action in compaction-off mode.
|
|
346
|
+
expect(actions).toEqual([]);
|
|
347
|
+
// File is byte-for-byte unchanged (native fields preserved).
|
|
348
|
+
expect(readFileSync(configPath, "utf-8")).toBe(original);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it("still removes DCP plugin in compaction-off mode (DCP policy is mode-independent)", () => {
|
|
352
|
+
const configPath = join(projectDir, "opencode.jsonc");
|
|
353
|
+
writeFileSync(
|
|
354
|
+
configPath,
|
|
355
|
+
JSON.stringify({
|
|
356
|
+
plugin: ["@tarquinen/opencode-dcp@latest"],
|
|
357
|
+
compaction: { auto: true, prune: true },
|
|
358
|
+
}),
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
const actions = fixConflicts(
|
|
362
|
+
projectDir,
|
|
363
|
+
{ compactionAuto: true, compactionPrune: true, dcpPlugin: true, ...noOmoConflicts },
|
|
364
|
+
{ compactionEnabled: false },
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
// DCP removed, compaction NOT flipped.
|
|
368
|
+
expect(actions).toEqual(["Removed opencode-dcp plugin"]);
|
|
369
|
+
const updated = parseJsonc(readFileSync(configPath, "utf-8")) as Record<
|
|
370
|
+
string,
|
|
371
|
+
unknown
|
|
372
|
+
>;
|
|
373
|
+
expect(updated.compaction).toEqual({ auto: true, prune: true });
|
|
374
|
+
expect(updated.plugin).toEqual([]);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("still disables OMO hooks in compaction-off mode (OMO policy is mode-independent)", () => {
|
|
378
|
+
const configPath = join(projectDir, "opencode.json");
|
|
379
|
+
writeFileSync(configPath, JSON.stringify({ plugin: ["oh-my-opencode"] }));
|
|
380
|
+
const omoDir = join(homeDir, ".omo");
|
|
381
|
+
mkdirSync(omoDir, { recursive: true });
|
|
382
|
+
const omoPath = join(omoDir, "omo.jsonc");
|
|
383
|
+
writeFileSync(omoPath, JSON.stringify({ "[opencode]": {} }));
|
|
384
|
+
|
|
385
|
+
const actions = fixConflicts(
|
|
386
|
+
projectDir,
|
|
387
|
+
{
|
|
388
|
+
compactionAuto: false,
|
|
389
|
+
compactionPrune: false,
|
|
390
|
+
dcpPlugin: false,
|
|
391
|
+
omoPreemptiveCompaction: true,
|
|
392
|
+
omoContextWindowMonitor: true,
|
|
393
|
+
omoAnthropicRecovery: true,
|
|
394
|
+
},
|
|
395
|
+
{ compactionEnabled: false },
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
expect(actions).toEqual(["Disabled conflicting oh-my-opencode hooks"]);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// Mutation direction: with mode ON, the same compaction conflict IS
|
|
402
|
+
// repaired. Proves the off-gate isn't just always-skip.
|
|
403
|
+
it("mutation direction: same conflict IS repaired when mode forced on", () => {
|
|
404
|
+
const configPath = join(projectDir, "opencode.jsonc");
|
|
405
|
+
writeFileSync(configPath, JSON.stringify({ compaction: { auto: true, prune: true } }));
|
|
406
|
+
|
|
407
|
+
const offActions = fixConflicts(
|
|
408
|
+
projectDir,
|
|
409
|
+
{
|
|
410
|
+
compactionAuto: true,
|
|
411
|
+
compactionPrune: true,
|
|
412
|
+
dcpPlugin: false,
|
|
413
|
+
...noOmoConflicts,
|
|
414
|
+
},
|
|
415
|
+
{ compactionEnabled: false },
|
|
416
|
+
);
|
|
417
|
+
const onActions = fixConflicts(
|
|
418
|
+
projectDir,
|
|
419
|
+
{
|
|
420
|
+
compactionAuto: true,
|
|
421
|
+
compactionPrune: true,
|
|
422
|
+
dcpPlugin: false,
|
|
423
|
+
...noOmoConflicts,
|
|
424
|
+
},
|
|
425
|
+
{ compactionEnabled: true },
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
expect(offActions).toEqual([]);
|
|
429
|
+
expect(onActions).toEqual(["Disabled auto-compaction"]);
|
|
430
|
+
const updated = parseJsonc(readFileSync(configPath, "utf-8")) as Record<
|
|
431
|
+
string,
|
|
432
|
+
unknown
|
|
433
|
+
>;
|
|
434
|
+
expect(updated.compaction).toEqual({ auto: false, prune: false });
|
|
435
|
+
});
|
|
436
|
+
});
|
|
128
437
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { parse, stringify } from "comment-json";
|
|
4
5
|
|
|
@@ -25,6 +26,9 @@ const OMO_CONFIG_NAMES = [
|
|
|
25
26
|
"oh-my-opencode.json",
|
|
26
27
|
] as const;
|
|
27
28
|
|
|
29
|
+
/** Unified OMO config names (oh-my-openagent >= 4.19.0) */
|
|
30
|
+
const OMO_UNIFIED_NAMES = ["omo.jsonc", "omo.json"] as const;
|
|
31
|
+
|
|
28
32
|
function isRecord(value: unknown): value is JsonObject {
|
|
29
33
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
30
34
|
}
|
|
@@ -84,6 +88,7 @@ function collectOmoConfigPaths(directory: string): string[] {
|
|
|
84
88
|
const paths = new Set<string>();
|
|
85
89
|
const configDir = getOpenCodeConfigPaths({ binary: "opencode" }).configDir;
|
|
86
90
|
|
|
91
|
+
// Legacy OMO config names in the OpenCode config dir and project dir
|
|
87
92
|
for (const fileName of OMO_CONFIG_NAMES) {
|
|
88
93
|
const userPath = join(configDir, fileName);
|
|
89
94
|
const projectPath = join(directory, fileName);
|
|
@@ -97,9 +102,48 @@ function collectOmoConfigPaths(directory: string): string[] {
|
|
|
97
102
|
}
|
|
98
103
|
}
|
|
99
104
|
|
|
105
|
+
// New unified omo.jsonc (oh-my-openagent >= 4.19.0)
|
|
106
|
+
// User-level: ~/.omo/omo.jsonc (fallback ~/.omo/omo.json)
|
|
107
|
+
const homeDir = process.env.HOME || homedir();
|
|
108
|
+
const omoHomeDir = join(homeDir, ".omo");
|
|
109
|
+
for (const name of OMO_UNIFIED_NAMES) {
|
|
110
|
+
const userPath = join(omoHomeDir, name);
|
|
111
|
+
if (existsSync(userPath)) {
|
|
112
|
+
paths.add(userPath);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Project-level: .omo/omo.jsonc (fallback .omo/omo.json)
|
|
117
|
+
for (const name of OMO_UNIFIED_NAMES) {
|
|
118
|
+
const projectPath = join(directory, ".omo", name);
|
|
119
|
+
if (existsSync(projectPath)) {
|
|
120
|
+
paths.add(projectPath);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
100
124
|
return [...paths];
|
|
101
125
|
}
|
|
102
126
|
|
|
127
|
+
/** Check if a config path is a unified omo.json(c) path (not legacy). */
|
|
128
|
+
function isUnifiedOmoPath(configPath: string): boolean {
|
|
129
|
+
const basename = configPath.split("/").pop() ?? "";
|
|
130
|
+
return basename === "omo.jsonc" || basename === "omo.json";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get or create the `[opencode]` block inside a unified omo.json(c) config.
|
|
135
|
+
* Returns the block itself so callers can read/write `disabled_hooks` on it.
|
|
136
|
+
*/
|
|
137
|
+
function getOrCreateOmoV2Block(config: JsonObject): JsonObject {
|
|
138
|
+
const block = config["[opencode]"];
|
|
139
|
+
if (isRecord(block)) {
|
|
140
|
+
return block;
|
|
141
|
+
}
|
|
142
|
+
const newBlock: JsonObject = {};
|
|
143
|
+
config["[opencode]"] = newBlock;
|
|
144
|
+
return newBlock;
|
|
145
|
+
}
|
|
146
|
+
|
|
103
147
|
function filterDcpPluginEntries(entries: unknown[]): { plugins: unknown[]; removed: boolean } {
|
|
104
148
|
const plugins = entries.filter((entry) => {
|
|
105
149
|
const name = extractPluginName(entry);
|
|
@@ -108,13 +152,43 @@ function filterDcpPluginEntries(entries: unknown[]): { plugins: unknown[]; remov
|
|
|
108
152
|
return { plugins, removed: plugins.length !== entries.length };
|
|
109
153
|
}
|
|
110
154
|
|
|
111
|
-
|
|
155
|
+
/**
|
|
156
|
+
* Options for {@link fixConflicts}.
|
|
157
|
+
*
|
|
158
|
+
* `compactionEnabled` is the boot-resolved MC compaction mode (the result of
|
|
159
|
+
* {@link isCompactionEnabled} on the resolved user-tier config). When `false`
|
|
160
|
+
* (compaction-off mode), the fixer MUST NOT flip `compaction.auto`/`prune` to
|
|
161
|
+
* `false` — native compaction fields are left byte-for-byte as found, because
|
|
162
|
+
* native compaction (or nothing) is the user's chosen window manager. DCP and
|
|
163
|
+
* OMO hook fixes keep their existing policy in BOTH modes.
|
|
164
|
+
*
|
|
165
|
+
* Default `true` (mode-on) preserves today's fix behavior for call sites that
|
|
166
|
+
* cannot supply the resolved mode; they fail toward mode-on, never silently
|
|
167
|
+
* skipping the fix.
|
|
168
|
+
*/
|
|
169
|
+
export interface FixConflictsOptions {
|
|
170
|
+
compactionEnabled?: boolean;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function fixConflicts(
|
|
174
|
+
directory: string,
|
|
175
|
+
conflicts: ConflictResult["conflicts"],
|
|
176
|
+
options?: FixConflictsOptions,
|
|
177
|
+
): string[] {
|
|
178
|
+
const compactionEnabled = options?.compactionEnabled ?? true;
|
|
112
179
|
const actions: string[] = [];
|
|
113
180
|
let updatedCompaction = false;
|
|
114
181
|
let removedDcpPlugin = false;
|
|
115
182
|
let disabledOmoHooks = false;
|
|
116
183
|
|
|
117
|
-
|
|
184
|
+
// Native compaction fields are repaired ONLY when MC compaction is ON. In
|
|
185
|
+
// compaction-off mode the fixer must not rewrite compaction.auto/prune —
|
|
186
|
+
// native compaction is the intended manager and pre-existing values are
|
|
187
|
+
// left byte-for-byte as found.
|
|
188
|
+
const repairCompaction =
|
|
189
|
+
compactionEnabled && (conflicts.compactionAuto || conflicts.compactionPrune);
|
|
190
|
+
|
|
191
|
+
if (repairCompaction || conflicts.dcpPlugin) {
|
|
118
192
|
for (const configPath of collectOpenCodeConfigPaths(directory)) {
|
|
119
193
|
const config = readConfig(configPath);
|
|
120
194
|
if (!config) {
|
|
@@ -123,7 +197,7 @@ export function fixConflicts(directory: string, conflicts: ConflictResult["confl
|
|
|
123
197
|
|
|
124
198
|
let changed = false;
|
|
125
199
|
|
|
126
|
-
if (
|
|
200
|
+
if (repairCompaction) {
|
|
127
201
|
const compaction = isRecord(config.compaction) ? config.compaction : {};
|
|
128
202
|
|
|
129
203
|
if (compaction.auto !== false) {
|
|
@@ -180,7 +254,10 @@ export function fixConflicts(directory: string, conflicts: ConflictResult["confl
|
|
|
180
254
|
continue;
|
|
181
255
|
}
|
|
182
256
|
|
|
183
|
-
|
|
257
|
+
// Unified paths nest hooks under the [opencode] block; legacy paths use top-level
|
|
258
|
+
const target = isUnifiedOmoPath(configPath) ? getOrCreateOmoV2Block(config) : config;
|
|
259
|
+
|
|
260
|
+
const disabledHooks = new Set(asStringArray(target.disabled_hooks));
|
|
184
261
|
let changed = false;
|
|
185
262
|
|
|
186
263
|
for (const hook of hooksToDisable) {
|
|
@@ -192,7 +269,7 @@ export function fixConflicts(directory: string, conflicts: ConflictResult["confl
|
|
|
192
269
|
}
|
|
193
270
|
|
|
194
271
|
if (changed) {
|
|
195
|
-
|
|
272
|
+
target.disabled_hooks = [
|
|
196
273
|
...CONFLICTING_OMO_HOOKS.filter((hook) => disabledHooks.has(hook)),
|
|
197
274
|
...[...disabledHooks].filter(
|
|
198
275
|
(hook) =>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { escalationBands } from "./escalation-bands";
|
|
3
|
+
|
|
4
|
+
describe("escalationBands", () => {
|
|
5
|
+
test.each([
|
|
6
|
+
[65, 85],
|
|
7
|
+
[80, 85],
|
|
8
|
+
[88, 90],
|
|
9
|
+
[90, 92],
|
|
10
|
+
])("keeps the force band ordered for threshold %i", (threshold, expectedForce) => {
|
|
11
|
+
const bands = escalationBands(threshold);
|
|
12
|
+
expect(bands.forceMaterializationPercentage).toBe(expectedForce);
|
|
13
|
+
expect(threshold).toBeLessThan(bands.forceMaterializationPercentage);
|
|
14
|
+
expect(bands.forceMaterializationPercentage).toBeGreaterThanOrEqual(85);
|
|
15
|
+
expect(bands.forceMaterializationPercentage).toBeLessThan(95);
|
|
16
|
+
expect(bands.emergencyPercentage).toBe(95);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("preserves the exact pre-raise behavior through threshold 80", () => {
|
|
20
|
+
expect(escalationBands(65).forceMaterializationPercentage).toBe(85);
|
|
21
|
+
expect(escalationBands(80).forceMaterializationPercentage).toBe(85);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const MAX_EXECUTE_THRESHOLD = 90;
|
|
2
|
+
export const ABSOLUTE_EMERGENCY_PERCENTAGE = 95;
|
|
3
|
+
|
|
4
|
+
export interface EscalationBands {
|
|
5
|
+
forceMaterializationPercentage: number;
|
|
6
|
+
emergencyPercentage: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Keep force cleanup above normal execution while preserving the absolute 95% wall. */
|
|
10
|
+
export function escalationBands(effectiveThresholdPercentage: number): EscalationBands {
|
|
11
|
+
const threshold = Number.isFinite(effectiveThresholdPercentage)
|
|
12
|
+
? Math.min(effectiveThresholdPercentage, MAX_EXECUTE_THRESHOLD)
|
|
13
|
+
: 65;
|
|
14
|
+
return {
|
|
15
|
+
forceMaterializationPercentage: Math.max(85, threshold + 2),
|
|
16
|
+
emergencyPercentage: ABSOLUTE_EMERGENCY_PERCENTAGE,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -31,7 +31,7 @@ export function formatThresholdPercent(value: number | undefined | null): string
|
|
|
31
31
|
* Build a terse "clamped" annotation for execute-threshold display, or "" when the
|
|
32
32
|
* configured value was not clamped. Status surfaces (/ctx-status text, pi status
|
|
33
33
|
* dialog) append this so a user who configured e.g. 190000 tokens on a 128K model
|
|
34
|
-
* sees that it was reduced to the
|
|
34
|
+
* sees that it was reduced to the 90% safety cap, instead of the value being
|
|
35
35
|
* silently ignored (issue #241). The note includes the configured value and the cap
|
|
36
36
|
* so the user can see the math.
|
|
37
37
|
*
|