@cortexkit/opencode-magic-context 0.41.2 → 0.41.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/README.md +9 -3
  2. package/dist/announcement-3c1vertn.js +14 -0
  3. package/dist/config/index.d.ts +9 -0
  4. package/dist/config/index.d.ts.map +1 -1
  5. package/dist/config/project-security.d.ts.map +1 -1
  6. package/dist/config/schema/magic-context.d.ts +756 -4
  7. package/dist/config/schema/magic-context.d.ts.map +1 -1
  8. package/dist/conflict-warning-hook-ejkc9sjn.js +16 -0
  9. package/dist/derive-budgets-bc366cz8.js +13 -0
  10. package/dist/features/magic-context/compartment-lease.d.ts +6 -0
  11. package/dist/features/magic-context/compartment-lease.d.ts.map +1 -1
  12. package/dist/features/magic-context/dreamer/classify.d.ts.map +1 -1
  13. package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts +13 -0
  14. package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts.map +1 -1
  15. package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
  16. package/dist/features/magic-context/dreamer/task-scheduler.d.ts +3 -0
  17. package/dist/features/magic-context/dreamer/task-scheduler.d.ts.map +1 -1
  18. package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
  19. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  20. package/dist/features/magic-context/storage-db.d.ts +18 -1
  21. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  22. package/dist/features/magic-context/storage-meta-persisted.d.ts +13 -14
  23. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  24. package/dist/features/magic-context/storage-meta.d.ts +1 -1
  25. package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
  26. package/dist/features/magic-context/storage-subagent-invocations.d.ts +3 -2
  27. package/dist/features/magic-context/storage-subagent-invocations.d.ts.map +1 -1
  28. package/dist/features/magic-context/storage-tags.d.ts +6 -0
  29. package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
  30. package/dist/features/magic-context/storage.d.ts +2 -2
  31. package/dist/features/magic-context/storage.d.ts.map +1 -1
  32. package/dist/features/magic-context/subagent-token-capture.d.ts +2 -1
  33. package/dist/features/magic-context/subagent-token-capture.d.ts.map +1 -1
  34. package/dist/hooks/magic-context/command-handler.d.ts +5 -1
  35. package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
  36. package/dist/hooks/magic-context/compaction-marker-manager.d.ts +6 -1
  37. package/dist/hooks/magic-context/compaction-marker-manager.d.ts.map +1 -1
  38. package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
  39. package/dist/hooks/magic-context/compartment-runner-types.d.ts +4 -0
  40. package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
  41. package/dist/hooks/magic-context/compartment-runner.d.ts +2 -1
  42. package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
  43. package/dist/hooks/magic-context/derive-budgets.d.ts +1 -0
  44. package/dist/hooks/magic-context/derive-budgets.d.ts.map +1 -1
  45. package/dist/hooks/magic-context/emergency-drop.d.ts +3 -1
  46. package/dist/hooks/magic-context/emergency-drop.d.ts.map +1 -1
  47. package/dist/hooks/magic-context/execute-status.d.ts +7 -1
  48. package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
  49. package/dist/hooks/magic-context/heuristic-cleanup.d.ts +4 -3
  50. package/dist/hooks/magic-context/heuristic-cleanup.d.ts.map +1 -1
  51. package/dist/hooks/magic-context/hook-handlers.d.ts +2 -0
  52. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  53. package/dist/hooks/magic-context/hook.d.ts +9 -5
  54. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  55. package/dist/hooks/magic-context/inject-compartments.d.ts +6 -0
  56. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  57. package/dist/hooks/magic-context/issue-423-fixture.d.ts +37 -0
  58. package/dist/hooks/magic-context/issue-423-fixture.d.ts.map +1 -0
  59. package/dist/hooks/magic-context/issue-424-fixture.d.ts +11 -0
  60. package/dist/hooks/magic-context/issue-424-fixture.d.ts.map +1 -0
  61. package/dist/hooks/magic-context/module-state-sync.d.ts +2 -1
  62. package/dist/hooks/magic-context/module-state-sync.d.ts.map +1 -1
  63. package/dist/hooks/magic-context/module-transport.d.ts +11 -4
  64. package/dist/hooks/magic-context/module-transport.d.ts.map +1 -1
  65. package/dist/hooks/magic-context/module-wire.d.ts +6 -1
  66. package/dist/hooks/magic-context/module-wire.d.ts.map +1 -1
  67. package/dist/hooks/magic-context/producer-window-guard.d.ts +8 -0
  68. package/dist/hooks/magic-context/producer-window-guard.d.ts.map +1 -0
  69. package/dist/hooks/magic-context/protected-tail-boundary.d.ts +59 -0
  70. package/dist/hooks/magic-context/protected-tail-boundary.d.ts.map +1 -1
  71. package/dist/hooks/magic-context/read-session-chunk.d.ts +2 -0
  72. package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
  73. package/dist/hooks/magic-context/read-session-db.d.ts +15 -0
  74. package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
  75. package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts +4 -1
  76. package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts.map +1 -1
  77. package/dist/hooks/magic-context/recomp-orchestrator.d.ts +2 -0
  78. package/dist/hooks/magic-context/recomp-orchestrator.d.ts.map +1 -1
  79. package/dist/hooks/magic-context/rust-mode-transform.d.ts +15 -2
  80. package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
  81. package/dist/hooks/magic-context/send-session-notification.d.ts +6 -0
  82. package/dist/hooks/magic-context/send-session-notification.d.ts.map +1 -1
  83. package/dist/hooks/magic-context/sentinel.d.ts +8 -19
  84. package/dist/hooks/magic-context/sentinel.d.ts.map +1 -1
  85. package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
  86. package/dist/hooks/magic-context/tag-messages.d.ts +2 -0
  87. package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
  88. package/dist/hooks/magic-context/tail-hygiene-walk.d.ts +8 -3
  89. package/dist/hooks/magic-context/tail-hygiene-walk.d.ts.map +1 -1
  90. package/dist/hooks/magic-context/tool-drop-target.d.ts +1 -0
  91. package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
  92. package/dist/hooks/magic-context/transform-compartment-phase.d.ts +2 -0
  93. package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
  94. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +32 -0
  95. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  96. package/dist/hooks/magic-context/transform.d.ts +4 -0
  97. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  98. package/dist/hooks/magic-context/wrapup-orchestrator.d.ts.map +1 -1
  99. package/dist/{index-j8r0ejb3.js → index-0y2mcg5y.js} +1971 -11203
  100. package/dist/{index-1yh8g550.js → index-22jkk6wg.js} +2 -15
  101. package/dist/{index-dmvy29z0.js → index-3wzdwt6p.js} +11 -15
  102. package/dist/{index-eq9wr75c.js → index-4avmwfp5.js} +57 -108
  103. package/dist/{index-d1mxrd3p.js → index-7k55qm8a.js} +17 -12
  104. package/dist/{index-2wchnyk6.js → index-avwt563y.js} +8 -1
  105. package/dist/{index-4x691bz5.js → index-dkv2dn46.js} +5 -2
  106. package/dist/{index-rgdqj8nj.js → index-e7vx3byk.js} +1 -1
  107. package/dist/{index-szf8w1p5.js → index-fmsr7v69.js} +2 -2
  108. package/dist/{index-y1yr57fw.js → index-fz104x1j.js} +18 -18
  109. package/dist/{index-d62ypazr.js → index-jdxxq8cz.js} +6 -11
  110. package/dist/{index-9j3tx8hy.js → index-may8skc8.js} +6 -25
  111. package/dist/{index-cvda5jb3.js → index-mjv9je4e.js} +23 -19
  112. package/dist/{index-zwjhba2r.js → index-q264erdp.js} +122 -41
  113. package/dist/{index-b9j85d29.js → index-raatkvw5.js} +49 -11
  114. package/dist/{index-9xexf8s7.js → index-rn7gafjr.js} +1 -9
  115. package/dist/index-sd3egckz.js +1583 -0
  116. package/dist/index-v6t0tg85.js +13 -0
  117. package/dist/index.d.ts.map +1 -1
  118. package/dist/index.js +2660 -3255
  119. package/dist/model-resolution-dxatzjde.js +12 -0
  120. package/dist/{index-dynqfgx1.js → module-asyncify-2EFITU5U-rkqjhjxd.js} +69 -8
  121. package/dist/plugin/boot-deadline.d.ts +47 -0
  122. package/dist/plugin/boot-deadline.d.ts.map +1 -0
  123. package/dist/plugin/conflict-warning-hook.d.ts +10 -0
  124. package/dist/plugin/conflict-warning-hook.d.ts.map +1 -1
  125. package/dist/plugin/hooks/create-session-hooks.d.ts +5 -0
  126. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  127. package/dist/plugin/messages-transform.d.ts.map +1 -1
  128. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  129. package/dist/prompt-context-5y5ms8f9.js +6 -0
  130. package/dist/recomp-orchestrator-q98r6rga.js +19 -0
  131. package/dist/rpc-notifications-4amf6mf4.js +14 -0
  132. package/dist/{index-vnz6jvdc.js → safe-notification-target-q9z81zt3.js} +4 -3
  133. package/dist/send-session-notification-hdh02wy9.js +16 -0
  134. package/dist/shared/cache-ttl-display.d.ts +23 -0
  135. package/dist/shared/cache-ttl-display.d.ts.map +1 -0
  136. package/dist/shared/cache-ttl-seed.d.ts +10 -0
  137. package/dist/shared/cache-ttl-seed.d.ts.map +1 -0
  138. package/dist/shared/config-diagnostics.d.ts +30 -0
  139. package/dist/shared/config-diagnostics.d.ts.map +1 -0
  140. package/dist/shared/config-warning-surface.d.ts +3 -0
  141. package/dist/shared/config-warning-surface.d.ts.map +1 -0
  142. package/dist/shared/conflict-detector.d.ts +3 -1
  143. package/dist/shared/conflict-detector.d.ts.map +1 -1
  144. package/dist/shared/data-path.d.ts +10 -9
  145. package/dist/shared/data-path.d.ts.map +1 -1
  146. package/dist/shared/harness-provider-map.d.ts +7 -0
  147. package/dist/shared/harness-provider-map.d.ts.map +1 -1
  148. package/dist/shared/harness.d.ts +3 -3
  149. package/dist/shared/harness.d.ts.map +1 -1
  150. package/dist/shared/jsonc-parser.d.ts +12 -0
  151. package/dist/shared/jsonc-parser.d.ts.map +1 -1
  152. package/dist/shared/model-resolution.d.ts +3 -3
  153. package/dist/shared/model-resolution.d.ts.map +1 -1
  154. package/dist/shared/model-suggestion-retry.d.ts +14 -10
  155. package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
  156. package/dist/shared/pi-executable.d.ts +6 -0
  157. package/dist/shared/pi-executable.d.ts.map +1 -0
  158. package/dist/shared/prompt-surface-runtime.d.ts +2 -2
  159. package/dist/shared/prompt-surface-runtime.d.ts.map +1 -1
  160. package/dist/shared/rpc-types.d.ts +6 -0
  161. package/dist/shared/rpc-types.d.ts.map +1 -1
  162. package/dist/shared/rpc-utils.d.ts.map +1 -1
  163. package/dist/shared/status-detail-text.d.ts.map +1 -1
  164. package/dist/shared/tag-transcript.d.ts.map +1 -1
  165. package/dist/shared/transcript.d.ts +2 -1
  166. package/dist/shared/transcript.d.ts.map +1 -1
  167. package/dist/storage-meta-session-dvwfw778.js +20 -0
  168. package/dist/task-config-hnv93r7p.js +9 -0
  169. package/dist/transformers-node-wasm.js +328 -328
  170. package/dist/transformers-web.js +72 -72
  171. package/dist/{transformers.node-b1wa0hgx.js → transformers.node-dpmf3wpx.js} +74 -74
  172. package/package.json +1 -1
  173. package/src/config/index.test.ts +72 -0
  174. package/src/config/index.ts +96 -15
  175. package/src/config/project-security.hostile-matrix.test.ts +78 -9
  176. package/src/config/project-security.ts +4 -3
  177. package/src/config/schema/magic-context.test.ts +64 -0
  178. package/src/config/schema/magic-context.ts +100 -6
  179. package/src/shared/cache-ttl-display.test.ts +84 -0
  180. package/src/shared/cache-ttl-display.ts +64 -0
  181. package/src/shared/cache-ttl-seed.ts +23 -0
  182. package/src/shared/config-diagnostics.test.ts +29 -0
  183. package/src/shared/config-diagnostics.ts +64 -0
  184. package/src/shared/config-warning-surface.ts +28 -0
  185. package/src/shared/conflict-detector.test.ts +24 -0
  186. package/src/shared/conflict-detector.ts +25 -7
  187. package/src/shared/data-path.ts +10 -9
  188. package/src/shared/harness-provider-map.test.ts +16 -0
  189. package/src/shared/harness-provider-map.ts +15 -0
  190. package/src/shared/harness.ts +3 -3
  191. package/src/shared/jsonc-parser.test.ts +14 -1
  192. package/src/shared/jsonc-parser.ts +93 -0
  193. package/src/shared/model-resolution.test.ts +61 -0
  194. package/src/shared/model-resolution.ts +14 -5
  195. package/src/shared/model-suggestion-retry.ts +143 -28
  196. package/src/shared/pi-executable.ts +20 -0
  197. package/src/shared/prompt-surface-runtime.ts +3 -3
  198. package/src/shared/rpc-types.ts +6 -0
  199. package/src/shared/rpc-utils.ts +2 -3
  200. package/src/shared/status-detail-text.test.ts +2 -1
  201. package/src/shared/status-detail-text.ts +5 -1
  202. package/src/shared/tag-transcript.ts +32 -2
  203. package/src/shared/transcript.ts +3 -1
  204. package/src/tui/index.tsx +15 -1
  205. package/src/tui-compiled/index.tsx +234 -222
  206. package/dist/announcement-3g9xg433.js +0 -18
  207. package/dist/conflict-warning-hook-rf17b6et.js +0 -17
  208. package/dist/derive-budgets-9q2d3r93.js +0 -14
  209. package/dist/emscripten-module-49k43zfy.js +0 -507
  210. package/dist/emscripten-module-ex4v0x4z.js +0 -606
  211. package/dist/emscripten-module-jfs3d1dq.js +0 -1536
  212. package/dist/emscripten-module-ss4g4spn.js +0 -4563
  213. package/dist/ffi-bbm9vkfa.js +0 -92
  214. package/dist/ffi-kb5r0x1j.js +0 -92
  215. package/dist/ffi-qnrt0y78.js +0 -80
  216. package/dist/ffi-yx2hpf4p.js +0 -80
  217. package/dist/index-8mtsfhyg.js +0 -146
  218. package/dist/index-8sseka65.js +0 -251
  219. package/dist/index-ss632za9.js +0 -77
  220. package/dist/model-resolution-6w6wzw18.js +0 -17
  221. package/dist/module-ES6BEMUI-1yjdz59c.js +0 -14
  222. package/dist/module-asyncify-2EFITU5U-fdkx4fhy.js +0 -9
  223. package/dist/prompt-context-29qkb28f.js +0 -7
  224. package/dist/recomp-orchestrator-m37p3fq6.js +0 -34
  225. package/dist/rpc-notifications-1ffm755w.js +0 -17
  226. package/dist/safe-notification-target-2a8py95v.js +0 -11
  227. package/dist/send-session-notification-bw7eex6w.js +0 -20
  228. package/dist/storage-meta-session-dse3qv6s.js +0 -23
  229. package/dist/task-config-gv6s04fp.js +0 -18
  230. /package/dist/{index-37yzyeca.js → index-3fe78beq.js} +0 -0
