@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.
Files changed (192) hide show
  1. package/dist/config/index.d.ts.map +1 -1
  2. package/dist/config/project-security.d.ts +2 -1
  3. package/dist/config/project-security.d.ts.map +1 -1
  4. package/dist/config/schema/magic-context.d.ts +10 -0
  5. package/dist/config/schema/magic-context.d.ts.map +1 -1
  6. package/dist/{conflict-warning-hook-dw9fs934.js → conflict-warning-hook-an4fxhwf.js} +4 -4
  7. package/dist/{derive-budgets-88mxk2w1.js → derive-budgets-0f8xpyw2.js} +3 -2
  8. package/dist/features/magic-context/context-authority.d.ts +12 -0
  9. package/dist/features/magic-context/context-authority.d.ts.map +1 -1
  10. package/dist/features/magic-context/dreamer/classify.d.ts +2 -0
  11. package/dist/features/magic-context/dreamer/classify.d.ts.map +1 -1
  12. package/dist/features/magic-context/dreamer/evaluate-smart-notes.d.ts +2 -0
  13. package/dist/features/magic-context/dreamer/evaluate-smart-notes.d.ts.map +1 -1
  14. package/dist/features/magic-context/dreamer/lease.d.ts +10 -3
  15. package/dist/features/magic-context/dreamer/lease.d.ts.map +1 -1
  16. package/dist/features/magic-context/dreamer/map-memories.d.ts +2 -0
  17. package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
  18. package/dist/features/magic-context/dreamer/module-apply.d.ts +5 -0
  19. package/dist/features/magic-context/dreamer/module-apply.d.ts.map +1 -1
  20. package/dist/features/magic-context/dreamer/promote-primers.d.ts +2 -0
  21. package/dist/features/magic-context/dreamer/promote-primers.d.ts.map +1 -1
  22. package/dist/features/magic-context/dreamer/refresh-primers.d.ts +2 -0
  23. package/dist/features/magic-context/dreamer/refresh-primers.d.ts.map +1 -1
  24. package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
  25. package/dist/features/magic-context/dreamer/task-scheduler.d.ts +5 -2
  26. package/dist/features/magic-context/dreamer/task-scheduler.d.ts.map +1 -1
  27. package/dist/features/magic-context/dreamer/verify.d.ts +2 -0
  28. package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
  29. package/dist/features/magic-context/memory/embedding-openai.d.ts +3 -1
  30. package/dist/features/magic-context/memory/embedding-openai.d.ts.map +1 -1
  31. package/dist/features/magic-context/memory/project-identity.d.ts +6 -0
  32. package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
  33. package/dist/features/magic-context/message-index-async.d.ts.map +1 -1
  34. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  35. package/dist/features/magic-context/mural/compress-cues.d.ts +2 -0
  36. package/dist/features/magic-context/mural/compress-cues.d.ts.map +1 -1
  37. package/dist/features/magic-context/overflow-detection.d.ts.map +1 -1
  38. package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
  39. package/dist/features/magic-context/schema-fence-probe.d.ts +1 -0
  40. package/dist/features/magic-context/schema-fence-probe.d.ts.map +1 -1
  41. package/dist/features/magic-context/smart-notes/condition-compiler.d.ts +1 -0
  42. package/dist/features/magic-context/smart-notes/condition-compiler.d.ts.map +1 -1
  43. package/dist/features/magic-context/smart-notes/runner.d.ts.map +1 -1
  44. package/dist/features/magic-context/smart-notes/wake-plane.d.ts +21 -0
  45. package/dist/features/magic-context/smart-notes/wake-plane.d.ts.map +1 -0
  46. package/dist/features/magic-context/storage-clone.d.ts +6 -0
  47. package/dist/features/magic-context/storage-clone.d.ts.map +1 -1
  48. package/dist/features/magic-context/storage-db.d.ts +14 -3
  49. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  50. package/dist/features/magic-context/storage-identity-merge.d.ts.map +1 -1
  51. package/dist/features/magic-context/storage-meta-persisted.d.ts +22 -0
  52. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  53. package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
  54. package/dist/features/magic-context/storage-meta-shared.d.ts.map +1 -1
  55. package/dist/features/magic-context/storage-primers.d.ts +11 -0
  56. package/dist/features/magic-context/storage-primers.d.ts.map +1 -1
  57. package/dist/features/magic-context/storage-schema-helpers.d.ts.map +1 -1
  58. package/dist/features/magic-context/storage-tags.d.ts +2 -0
  59. package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
  60. package/dist/features/magic-context/storage.d.ts +1 -1
  61. package/dist/features/magic-context/storage.d.ts.map +1 -1
  62. package/dist/features/magic-context/user-memory/review-user-memories.d.ts +2 -0
  63. package/dist/features/magic-context/user-memory/review-user-memories.d.ts.map +1 -1
  64. package/dist/features/magic-context/user-memory/storage-user-memory.d.ts +7 -0
  65. package/dist/features/magic-context/user-memory/storage-user-memory.d.ts.map +1 -1
  66. package/dist/features/magic-context/window-report-ledger.d.ts +70 -0
  67. package/dist/features/magic-context/window-report-ledger.d.ts.map +1 -0
  68. package/dist/hooks/magic-context/child-session-spawn.d.ts +1 -0
  69. package/dist/hooks/magic-context/child-session-spawn.d.ts.map +1 -1
  70. package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
  71. package/dist/hooks/magic-context/compaction-off-transition.d.ts.map +1 -1
  72. package/dist/hooks/magic-context/compartment-render-epoch.d.ts +5 -3
  73. package/dist/hooks/magic-context/compartment-render-epoch.d.ts.map +1 -1
  74. package/dist/hooks/magic-context/embed-session-state.d.ts +1 -1
  75. package/dist/hooks/magic-context/embed-session-state.d.ts.map +1 -1
  76. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  77. package/dist/hooks/magic-context/event-resolvers.d.ts +4 -0
  78. package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
  79. package/dist/hooks/magic-context/execute-status.d.ts +2 -1
  80. package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
  81. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  82. package/dist/hooks/magic-context/inject-compartments.d.ts +8 -0
  83. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  84. package/dist/hooks/magic-context/lkg-replay.d.ts +5 -2
  85. package/dist/hooks/magic-context/lkg-replay.d.ts.map +1 -1
  86. package/dist/hooks/magic-context/lkg-slot.d.ts +20 -0
  87. package/dist/hooks/magic-context/lkg-slot.d.ts.map +1 -1
  88. package/dist/hooks/magic-context/module-state-sync.d.ts +9 -0
  89. package/dist/hooks/magic-context/module-state-sync.d.ts.map +1 -1
  90. package/dist/hooks/magic-context/module-transport.d.ts +20 -3
  91. package/dist/hooks/magic-context/module-transport.d.ts.map +1 -1
  92. package/dist/hooks/magic-context/module-wire.d.ts +6 -1
  93. package/dist/hooks/magic-context/module-wire.d.ts.map +1 -1
  94. package/dist/hooks/magic-context/read-session-formatting.d.ts +1 -0
  95. package/dist/hooks/magic-context/read-session-formatting.d.ts.map +1 -1
  96. package/dist/hooks/magic-context/rust-mode-transform.d.ts +20 -0
  97. package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
  98. package/dist/hooks/magic-context/strip-content.d.ts +31 -1
  99. package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
  100. package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
  101. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +12 -3
  102. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  103. package/dist/hooks/magic-context/transform.d.ts +10 -0
  104. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  105. package/dist/hooks/magic-context/wrapup-orchestrator.d.ts.map +1 -1
  106. package/dist/{index-3nfgbsp4.js → index-0ze9p983.js} +897 -114
  107. package/dist/{index-nsx2n9eg.js → index-4cz5nfxf.js} +3 -3
  108. package/dist/{index-0c9dmw9m.js → index-8mtsfhyg.js} +33 -3
  109. package/dist/{index-f1tnb3vm.js → index-8pqfcghm.js} +21 -9
  110. package/dist/{index-37rt2bka.js → index-cae89eas.js} +26 -7
  111. package/dist/index-cgyfn1s2.js +53 -0
  112. package/dist/{index-z8v31d85.js → index-crke4z5j.js} +144 -14
  113. package/dist/{index-tsgs7td8.js → index-pck9vadr.js} +1 -1
  114. package/dist/index-q0t189ay.js +741 -0
  115. package/dist/{index-3tnkymva.js → index-sw8gnq34.js} +1 -1
  116. package/dist/index.d.ts.map +1 -1
  117. package/dist/index.js +1952 -795
  118. package/dist/plugin/hooks/create-session-hooks.d.ts +3 -0
  119. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  120. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  121. package/dist/plugin/rust-tool-backends.d.ts +4 -0
  122. package/dist/plugin/rust-tool-backends.d.ts.map +1 -1
  123. package/dist/{recomp-orchestrator-7wbbvf87.js → recomp-orchestrator-c3ff16k7.js} +8 -7
  124. package/dist/{safe-notification-target-c11fdda2.js → safe-notification-target-b693f9x3.js} +2 -2
  125. package/dist/{send-session-notification-bgtf4853.js → send-session-notification-dxske5zm.js} +3 -3
  126. package/dist/shared/conflict-detector.d.ts +63 -0
  127. package/dist/shared/conflict-detector.d.ts.map +1 -1
  128. package/dist/shared/conflict-fixer.d.ts.map +1 -1
  129. package/dist/shared/harness-provider-map.d.ts +8 -0
  130. package/dist/shared/harness-provider-map.d.ts.map +1 -1
  131. package/dist/shared/jsonc-edit.d.ts +18 -0
  132. package/dist/shared/jsonc-edit.d.ts.map +1 -0
  133. package/dist/shared/jsonc-parser.d.ts +11 -1
  134. package/dist/shared/jsonc-parser.d.ts.map +1 -1
  135. package/dist/shared/logger.d.ts +8 -0
  136. package/dist/shared/logger.d.ts.map +1 -1
  137. package/dist/shared/models-dev-cache.d.ts +5 -0
  138. package/dist/shared/models-dev-cache.d.ts.map +1 -1
  139. package/dist/shared/prompt-surface-runtime.d.ts.map +1 -1
  140. package/dist/shared/prompt-surface.d.ts +7 -3
  141. package/dist/shared/prompt-surface.d.ts.map +1 -1
  142. package/dist/shared/rpc-client.d.ts.map +1 -1
  143. package/dist/shared/rpc-types.d.ts +14 -0
  144. package/dist/shared/rpc-types.d.ts.map +1 -1
  145. package/dist/shared/rpc-utils.d.ts +32 -6
  146. package/dist/shared/rpc-utils.d.ts.map +1 -1
  147. package/dist/shared/tui-preferences.d.ts +9 -0
  148. package/dist/shared/tui-preferences.d.ts.map +1 -1
  149. package/dist/shared/window-geometry.d.ts +89 -0
  150. package/dist/shared/window-geometry.d.ts.map +1 -0
  151. package/dist/{storage-meta-session-kh5egr11.js → storage-meta-session-mk4gsnke.js} +4 -3
  152. package/dist/tools/ctx-memory/tools.d.ts.map +1 -1
  153. package/dist/tools/ctx-note/tools.d.ts.map +1 -1
  154. package/dist/tui/data/context-db.d.ts.map +1 -1
  155. package/dist/tui-compiled/data/context-db.d.ts.map +1 -1
  156. package/package.json +2 -1
  157. package/src/config/index.test.ts +23 -0
  158. package/src/config/index.ts +43 -9
  159. package/src/config/latch-permanence-guard.test.ts +218 -0
  160. package/src/config/project-security.ts +10 -1
  161. package/src/config/schema/magic-context.test.ts +2 -1
  162. package/src/config/schema/magic-context.ts +13 -3
  163. package/src/shared/__fixtures__/window-overlay.v1.json +337 -0
  164. package/src/shared/conflict-detector.test.ts +176 -1
  165. package/src/shared/conflict-detector.ts +126 -3
  166. package/src/shared/conflict-fixer.test.ts +174 -0
  167. package/src/shared/conflict-fixer.ts +66 -70
  168. package/src/shared/harness-provider-map.test.ts +20 -0
  169. package/src/shared/harness-provider-map.ts +19 -0
  170. package/src/shared/jsonc-edit.ts +295 -0
  171. package/src/shared/jsonc-parser.test.ts +33 -0
  172. package/src/shared/jsonc-parser.ts +54 -2
  173. package/src/shared/logger.test.ts +119 -0
  174. package/src/shared/logger.ts +33 -15
  175. package/src/shared/models-dev-cache.test.ts +17 -0
  176. package/src/shared/models-dev-cache.ts +58 -6
  177. package/src/shared/prompt-surface-runtime.ts +5 -3
  178. package/src/shared/prompt-surface.test.ts +91 -2
  179. package/src/shared/prompt-surface.ts +35 -11
  180. package/src/shared/rpc-client.ts +4 -2
  181. package/src/shared/rpc-types.ts +14 -0
  182. package/src/shared/rpc-utils.test.ts +173 -13
  183. package/src/shared/rpc-utils.ts +202 -18
  184. package/src/shared/tui-preferences.test.ts +52 -2
  185. package/src/shared/tui-preferences.ts +42 -17
  186. package/src/shared/window-geometry.test.ts +309 -0
  187. package/src/shared/window-geometry.ts +629 -0
  188. package/src/tui/data/context-db.ts +5 -0
  189. package/src/tui/index.tsx +22 -0
  190. package/src/tui-compiled/data/context-db.ts +5 -0
  191. package/src/tui-compiled/index.tsx +319 -255
  192. package/dist/index-wgvxkqpx.js +0 -342