@@ -0,0 +1,64 @@
1
+ import type { MagicContextConfig } from "../config/schema/magic-context";
2
+ import { resolveModelConfigValue } from "./prompt-surface";
3
+
4
+ export type CacheTtlDisplaySource = "config" | "session" | "default";
5
+
6
+ export interface CacheTtlDisplay {
7
+ value: string;
8
+ source: CacheTtlDisplaySource;
9
+ modelKey: string | undefined;
10
+ }
11
+
12
+ export interface ResolveCacheTtlDisplayArgs {
13
+ configured: MagicContextConfig["cache_ttl"];
14
+ configuredExplicitly: boolean;
15
+ modelKey: string | undefined;
16
+ sessionValue: string;
17
+ /** Model key persisted with the last completed assistant response. */
18
+ sessionModelKey: string | null;
19
+ }
20
+
21
+ /**
22
+ * Resolve status-only TTL text without changing the scheduler's persisted truth.
23
+ * Use the session value only when its persisted model key matches the model being
24
+ * displayed; otherwise use live config because the session value may be for another model.
25
+ */
26
+ export function resolveCacheTtlDisplay(args: ResolveCacheTtlDisplayArgs): CacheTtlDisplay {
27
+ if (
28
+ (args.sessionModelKey && (!args.modelKey || args.sessionModelKey === args.modelKey)) ||
29
+ (!args.modelKey && !args.sessionModelKey && args.sessionValue !== "5m")
30
+ ) {
31
+ return {
32
+ value: args.sessionValue || "5m",
33
+ source: "session",
34
+ modelKey: args.modelKey ?? args.sessionModelKey ?? undefined,
35
+ };
36
+ }
37
+
38
+ if (typeof args.configured === "string") {
39
+ return {
40
+ value: args.configured,
41
+ source: args.configuredExplicitly ? "config" : "default",
42
+ modelKey: args.modelKey,
43
+ };
44
+ }
45
+
46
+ const matched = resolveModelConfigValue(args.configured, args.modelKey);
47
+ if (matched) {
48
+ return { value: matched.value, source: "config", modelKey: args.modelKey };
49
+ }
50
+
51
+ return {
52
+ value: args.configured.default ?? "5m",
53
+ source: "default",
54
+ modelKey: args.modelKey,
55
+ };
56
+ }
57
+
58
+ export function formatCacheTtlDisplay(display: CacheTtlDisplay): string {
59
+ if (display.source === "session") return `Cache TTL: ${display.value} (session)`;
60
+ if (display.source === "config") {
61
+ return `Cache TTL: ${display.value} (config for ${display.modelKey ?? "current model"})`;
62
+ }
63
+ return `Cache TTL: ${display.value} (default — no cache_ttl for ${display.modelKey ?? "unknown model"})`;
64
+ }
@@ -0,0 +1,23 @@
1
+ import type { MagicContextConfig } from "../config/schema/magic-context";
2
+ import { getOrCreateSessionMeta, updateSessionMeta } from "../features/magic-context/storage-meta";
3
+ import { resolveModelConfigValue } from "./prompt-surface";
4
+ import type { Database } from "./sqlite";
5
+
6
+ /** Seed only before a completed assistant response has made the session row authoritative. */
7
+ export function seedSessionCacheTtlIfUnsynced(args: {
8
+ db: Database;
9
+ sessionId: string;
10
+ configured: MagicContextConfig["cache_ttl"];
11
+ modelKey: string;
12
+ }): boolean {
13
+ const meta = getOrCreateSessionMeta(args.db, args.sessionId);
14
+ if (meta.lastResponseTime !== 0 || meta.lastObservedModelKey !== null) return false;
15
+ const cacheTtl =
16
+ typeof args.configured === "string"
17
+ ? args.configured
18
+ : (resolveModelConfigValue(args.configured, args.modelKey)?.value ??
19
+ args.configured.default ??
20
+ "5m");
21
+ updateSessionMeta(args.db, args.sessionId, { cacheTtl });
22
+ return true;
23
+ }
@@ -0,0 +1,29 @@
1
+ import { beforeEach, describe, expect, it } from "bun:test";
2
+
3
+ import {
4
+ type ConfigParseFailure,
5
+ claimConfigParseFailuresOnce,
6
+ resetClaimedConfigParseFailuresForTesting,
7
+ } from "./config-diagnostics";
8
+
9
+ const failure: ConfigParseFailure = {
10
+ warningClass: "file-parse",
11
+ source: "user",
12
+ path: "/tmp/magic-context.jsonc",
13
+ line: 1,
14
+ column: 1,
15
+ message: "invalid symbol",
16
+ recovered: true,
17
+ warning: "/tmp/magic-context.jsonc:1:1: invalid symbol",
18
+ };
19
+
20
+ describe("config parse warning notice claims", () => {
21
+ beforeEach(resetClaimedConfigParseFailuresForTesting);
22
+
23
+ it("emits each parse failure once per harness surface", () => {
24
+ expect(claimConfigParseFailuresOnce("opencode", [failure])).toEqual([failure]);
25
+ expect(claimConfigParseFailuresOnce("opencode", [failure])).toEqual([]);
26
+ expect(claimConfigParseFailuresOnce("pi", [failure])).toEqual([failure]);
27
+ expect(claimConfigParseFailuresOnce("pi", [failure])).toEqual([]);
28
+ });
29
+ });
@@ -0,0 +1,64 @@
1
+ export const CONFIG_WARNING_CLASS = {
2
+ FILE_PARSE: "file-parse",
3
+ FILE_IO: "file-io",
4
+ INVALID_LEAF: "invalid-leaf",
5
+ } as const;
6
+
7
+ export type ConfigWarningClass = (typeof CONFIG_WARNING_CLASS)[keyof typeof CONFIG_WARNING_CLASS];
8
+
9
+ export interface ConfigParseFailure {
10
+ warningClass: typeof CONFIG_WARNING_CLASS.FILE_PARSE;
11
+ source: "user" | "project";
12
+ path: string;
13
+ line: number;
14
+ column: number;
15
+ message: string;
16
+ recovered: boolean;
17
+ warning: string;
18
+ }
19
+
20
+ export interface ConfigWarningDetail {
21
+ warningClass: ConfigWarningClass;
22
+ source?: "user" | "project";
23
+ path?: string;
24
+ line?: number;
25
+ column?: number;
26
+ message: string;
27
+ }
28
+
29
+ export function formatConfigParseStatusLine(failure: ConfigParseFailure): string {
30
+ const disposition = failure.recovered
31
+ ? "recovered values applied; fix the file"
32
+ : "running on defaults";
33
+ return `Config: PARSE FAILED (${failure.path}:${failure.line}:${failure.column}) — ${disposition}`;
34
+ }
35
+
36
+ export function formatConfigParseNotice(failures: readonly ConfigParseFailure[]): string {
37
+ const lines = failures.map(
38
+ (failure) => `${formatConfigParseStatusLine(failure)}\n${failure.message}`,
39
+ );
40
+ return ["⚠️ Magic Context config parse failure", ...lines].join("\n\n");
41
+ }
42
+
43
+ const claimedFailures = new Set<string>();
44
+
45
+ function failureKey(surface: string, failure: ConfigParseFailure): string {
46
+ return `${surface}\0${failure.path}\0${failure.line}\0${failure.column}\0${failure.message}`;
47
+ }
48
+
49
+ /** Claim parse-failure notices once for each runtime surface in this process. */
50
+ export function claimConfigParseFailuresOnce(
51
+ surface: "opencode" | "pi",
52
+ failures: readonly ConfigParseFailure[],
53
+ ): ConfigParseFailure[] {
54
+ return failures.filter((failure) => {
55
+ const key = failureKey(surface, failure);
56
+ if (claimedFailures.has(key)) return false;
57
+ claimedFailures.add(key);
58
+ return true;
59
+ });
60
+ }
61
+
62
+ export function resetClaimedConfigParseFailuresForTesting(): void {
63
+ claimedFailures.clear();
64
+ }
@@ -0,0 +1,28 @@
1
+ import type { ConfigParseFailure } from "./config-diagnostics";
2
+ import { formatConfigParseStatusLine } from "./config-diagnostics";
3
+
4
+ export function buildOpenCodeConfigWarningBanner(
5
+ warnings: readonly string[],
6
+ parseFailures: readonly ConfigParseFailure[],
7
+ allParseFailures: readonly ConfigParseFailure[] = parseFailures,
8
+ ): string {
9
+ const parseWarningStrings = new Set(allParseFailures.map((failure) => failure.warning));
10
+ const ordinaryWarnings = warnings.filter(
11
+ (warning) =>
12
+ ![...parseWarningStrings].some((parseWarning) => warning.includes(parseWarning)),
13
+ );
14
+ const entries = [
15
+ ...parseFailures.map(
16
+ (failure) =>
17
+ `- **TOP SEVERITY:** ${formatConfigParseStatusLine(failure)} — ${failure.message}`,
18
+ ),
19
+ ...ordinaryWarnings.map((warning) => `- ${warning}`),
20
+ ];
21
+ return [
22
+ "## ⚠️ Magic Context Config Warning",
23
+ "",
24
+ ...entries,
25
+ "",
26
+ "Fix the reported `magic-context.jsonc` file; Magic Context remains enabled.",
27
+ ].join("\n");
28
+ }
@@ -653,5 +653,29 @@ describe("detectConflicts", () => {
653
653
  const result = await resolveCompactionForBoot(client, 20);
654
654
  expect(result).toBeNull();
655
655
  });
656
+
657
+ it("aborts the host request when its boot timeout expires", async () => {
658
+ let requestSignal: AbortSignal | undefined;
659
+ const client = {
660
+ config: {
661
+ get: (options?: { signal?: AbortSignal }) => {
662
+ requestSignal = options?.signal;
663
+ return new Promise<{ data?: unknown }>((_resolve, reject) => {
664
+ options?.signal?.addEventListener(
665
+ "abort",
666
+ () => reject(options.signal?.reason ?? new Error("aborted")),
667
+ { once: true },
668
+ );
669
+ });
670
+ },
671
+ },
672
+ };
673
+
674
+ const result = await resolveCompactionForBoot(client, 20);
675
+
676
+ expect(result).toBeNull();
677
+ expect(requestSignal).toBeDefined();
678
+ expect(requestSignal?.aborted).toBe(true);
679
+ });
656
680
  });
657
681
  });
@@ -209,7 +209,7 @@ export function detectConflicts(
209
209
  */
210
210
  export interface OpencodeConfigClientLike {
211
211
  config: {
212
- get: () => Promise<{ data?: unknown }>;
212
+ get: (options?: { signal?: AbortSignal }) => Promise<{ data?: unknown }>;
213
213
  };
214
214
  }
215
215
 
@@ -250,13 +250,29 @@ export async function resolveCompactionForBoot(
250
250
  client: OpencodeConfigClientLike,
251
251
  timeoutMs = 2_000,
252
252
  ): Promise<ResolvedCompaction | null> {
253
+ const controller = new AbortController();
254
+ let timer: ReturnType<typeof setTimeout> | undefined;
253
255
  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
- ]);
256
+ // Install the deadline before invoking the generated SDK method. During
257
+ // OpenCode bootstrap, /config can recursively wait for the same project
258
+ // instance that is currently loading this plugin. Deferring invocation
259
+ // ensures the deadline exists before any synchronous SDK/fetch prefix runs.
260
+ const timeout = new Promise<null>((resolve) => {
261
+ timer = setTimeout(
262
+ () => {
263
+ controller.abort(new Error("config.get() timed out"));
264
+ resolve(null);
265
+ },
266
+ Math.max(0, timeoutMs),
267
+ );
268
+ (timer as { unref?: () => void }).unref?.();
269
+ });
270
+ const request = Promise.resolve().then(() =>
271
+ client.config.get({ signal: controller.signal }),
272
+ );
273
+ const result = await Promise.race([request, timeout]);
274
+ if (result === null) return null;
275
+
260
276
  // The SDK's generated `Config` type has no `compaction` key, so read it