@@ -30,9 +30,15 @@ import { join } from "node:path";
30
30
  import type { ContextLimitProvenance } from "./context-limit-provenance";
31
31
  import { getMagicContextStorageDir } from "./data-path";
32
32
  import { getHarness } from "./harness";
33
- import { piModelRefToCanonical } from "./harness-provider-map";
33
+ import { modelRefLookupOrder } from "./harness-provider-map";
34
34
  import { sessionLog } from "./logger";
35
35
  import { shouldEnforcePrivateStoragePermissions } from "./storage-permissions";
36
+ import {
37
+ deriveWindowGeometry,
38
+ getWindowOverlay,
39
+ resolveWindowOverlayFacts,
40
+ type WindowGeometryResult,
41
+ } from "./window-geometry";
36
42
 
37
43
  interface OpencodeClientLike {
38
44
  config: {
@@ -196,14 +202,11 @@ function isFinitePositive(value: number | undefined): value is number {
196
202
  }
197
203
 
198
204
  function modelKeyLookupOrder(providerID: string, modelID: string): string[] {
199
- const full = `${providerID}/${modelID}`;
200
- const canonicalFull = piModelRefToCanonical(full);
201
- const candidates = [full, canonicalFull, modelID];
205
+ const candidates = [...modelRefLookupOrder(`${providerID}/${modelID}`), modelID];
202
206
  const colon = modelID.lastIndexOf(":");
203
207
  if (colon > 0) {
204
208
  const bareModel = modelID.slice(0, colon);
205
- const providerBare = `${providerID}/${bareModel}`;
206
- candidates.push(providerBare, piModelRefToCanonical(providerBare), bareModel);
209
+ candidates.push(...modelRefLookupOrder(`${providerID}/${bareModel}`), bareModel);
207
210
  }
208
211
  return [...new Set(candidates)];
209
212
  }
@@ -493,6 +496,50 @@ async function refreshModelLimitsOnce(client: OpencodeClientLike): Promise<boole
493
496
  * `ctx.model.contextWindow`), so for Pi this returns `undefined` and Pi's
494
497
  * own resolution path is used.
495
498
  */
499
+ export function getSdkWindowGeometry(
500
+ providerID: string,
501
+ modelID: string,
502
+ detectedContextLimit?: number,
503
+ options?: {
504
+ detectedLimitProvenance?: ContextLimitProvenance;
505
+ harness?: "opencode" | "pi";
506
+ },
507
+ ): WindowGeometryResult | undefined {
508
+ loadPersistedApiCacheOnce();
509
+ const metadata = lookupMetadataWithTagFallback(apiCache, providerID, modelID);
510
+ if (!metadata) return undefined;
511
+ const rawContext = metadata.contextLimit ?? metadata.limit;
512
+ const promptOnlyDetected =
513
+ options?.detectedLimitProvenance === "prompt_only" && isFinitePositive(detectedContextLimit)
514
+ ? detectedContextLimit
515
+ : undefined;
516
+ const result = deriveWindowGeometry(
517
+ providerID,
518
+ modelID,
519
+ {
520
+ context: rawContext,
521
+ input: metadata.inputLimit,
522
+ output: metadata.outputLimit,
523
+ },
524
+ {
525
+ overlay: resolveWindowOverlayFacts(providerID, modelID, getWindowOverlay()),
526
+ reserveConfig: outputReserveConfig,
527
+ harness: options?.harness ?? "opencode",
528
+ contextCap:
529
+ promptOnlyDetected === undefined && isFinitePositive(detectedContextLimit)
530
+ ? detectedContextLimit
531
+ : undefined,
532
+ },
533
+ );
534
+ if (!result || promptOnlyDetected === undefined) return result;
535
+ const usableSoft = promptOnlyDetected;
536
+ return {
537
+ ...result,
538
+ usableSoft,
539
+ usableHard: Math.max(usableSoft, Math.min(result.usableHard, promptOnlyDetected)),
540
+ };
541
+ }
542
+
496
543
  export function getSdkContextLimit(
497
544
  providerID: string,
498
545
  modelID: string,
@@ -502,6 +549,11 @@ export function getSdkContextLimit(
502
549
  detectedLimitProvenance?: ContextLimitProvenance;
503
550
  },
504
551
  ): number | undefined {
552
+ if (options?.reservation !== "none") {
553
+ return getSdkWindowGeometry(providerID, modelID, detectedContextLimit, {
554
+ detectedLimitProvenance: options?.detectedLimitProvenance,
555
+ })?.usableSoft;
556
+ }
505
557
  loadPersistedApiCacheOnce();
506
558
  const metadata = lookupMetadataWithTagFallback(apiCache, providerID, modelID);
507
559
  if (!metadata) return undefined;
@@ -13,6 +13,7 @@ import {
13
13
  CTX_REDUCE_LIGHT_DESCRIPTION,
14
14
  CTX_SEARCH_LIGHT_DESCRIPTION,
15
15
  } from "../tools/light-descriptions";
16
+ import { piModelRefToCanonical } from "./harness-provider-map";
16
17
  import { detectConfigFile } from "./jsonc-parser";
17
18
  import {
18
19
  type PromptSurfaceConfig,
@@ -242,13 +243,14 @@ export function createPromptSurfaceGuidanceEpochCache(runtime: PromptSurfaceRunt
242
243
 
243
244
  return {
244
245
  resolve(sessionId, config, modelKey) {
246
+ const canonicalModelKey = modelKey ? piModelRefToCanonical(modelKey) : undefined;
245
247
  const cached = epochs.get(sessionId);
246
- if (cached && cached.config === config && cached.modelKey === modelKey) {
248
+ if (cached && cached.config === config && cached.modelKey === canonicalModelKey) {
247
249
  return cached.selection;
248
250
  }
249
251
 
250
- const selection = runtime.resolveGuidance(config, modelKey);
251
- epochs.set(sessionId, { config, modelKey, selection });
252
+ const selection = runtime.resolveGuidance(config, canonicalModelKey);
253
+ epochs.set(sessionId, { config, modelKey: canonicalModelKey, selection });
252
254
  return selection;
253
255
  },
254
256
  clear(sessionId) {
@@ -1,7 +1,13 @@
1
1
  import { describe, expect, it } from "bun:test";
2
+ import { readFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
2
4
 
3
5
  import { resolveCacheTtl } from "../hooks/magic-context/event-resolvers";
4
- import { modelKeyLookupOrder, resolvePromptSurface } from "./prompt-surface";
6
+ import {
7
+ modelKeyLookupOrder,
8
+ promptSurfaceConfigIdentity,
9
+ resolvePromptSurface,
10
+ } from "./prompt-surface";
5
11
 
6
12
  describe("prompt-surface resolution", () => {
7
13
  it("keeps cache_ttl and prompt-surface routing on the same lookup walk", () => {
@@ -11,6 +17,7 @@ describe("prompt-surface resolution", () => {
11
17
  "google/*": "light" as const,
12
18
  "CaseSensitive/model": "light" as const,
13
19
  "progressive/base": "light" as const,
20
+ "bare-model": "light" as const,
14
21
  };
15
22
  const cacheTtl = {
16
23
  default: "full",
@@ -26,6 +33,7 @@ describe("prompt-surface resolution", () => {
26
33
  ["casesensitive/model", "full"],
27
34
  ["CaseSensitive/model", "light"],
28
35
  ["progressive/base-extra", "light"],
36
+ ["provider/bare-model", "light"],
29
37
  ["unknown/model", "full"],
30
38
  [undefined, "full"],
31
39
  ] as const;
@@ -39,11 +47,88 @@ describe("prompt-surface resolution", () => {
39
47
  }
40
48
  });
41
49
 
42
- it("applies exact, wildcard, then default precedence", () => {
50
+ it("resolves Pi-native and canonical model keys with canonical precedence", () => {
51
+ const nativeKey = "openai-codex/gpt-5.6-sol";
52
+ const canonicalKey = "openai/gpt-5.6-sol";
53
+
54
+ expect(resolveCacheTtl({ default: "5m", [nativeKey]: "60m" }, canonicalKey)).toBe("60m");
55
+ expect(
56
+ resolvePromptSurface(
57
+ { default: "full", models: { [nativeKey]: "light" } },
58
+ canonicalKey,
59
+ ),
60
+ ).toEqual({ preset: "light", source: "exact" });
61
+ expect(
62
+ resolveCacheTtl(
63
+ { default: "5m", [nativeKey]: "60m", [canonicalKey]: "30m" },
64
+ nativeKey,
65
+ ),
66
+ ).toBe("30m");
67
+ expect(
68
+ resolvePromptSurface(
69
+ { default: "full", models: { [nativeKey]: "light", [canonicalKey]: "full" } },
70
+ nativeKey,
71
+ ),
72
+ ).toEqual({ preset: "full", source: "exact" });
73
+ });
74
+
75
+ it("leaves unknown provider keys unchanged", () => {
76
+ expect(
77
+ resolveCacheTtl(
78
+ { default: "5m", "custom-provider/model": "1m" },
79
+ "custom-provider/model",
80
+ ),
81
+ ).toBe("1m");
82
+ expect(modelKeyLookupOrder("custom-provider/model")[0]).toEqual({
83
+ key: "custom-provider/model",
84
+ source: "exact",
85
+ });
86
+ });
87
+
88
+ it("derives a stable config identity independent of object key order", () => {
89
+ const left = promptSurfaceConfigIdentity({
90
+ default: "full",
91
+ models: { "provider/b": "light", "provider/a": "full" },
92
+ tool_descriptions: { ctx_search: "Search", ctx_note: "Note" },
93
+ });
94
+ const right = promptSurfaceConfigIdentity({
95
+ tool_descriptions: { ctx_note: "Note", ctx_search: "Search" },
96
+ models: { "provider/a": "full", "provider/b": "light" },
97
+ default: "full",
98
+ });
99
+ expect(left).toBe(right);
100
+ expect(
101
+ promptSurfaceConfigIdentity({ default: "light", models: { "provider/a": "full" } }),
102
+ ).not.toBe(right);
103
+ });
104
+
105
+ it("matches the Rust cache_ttl resolver over shared routing vectors", () => {
106
+ const vectors = JSON.parse(
107
+ readFileSync(
108
+ resolve(
109
+ import.meta.dir,
110
+ "../../../../crates/mc-module/testdata/cache-ttl-routing-vectors.json",
111
+ ),
112
+ "utf8",
113
+ ),
114
+ ) as {
115
+ default: string;
116
+ models: Record<string, string>;
117
+ cases: Array<{ name: string; modelKey: string; expected: string }>;
118
+ };
119
+ const config = { default: vectors.default, ...vectors.models };
120
+
121
+ for (const vector of vectors.cases) {
122
+ expect(resolveCacheTtl(config, vector.modelKey), vector.name).toBe(vector.expected);
123
+ }
124
+ });
125
+
126
+ it("applies exact, bare, wildcard, then default precedence", () => {
43
127
  const config = {
44
128
  default: "full" as const,
45
129
  models: {
46
130
  "provider/model": "light" as const,
131
+ bare: "light" as const,
47
132
  "provider/*": "full" as const,
48
133
  },
49
134
  };
@@ -52,6 +137,10 @@ describe("prompt-surface resolution", () => {
52
137
  preset: "light",
53
138
  source: "exact",
54
139
  });
140
+ expect(resolvePromptSurface(config, "provider/bare")).toEqual({
141
+ preset: "light",
142
+ source: "bare",
143
+ });
55
144
  expect(resolvePromptSurface(config, "provider/other")).toEqual({
56
145
  preset: "full",
57
146
  source: "wildcard",
@@ -1,3 +1,5 @@
1
+ import { modelRefLookupOrder } from "./harness-provider-map";
2
+
1
3
  /** The built-in prompt-surface variants. */
2
4
  export type PromptSurfacePreset = "full" | "light";
3
5
 
@@ -13,14 +15,29 @@ export interface PromptSurfaceConfig {
13
15
  tool_descriptions?: Readonly<Record<string, string>>;
14
16
  }
15
17
 
16
- export type PromptSurfaceResolutionSource = "exact" | "wildcard" | "default";
18
+ /** Stable wire identity for the config fields that can alter a served prompt surface. */
19
+ export function promptSurfaceConfigIdentity(config: PromptSurfaceConfig | undefined): string {
20
+ return JSON.stringify({
21
+ default: config?.default ?? "full",
22
+ models: Object.entries(config?.models ?? {}).sort(([left], [right]) =>
23
+ left < right ? -1 : left > right ? 1 : 0,
24
+ ),
25
+ guidanceOverridePath: config?.guidance_override_path ?? null,
26
+ toolDescriptions: Object.entries(config?.tool_descriptions ?? {}).sort(([left], [right]) =>
27
+ left < right ? -1 : left > right ? 1 : 0,
28
+ ),
29
+ });
30
+ }
31
+
32
+ export type PromptSurfaceResolutionSource = "exact" | "bare" | "wildcard" | "default";
17
33
 
18
- /** Validate the literal provider/model and provider/* key grammar. */
34
+ /** Validate bare model, provider/model, and provider/* routing keys. */
19
35
  export function isValidPromptSurfaceModelKey(key: string): boolean {
20
36
  if (key.length === 0 || key.trim() !== key) return false;
21
37
 
22
38
  const slash = key.indexOf("/");
23
- if (slash <= 0 || slash === key.length - 1) return false;
39
+ if (slash < 0) return !key.includes("*");
40
+ if (slash === 0 || slash === key.length - 1) return false;
24
41
 
25
42
  const provider = key.slice(0, slash);
26
43
  const modelID = key.slice(slash + 1);
@@ -42,7 +59,7 @@ export function isValidPromptSurfaceModelKey(key: string): boolean {
42
59
  );
43
60
  }
44
61
 
45
- export type ModelKeyLookupSource = PromptSurfaceResolutionSource | "bare";
62
+ export type ModelKeyLookupSource = Exclude<PromptSurfaceResolutionSource, "default">;
46
63
 
47
64
  export interface ModelKeyCandidate {
48
65
  key: string;
@@ -54,6 +71,8 @@ export interface ModelKeyCandidate {
54
71
  * provider/model boundary is the first slash; the rest of the string remains
55
72
  * the model ID, including additional slashes. Candidates are case-sensitive.
56
73
  *
74
+ * Known harness provider aliases are checked canonical-first at each specificity,
75
+ * so one shared config works on every harness and canonical wins on collisions.
57
76
  * Provider wildcards are checked after progressively less-specific model keys,
58
77
  * but before the caller's default. That keeps an exact or base-model override
59
78
  * authoritative while still allowing `provider/*` to cover otherwise-unlisted
@@ -67,10 +86,15 @@ export function modelKeyLookupOrder(modelKey: string | undefined): ModelKeyCandi
67
86
 
68
87
  const provider = modelKey.slice(0, slash);
69
88
  let modelID = modelKey.slice(slash + 1);
89
+ const providerRefs = modelRefLookupOrder(`${provider}/${modelID}`);
70
90
  const candidates: ModelKeyCandidate[] = [];
71
91
 
72
92
  while (modelID.length > 0) {
73
- candidates.push({ key: `${provider}/${modelID}`, source: "exact" });
93
+ for (const providerRef of providerRefs) {
94
+ const providerSlash = providerRef.indexOf("/");
95
+ const providerPrefix = providerRef.slice(0, providerSlash);
96
+ candidates.push({ key: `${providerPrefix}/${modelID}`, source: "exact" });
97
+ }
74
98
  candidates.push({ key: modelID, source: "bare" });
75
99
 
76
100
  const lastDash = modelID.lastIndexOf("-");
@@ -78,7 +102,11 @@ export function modelKeyLookupOrder(modelKey: string | undefined): ModelKeyCandi
78
102
  modelID = modelID.slice(0, lastDash);
79
103
  }
80
104
 
81
- candidates.push({ key: `${provider}/*`, source: "wildcard" });
105
+ for (const providerRef of providerRefs) {
106
+ const providerSlash = providerRef.indexOf("/");
107
+ const providerPrefix = providerRef.slice(0, providerSlash);
108
+ candidates.push({ key: `${providerPrefix}/*`, source: "wildcard" });
109
+ }
82
110
 
83
111
  const seen = new Set<string>();
84
112
  return candidates.filter((candidate) => {
@@ -117,11 +145,7 @@ export function resolvePromptSurface(
117
145
  const fallback = config?.default ?? "full";
118
146
  const match = resolveModelConfigValue(config?.models, modelKey);
119
147
 
120
- // Bare model keys are accepted by cache_ttl for compatibility, but the
121
- // prompt_surface schema deliberately accepts only provider/model keys and
122
- // provider wildcards. Ignore a bare entry if an unvalidated object reaches
123
- // this API so prompt routing keeps its documented grammar.
124
- if (match && match.source !== "bare") {
148
+ if (match) {
125
149
  return { preset: match.value, source: match.source };
126
150
  }
127
151
 
@@ -149,7 +149,9 @@ export class MagicContextRpcClient {
149
149
  for (const entry of readdirSync(this.portDir)) {
150
150
  if (!entry.startsWith("port-") || !entry.endsWith(".json")) continue;
151
151
  const record = parseRpcPortFile(readFileSync(join(this.portDir, entry), "utf-8"));
152
- if (!record || !isPidAlive(record.pid)) continue;
152
+ // A denied liveness probe still leaves this as a candidate; the
153
+ // mandatory health check below confirms whether its RPC server is reachable.
154
+ if (!record || isPidAlive(record.pid) === "dead") continue;
153
155
  records.push(record);
154
156
  }
155
157
  } catch {
@@ -158,7 +160,7 @@ export class MagicContextRpcClient {
158
160
 
159
161
  try {
160
162
  const legacy = parseRpcPortFile(readFileSync(this.legacyPortFilePath, "utf-8"));
161
- if (legacy && (!legacy.pid || isPidAlive(legacy.pid))) records.push(legacy);
163
+ if (legacy && (!legacy.pid || isPidAlive(legacy.pid) !== "dead")) records.push(legacy);
162
164
  } catch {
163
165
  // Legacy discovery is optional.
164
166
  }
@@ -7,6 +7,7 @@ import type {
7
7
  DreamTaskBacklogMap,
8
8
  DreamTaskProgress,
9
9
  } from "../features/magic-context/dreamer/task-registry";
10
+ import type { LoggerDiagnostics } from "./logger";
10
11
 
11
12
  export interface SidebarSnapshot {
12
13
  sessionId: string;
@@ -135,6 +136,17 @@ export interface StatusDetail extends SidebarSnapshot {
135
136
  isSubagent: boolean;
136
137
  pendingOps: Array<{ tagId: number; operation: string }>;
137
138
  contextLimit: number;
139
+ windowGeometry?: {
140
+ usableSoft: number;
141
+ usableHard: number;
142
+ geometry: "shared_upfront" | "shared_truncating" | "separate";
143
+ derivation: {
144
+ window: number;
145
+ reserve: number;
146
+ reserveSource: "output_catalog" | "output_config" | "wall_margin" | "none";
147
+ geometry: "shared_upfront" | "shared_truncating" | "separate";
148
+ };
149
+ };
138
150
  /**
139
151
  * Parsed cache TTL in ms. -1 = never expires (cacheTtl "never"; Infinity
140
152
  * cannot ride JSON-RPC and 0 would be indistinguishable from unset). The
@@ -170,6 +182,8 @@ export interface StatusDetail extends SidebarSnapshot {
170
182
  toastDurationMs: number;
171
183
  /** One-line status data for the experimental memory mural. */
172
184
  mural?: { present: boolean; ageMs: number | null };
185
+ /** Runtime logger write failures observed by this plugin process. */
186
+ loggerDiagnostics: LoggerDiagnostics;
173
187
  /**
174
188
  * Stable storage-version probe: "which schema is the DB at, which fence does
175
189
  * this binary carry". Field names are deliberately snake_case, mirroring the