261
277
  // defensively from the runtime response.
262
278
  const compaction = (result?.data as ResolvedCompactionBlock | undefined)?.compaction;
@@ -272,6 +288,8 @@ export async function resolveCompactionForBoot(
272
288
  return { auto: compaction.auto, prune: compaction.prune };
273
289
  } catch {
274
290
  return null;
291
+ } finally {
292
+ if (timer) clearTimeout(timer);
275
293
  }
276
294
  }
277
295
 
@@ -13,16 +13,17 @@ export function getDataDir(): string {
13
13
  * Layout:
14
14
  * - OpenCode: `${os.tmpdir()}/opencode/magic-context/`
15
15
  * - Pi: `${os.tmpdir()}/pi/magic-context/`
16
+ * - OMP: `${os.tmpdir()}/omp/magic-context/`
16
17
  *
17
18
  * Why a per-harness subtree of `os.tmpdir()`:
18
19
  * 1. OpenCode Desktop runs as an Electron app with a permission sandbox.
19
20
  * Writing to arbitrary tmp paths can trigger user-visible permission
20
21
  * prompts; the `${tmpdir}/opencode/` subtree is allow-listed by
21
22
  * OpenCode, so anything we put under it never asks for permission.
22
- * 2. Splitting OpenCode from Pi keeps their logs and historian dump
23
- * directories cleanly separated. `doctor --issue` for each harness
24
- * reports diagnostics from the matching subtree, so an OpenCode
25
- * issue report never includes Pi log noise (and vice versa).
23
+ * 2. Splitting harnesses keeps their logs and historian dump directories
24
+ * cleanly separated. `doctor --issue` for each harness reports diagnostics
25
+ * from the matching subtree, so issue reports do not include another
26
+ * harness's log noise.
26
27
  * 3. Pi has no permission sandbox, so the path choice is purely
27
28
  * cosmetic for Pi — it just keeps the layout symmetric.
28
29
  *
@@ -37,13 +38,13 @@ export function getMagicContextTempDir(harness: HarnessId = getHarness()): strin
37
38
  }
38
39
 
39
40
  /**
40
- * Standard log file path the plugin writes to. Pi and OpenCode write to
41
- * SEPARATE logs under their respective harness subtrees so a single
42
- * machine running both harnesses doesn't interleave session traces.
41
+ * Standard log file path the plugin writes to. Each harness writes to a
42
+ * separate log under its own subtree so a machine running multiple harnesses
43
+ * does not interleave session traces.
43
44
  *
44
45
  * The plugin's buffered logger calls this on every flush rather than
45
- * caching, so `setHarness("pi")` taking effect after module load is
46
- * reflected in the next flush.
46
+ * caching, so the boot-time `setHarness()` call is reflected in the next
47
+ * flush.
47
48
  */
48
49
  export function getMagicContextLogPath(harness: HarnessId = getHarness()): string {
49
50
  // An explicit override wins over the harness temp-dir default, so users on
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, it } from "bun:test";
2
2
  import {
3
+ canonicalModelIdentity,
3
4
  modelRefLookupOrder,
4
5
  ompModelRefToCanonical,
5
6
  piModelRefToCanonical,
@@ -8,6 +9,21 @@ import {
8
9
  } from "./harness-provider-map";
9
10
 
10
11
  describe("harness-provider-map", () => {
12
+ describe("canonicalModelIdentity", () => {
13
+ it("collapses Astra route aliases without changing unrelated Copilot models", () => {
14
+ for (const ref of [
15
+ "openai/gpt-6-astra",
16
+ "openai-codex/gpt-6-astra",
17
+ "github-copilot/gpt-6-astra",
18
+ ]) {
19
+ expect(canonicalModelIdentity(ref)).toBe("openai/gpt-6-astra");
20
+ }
21
+ expect(canonicalModelIdentity("github-copilot/claude-opus-5")).toBe(
22
+ "github-copilot/claude-opus-5",
23
+ );
24
+ });
25
+ });
26
+
11
27
  describe("resolveModelRefForPi (canonical -> Pi, used when spawning)", () => {
12
28
  it("maps the diverging auth-plugin providers, preserving the model id", () => {
13
29
  expect(resolveModelRefForPi("openai/gpt-5.5")).toBe("openai-codex/gpt-5.5");
@@ -58,6 +58,10 @@ const OMP_TO_CANONICAL_PROVIDER: Readonly<Record<string, string>> = {
58
58
  "opencode-zen": "opencode",
59
59
  };
60
60
 
61
+ const CANONICAL_MODEL_IDENTITY_ALIASES: Readonly<Record<string, string>> = {
62
+ "github-copilot/gpt-6-astra": "openai/gpt-6-astra",
63
+ };
64
+
61
65
  /** Remap only the provider prefix (text before the first "/"), preserving the
62
66
  * model id verbatim. No "/", empty provider, or unmapped provider -> unchanged.
63
67
  * Lookups are own-property only, so provider ids that collide with
@@ -72,6 +76,17 @@ function remapProviderPrefix(ref: string, map: Readonly<Record<string, string>>)
72
76
  return `${map[provider]}${ref.slice(slash)}`;
73
77
  }
74
78
 
79
+ /**
80
+ * Canonicalize only for identity comparisons and cache policy; callers must not
81
+ * use the result as a provider spawn route. Provider-wide Pi/OMP aliases are
82
+ * normalized first; model-specific aliases stay explicit because GitHub Copilot
83
+ * also serves non-OpenAI model families.
84
+ */
85
+ export function canonicalModelIdentity(ref: string): string {
86
+ const harnessCanonical = piModelRefToCanonical(ompModelRefToCanonical(ref));
87
+ return CANONICAL_MODEL_IDENTITY_ALIASES[harnessCanonical.toLowerCase()] ?? harnessCanonical;
88
+ }
89
+
75
90
  /** Pi-native `provider/model` -> canonical (OpenCode). Identity when unmapped.
76
91
  * Used by the Pi setup wizard so configs it writes stay OpenCode-readable. */
77
92
  export function piModelRefToCanonical(ref: string): string {
@@ -2,7 +2,7 @@
2
2
  * Identifier for the host harness this plugin is running inside.
3
3
  *
4
4
  * Magic Context's SQLite database lives at a vendor-scoped path
5
- * (`~/.local/share/cortexkit/magic-context/`) so OpenCode and Pi can share
5
+ * (`~/.local/share/cortexkit/magic-context/`) so OpenCode, Pi, and OMP can share
6
6
  * project memories, embedding cache, dreamer runs, and other project-scoped
7
7
  * state. Session-scoped tables carry a `harness` column populated from this
8
8
  * module so we can disambiguate which harness wrote each session row,
@@ -13,13 +13,13 @@
13
13
  * happens:
14
14
  * - OpenCode plugin: relies on the default ("opencode") — no setHarness call
15
15
  * needed
16
- * - Pi plugin: calls `setHarness("pi")` before opening the database
16
+ * - Pi-compatible plugin: resolves the host and calls `setHarness("pi" | "omp")` before opening the database
17
17
  *
18
18
  * NEVER read this from configuration or session state — it is a
19
19
  * boot-time constant per plugin instance. Cross-harness leakage is a
20
20
  * correctness bug, not a feature.
21
21
  */
22
- export type HarnessId = "opencode" | "pi";
22
+ export type HarnessId = "opencode" | "pi" | "omp";
23
23
 
24
24
  let currentHarness: HarnessId = "opencode";
25
25
  let harnessLocked = false;
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from "bun:test";
2
2
 
3
- import { parseJsonc } from "./jsonc-parser";
3
+ import { parseJsonc, parseJsoncRecovering } from "./jsonc-parser";
4
4
 
5
5
  describe("parseJsonc prototype-pollution hardening", () => {
6
6
  it("rejects dangerous keys recursively, including inside arrays", () => {
@@ -30,4 +30,17 @@ describe("parseJsonc prototype-pollution hardening", () => {
30
30
  expect(Object.getPrototypeOf(items[0])).toBe(Object.prototype);
31
31
  expect("polluted" in items[0]).toBe(false);
32
32
  });
33
+
34
+ it("preserves hardening when tolerant recovery encounters __proto__", () => {
35
+ const rejected: string[] = [];
36
+ const parsed = parseJsoncRecovering<Record<string, unknown>>(
37
+ '\\{\n"__proto__":{"polluted":true},"cache_ttl":"1h"}',
38
+ { onRejectedKey: (path) => rejected.push(path.join(".")) },
39
+ );
40
+
41
+ expect(parsed.value).toEqual({ cache_ttl: "1h" });
42
+ expect(parsed.issues[0]).toMatchObject({ line: 1, column: 1 });
43
+ expect(rejected).toContain("__proto__");
44
+ expect(({} as { polluted?: boolean }).polluted).toBeUndefined();
45
+ });
33
46
  });
@@ -1,5 +1,15 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
 
3
+ import {
4
+ type ParseError,
5
+ parse as parseJsoncDocument,
6
+ printParseErrorCode,
7
+ } from "jsonc-parser/lib/esm/main.js";
8
+ // ^ Deep ESM import on purpose, same as jsonc-edit.ts: the package's "main" is a
9
+ // UMD bundle whose runtime-relative requires (`./impl/format`) survive bundling
10
+ // verbatim and crash the plugin at boot. The build:dists load-probe gate exists
11
+ // to catch exactly this; do not import the bare package name.
12
+
3
13
  export function stripJsonComments(content: string): string {
4
14
  let result = "";
5
15
  let inString = false;
@@ -155,6 +165,89 @@ export function sanitizeParsedJson<T>(
155
165
  return sanitized as T;
156
166
  }
157
167
 
168
+ export interface JsoncParseIssue {
169
+ line: number;
170
+ column: number;
171
+ offset: number;
172
+ length: number;
173
+ message: string;
174
+ }
175
+
176
+ export interface RecoveringJsoncParseResult<T> {
177
+ value: T;
178
+ issues: JsoncParseIssue[];
179
+ }
180
+
181
+ function lineAndColumnAt(content: string, offset: number): { line: number; column: number } {
182
+ const before = content.slice(0, Math.max(0, offset));
183
+ const lines = before.split(/\r?\n/);
184
+ return { line: lines.length, column: (lines.at(-1)?.length ?? 0) + 1 };
185
+ }
186
+
187
+ function parseIssue(content: string, error: ParseError): JsoncParseIssue {
188
+ const location = lineAndColumnAt(content, error.offset);
189
+ const code = printParseErrorCode(error.error);
190
+ const message = code.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase();
191
+ return {
192
+ ...location,
193
+ offset: error.offset,
194
+ length: error.length,
195
+ message,
196
+ };
197
+ }
198
+
199
+ /**
200
+ * Parse JSONC with the same recovery grammar used by the runtime config loaders.
201
+ * Syntax diagnostics are never discarded: callers must surface every non-empty
202
+ * `issues` result even when the parser recovered a usable object.
203
+ */
204
+ function normalizeJsoncParserObjects(
205
+ value: unknown,
206
+ options: ParsedJsonSanitizerOptions,
207
+ path: string[] = [],
208
+ ): unknown {
209
+ if (Array.isArray(value)) {
210
+ return value.map((entry, index) =>
211
+ normalizeJsoncParserObjects(entry, options, [...path, String(index)]),
212
+ );
213
+ }
214
+ if (value === null || typeof value !== "object") return value;
215
+
216
+ const prototype = Object.getPrototypeOf(value);
217
+ if (prototype !== Object.prototype && prototype !== null) {
218
+ // jsonc-parser assigns an own "__proto__" property through normal JS
219
+ // assignment, which changes only this parsed object's prototype. Treat
220
+ // that prototype as the rejected key and copy only its own safe fields.
221
+ options.onRejectedKey?.([...path, "__proto__"]);
222
+ }
223
+ const normalized: Record<string, unknown> = {};
224
+ for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
225
+ Object.defineProperty(normalized, key, {
226
+ configurable: true,
227
+ enumerable: true,
228
+ writable: true,
229
+ value: normalizeJsoncParserObjects(entry, options, [...path, key]),
230
+ });
231
+ }
232
+ return normalized;
233
+ }
234
+
235
+ export function parseJsoncRecovering<T = unknown>(
236
+ content: string,
237
+ options: ParsedJsonSanitizerOptions = {},
238
+ ): RecoveringJsoncParseResult<T> {
239
+ const errors: ParseError[] = [];
240
+ const parsed = parseJsoncDocument(content, errors, {
241
+ allowTrailingComma: true,
242
+ disallowComments: false,
243
+ allowEmptyContent: false,
244
+ }) as T;
245
+ return {
246
+ value: sanitizeParsedJson(normalizeJsoncParserObjects(parsed, options) as T, options),
247
+ issues: errors.map((error) => parseIssue(content, error)),
248
+ };
249
+ }
250
+
158
251
  export function parseJsonc<T = unknown>(
159
252
  content: string,
160
253
  options: ParsedJsonSanitizerOptions = {},
@@ -130,6 +130,67 @@ describe("model-resolution", () => {
130
130
  });
131
131
  });
132
132
 
133
+ test("uses OMP blocks first, falls back to Pi blocks, then preserves existing defaults", () => {
134
+ const configured = {
135
+ historian: {
136
+ pi: {
137
+ model: { model: "pi/historian", thinking_level: "high" },
138
+ fallback_models: ["pi/historian-fallback"],
139
+ },
140
+ omp: {
141
+ model: { model: "omp/historian", thinking_level: "auto" },
142
+ fallback_models: [{ model: "omp/historian-fallback", thinking_level: "max" }],
143
+ },
144
+ },
145
+ dreamer: {
146
+ tasks: { verify: { schedule: "0 3 * * *" } },
147
+ pi: {
148
+ model: "pi/dreamer",
149
+ tasks: { verify: { model: "pi/verify" } },
150
+ },
151
+ omp: {
152
+ model: "omp/dreamer",
153
+ tasks: {
154
+ verify: {
155
+ model: { model: "omp/verify", thinking_level: "inherit" },
156
+ },
157
+ },
158
+ },
159
+ },
160
+ };
161
+
162
+ expect(resolveHistorianModel(configured, "omp")).toEqual({
163
+ primary: { model: "omp/historian", qualifier: "auto" },
164
+ fallbacks: [{ model: "omp/historian-fallback", qualifier: "max" }],
165
+ });
166
+ expect(
167
+ resolveDreamerTaskModel({ config: configured, harness: "omp", task: "verify" }),
168
+ ).toMatchObject({ primary: { model: "omp/verify", qualifier: "inherit" } });
169
+
170
+ const piFallback = {
171
+ historian: configured.historian,
172
+ dreamer: { ...configured.dreamer, omp: undefined },
173
+ };
174
+ delete (piFallback.historian as { omp?: unknown }).omp;
175
+ expect(resolveHistorianModel(piFallback, "omp")?.primary).toEqual({
176
+ model: "pi/historian",
177
+ qualifier: "high",
178
+ });
179
+ expect(
180
+ resolveDreamerTaskModel({ config: piFallback, harness: "omp", task: "verify" }),
181
+ ).toMatchObject({ primary: { model: "pi/verify" } });
182
+
183
+ expect(resolveHistorianModel({}, "omp")).toEqual({ fallbacks: [] });
184
+ const existingDefaults = resolveDreamerTaskModel({
185
+ config: { dreamer: { tasks: { verify: { schedule: "0 3 * * *" } } } },
186
+ harness: "omp",
187
+ task: "verify",
188
+ });
189
+ expect(existingDefaults.primary).toBeUndefined();
190
+ expect(existingDefaults.fallbacks).toEqual([]);
191
+ expect(existingDefaults.schedule).toBe("0 3 * * *");
192
+ });
193
+
133
194
  test("resolves ordinary task model and scheduling without crossing harnesses", () => {
134
195
  const config = {
135
196
  dreamer: {