@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
@@ -1,4 +1,4 @@
1
- export type ModelHarness = "opencode" | "pi";
1
+ export type ModelHarness = "opencode" | "pi" | "omp";
2
2
 
3
3
  export interface ResolvedModelEntry {
4
4
  /** Canonical provider/model reference used for identity and model selection. */
@@ -27,6 +27,14 @@ function hasOwn(record: ConfigRecord, key: string): boolean {
27
27
  return Object.hasOwn(record, key);
28
28
  }
29
29
 
30
+ function resolveHarnessBlock(
31
+ container: ConfigRecord | undefined,
32
+ harness: ModelHarness,
33
+ ): ConfigRecord | undefined {
34
+ const selected = harness === "omp" ? (container?.omp ?? container?.pi) : container?.[harness];
35
+ return asRecord(selected);
36
+ }
37
+
30
38
  /**
31
39
  * Collapses the string and object spellings of a harness model entry into the
32
40
  * same model identity. Qualifiers stay separate because they alter an attempt,
@@ -98,15 +106,15 @@ export interface ResolvedHistorianModel {
98
106
  }
99
107
 
100
108
  /**
101
- * Resolve historian model attempts from exactly one harness block. Flat keys
102
- * and the other harness are deliberately never consulted after cutover.
109
+ * Resolve historian model attempts from the selected harness block. OMP uses
110
+ * its own block when present and falls back to Pi when it is absent.
103
111
  */
104
112
  export function resolveHistorianModel(
105
113
  config: unknown,
106
114
  harness: ModelHarness,
107
115
  ): ResolvedHistorianModel {
108
116
  const historian = asRecord(asRecord(config)?.historian);
109
- const block = asRecord(historian?.[harness]);
117
+ const block = resolveHarnessBlock(historian, harness);
110
118
  if (!block) return { fallbacks: [] };
111
119
 
112
120
  return {
@@ -139,7 +147,7 @@ export function resolveDreamerTaskModel(args: {
139
147
  muralModel?: unknown;
140
148
  }): ResolvedDreamerTaskModel {
141
149
  const dreamer = asRecord(asRecord(args.config)?.dreamer);
142
- const harnessBlock = asRecord(dreamer?.[args.harness]);
150
+ const harnessBlock = resolveHarnessBlock(dreamer, args.harness);
143
151
  const taskBlock = asRecord(asRecord(harnessBlock?.tasks)?.[args.task]);
144
152
  const schedulingTask = asRecord(asRecord(dreamer?.tasks)?.[args.task]);
145
153
  if (!harnessBlock) {
@@ -211,6 +219,7 @@ export function resolveOpenCodeAgentOverrides(agent: unknown): Record<string, un
211
219
  thinking_level: _thinking,
212
220
  opencode,
213
221
  pi,
222
+ omp,
214
223
  tasks,
215
224
  ...rest
216
225
  } = source;
@@ -3,6 +3,7 @@ import type { createOpencodeClient } from "@opencode-ai/sdk";
3
3
  import { detectOverflow } from "../features/magic-context/overflow-detection";
4
4
  import { log } from "./logger";
5
5
  import type { ModelInput } from "./model-resolution";
6
+ import { sanitizeDiagnosticText } from "./redaction";
6
7
  import { parseProviderModel, toModelEntry } from "./resolve-fallbacks";
7
8
 
8
9
  type Client = ReturnType<typeof createOpencodeClient>;
@@ -100,6 +101,33 @@ export interface ValidatedPromptRetryResult<TOutput, TValidated> {
100
101
  attempt: PromptAttemptInfo;
101
102
  }
102
103
 
104
+ export type PromptFailureClass =
105
+ | "provider_timeout"
106
+ | "provider_error"
107
+ | "empty_completion"
108
+ | "no_models"
109
+ | "child_aborted"
110
+ | "parse_failed"
111
+ | "unknown";
112
+
113
+ /** Diagnostic facts retained when a validated child prompt ultimately fails. */
114
+ export interface PromptFailureDetail {
115
+ failureClass: PromptFailureClass;
116
+ modelAttempted: string | null;
117
+ modelsTried: string[];
118
+ providerError: string | null;
119
+ timeoutMs: number | null;
120
+ childSessionId: string | null;
121
+ }
122
+
123
+ const promptFailureDetails = new WeakMap<object, PromptFailureDetail>();
124
+
125
+ export function getPromptFailureDetail(error: unknown): PromptFailureDetail | null {
126
+ return error !== null && typeof error === "object"
127
+ ? (promptFailureDetails.get(error) ?? null)
128
+ : null;
129
+ }
130
+
103
131
  export interface ModelSuggestionInfo {
104
132
  providerID: string;
105
133
  modelID: string;
@@ -279,6 +307,61 @@ function shortErr(error: unknown): string {
279
307
  return extractMessage(error);
280
308
  }
281
309
 
310
+ type PromptFailurePhase = "prompt" | "output" | "validation";
311
+
312
+ interface FailedAttempt {
313
+ error: unknown;
314
+ failureClass: PromptFailureClass;
315
+ attempt: PromptAttemptInfo;
316
+ }
317
+
318
+ function classifyPromptFailure(
319
+ error: unknown,
320
+ phase: PromptFailurePhase,
321
+ externalSignal?: AbortSignal,
322
+ ): PromptFailureClass {
323
+ const message = extractMessage(error);
324
+ if (externalSignal?.aborted || message === "prompt aborted by external signal") {
325
+ return "child_aborted";
326
+ }
327
+ if (/^prompt timed out after \d+ms$/.test(message)) return "provider_timeout";
328
+ if (phase === "validation") {
329
+ if (/returned no (?:assistant )?output|no assistant output/i.test(message)) {
330
+ return "empty_completion";
331
+ }
332
+ if (error instanceof Error && error.name === "DreamerProviderOutputFailureError") {
333
+ return "provider_error";
334
+ }
335
+ return "parse_failed";
336
+ }
337
+ return phase === "prompt" || phase === "output" ? "provider_error" : "unknown";
338
+ }
339
+
340
+ function throwWithPromptFailure(
341
+ legacyError: unknown,
342
+ failedAttempts: FailedAttempt[],
343
+ args: PromptArgs,
344
+ timeoutMs: number,
345
+ ): never {
346
+ const error =
347
+ legacyError instanceof Error ? legacyError : new Error(extractMessage(legacyError));
348
+ const last = failedAttempts.at(-1);
349
+ const failureClass = last?.failureClass ?? "unknown";
350
+ const providerError =
351
+ failureClass === "provider_error"
352
+ ? sanitizeDiagnosticText(shortErr(last?.error ?? legacyError)).slice(0, 500)
353
+ : null;
354
+ promptFailureDetails.set(error, {
355
+ failureClass,
356
+ modelAttempted: last?.attempt.label ?? null,
357
+ modelsTried: failedAttempts.map((failure) => failure.attempt.label),
358
+ providerError,
359
+ timeoutMs: failureClass === "provider_timeout" ? timeoutMs : null,
360
+ childSessionId: args.path.id || null,
361
+ });
362
+ throw error;
363
+ }
364
+
282
365
  /**
283
366
  * Try a single prompt attempt against the supplied body, with the existing
284
367
  * single-suggestion retry layered inside (so "did you mean X?" still self-heals
@@ -449,23 +532,43 @@ async function attemptAndValidate<TOutput, TValidated>(
449
532
  attempt: PromptAttemptInfo,
450
533
  options: ValidatedPromptRetryOptions<TOutput, TValidated>,
451
534
  ): Promise<ValidatedPromptRetryResult<TOutput, TValidated>> {
452
- await attemptOnce(client, args, timeoutMs, signal, callContext, attempt.label);
453
- const output = await options.fetchOutput(args, attempt);
454
- const validated = await options.validateOutput(output, attempt);
455
- return { output, validated, attempt };
535
+ try {
536
+ await attemptOnce(client, args, timeoutMs, signal, callContext, attempt.label);
537
+ } catch (error) {
538
+ throw {
539
+ error,
540
+ failureClass: classifyPromptFailure(error, "prompt", signal),
541
+ attempt,
542
+ } satisfies FailedAttempt;
543
+ }
544
+
545
+ let output: TOutput;
546
+ try {
547
+ output = await options.fetchOutput(args, attempt);
548
+ } catch (error) {
549
+ throw {
550
+ error,
551
+ failureClass: classifyPromptFailure(error, "output", signal),
552
+ attempt,
553
+ } satisfies FailedAttempt;
554
+ }
555
+
556
+ try {
557
+ const validated = await options.validateOutput(output, attempt);
558
+ return { output, validated, attempt };
559
+ } catch (error) {
560
+ throw {
561
+ error,
562
+ failureClass: classifyPromptFailure(error, "validation", signal),
563
+ attempt,
564
+ } satisfies FailedAttempt;
565
+ }
456
566
  }
457
567
 
458
568
  /**
459
- * Run a prompt with model fallback support, but accept an attempt only after the
460
- * caller validates the model's actual output. This covers "empty success" cases
461
- * where the provider/OpenCode prompt call completes successfully but the subagent
462
- * produced no usable assistant text / JSON.
463
- *
464
- * The happy path is still one prompt + one caller-owned output fetch: callers
465
- * should use the returned output instead of fetching messages a second time.
466
- * Validation failures are retryable across configured fallback models. If every
467
- * attempt produces invalid output (or otherwise fails retryably), the first
468
- * failure is re-thrown so callers surface the original failure semantics.
569
+ * Run a prompt across the configured model chain and retain which attempt failed.
570
+ * Output validation remains caller-owned so empty completions and malformed
571
+ * responses can be distinguished from provider transport failures.
469
572
  */
470
573
  export async function promptSyncWithValidatedOutputRetry<TOutput, TValidated = TOutput>(
471
574
  client: Client,
@@ -486,7 +589,7 @@ export async function promptSyncWithValidatedOutputRetry<TOutput, TValidated = T
486
589
  ? `${baseBody.model.providerID}/${baseBody.model.modelID}`
487
590
  : "primary";
488
591
  const totalAttempts = fallbacks.length + 1;
489
-
592
+ const failedAttempts: FailedAttempt[] = [];
490
593
  let firstError: unknown = null;
491
594
  let lastError: unknown = null;
492
595
 
@@ -506,17 +609,21 @@ export async function promptSyncWithValidatedOutputRetry<TOutput, TValidated = T
506
609
  },
507
610
  options,
508
611
  );
509
- } catch (error) {
510
- firstError = error;
511
- lastError = error;
512
- if (isNonRetryable(error, options.signal)) throw error;
612
+ } catch (caught) {
613
+ const failure = caught as FailedAttempt;
614
+ failedAttempts.push(failure);
615
+ firstError = failure.error;
616
+ lastError = failure.error;
617
+ if (isNonRetryable(failure.error, options.signal)) {
618
+ throwWithPromptFailure(failure.error, failedAttempts, args, timeoutMs);
619
+ }
513
620
 
514
621
  if (fallbacks.length === 0) {
515
- throw error;
622
+ throwWithPromptFailure(failure.error, failedAttempts, args, timeoutMs);
516
623
  }
517
624
 
518
625
  log(
519
- `[${callContext}] primary (${explicitPrimaryLabel}) failed validation/prompt: ${shortErr(error)}; trying ${fallbacks.length} fallback(s)`,
626
+ `[${callContext}] primary (${explicitPrimaryLabel}) failed validation/prompt: ${shortErr(failure.error)}; trying ${fallbacks.length} fallback(s)`,
520
627
  );
521
628
  }
522
629
 
@@ -557,22 +664,30 @@ export async function promptSyncWithValidatedOutputRetry<TOutput, TValidated = T
557
664
  `[${callContext}] fallback succeeded with ${label} (attempt ${i + 2}/${fallbacks.length + 1})`,
558
665
  );
559
666
  return result;
560
- } catch (error) {
561
- if (firstError === null) firstError = error;
562
- lastError = error;
563
- if (isNonRetryable(error, options.signal)) throw error;
667
+ } catch (caught) {
668
+ const failure = caught as FailedAttempt;
669
+ failedAttempts.push(failure);
670
+ lastError = failure.error;
671
+ if (isNonRetryable(failure.error, options.signal)) {
672
+ throwWithPromptFailure(failure.error, failedAttempts, args, timeoutMs);
673
+ }
564
674
 
565
675
  const remaining = fallbacks.length - i - 1;
566
676
  if (remaining > 0) {
567
677
  log(
568
- `[${callContext}] ${label} failed validation/prompt: ${shortErr(error)}; ${remaining} fallback(s) left`,
678
+ `[${callContext}] ${label} failed validation/prompt: ${shortErr(failure.error)}; ${remaining} fallback(s) left`,
569
679
  );
570
680
  }
571
681
  }
572
682
  }
573
683
 
574
684
  log(
575
- `[${callContext}] all models exhausted; tried: ${[explicitPrimaryLabel, ...fallbacks.map((fallback) => toModelEntry(fallback)?.model ?? String(fallback))].join(", ")}; original error: ${shortErr(firstError)}; last error: ${shortErr(lastError)}`,
685
+ `[${callContext}] all models exhausted; tried: ${failedAttempts.map((failure) => failure.attempt.label).join(", ")}; original error: ${shortErr(firstError)}; last error: ${shortErr(lastError)}`,
686
+ );
687
+ throwWithPromptFailure(
688
+ firstError ?? lastError ?? new Error("All fallback models failed validation"),
689
+ failedAttempts,
690
+ args,
691
+ timeoutMs,
576
692
  );
577
- throw firstError ?? lastError ?? new Error("All fallback models failed validation");
578
693
  }
@@ -0,0 +1,20 @@
1
+ export type PiExecutableHarnessKind = "pi" | "omp";
2
+
3
+ /** Executable images recognized as Pi-compatible hosts. */
4
+ export const PI_IMAGE_NAMES = new Set(["pi", "pi.cmd", "omp", "oh-my-pi"]);
5
+
6
+ /** Classify a process title or executable path using the shared Pi image vocabulary. */
7
+ export function piHarnessKindFromExecutable(
8
+ value: string | undefined,
9
+ ): PiExecutableHarnessKind | undefined {
10
+ const executable = (value ?? "")
11
+ .trim()
12
+ .replace(/^['"]|['"]$/g, "")
13
+ .replaceAll("\\", "/")
14
+ .split("/")
15
+ .at(-1)
16
+ ?.toLowerCase()
17
+ .replace(/\.(?:exe|cmd)$/, "");
18
+ if (!executable || !PI_IMAGE_NAMES.has(executable)) return undefined;
19
+ return executable === "pi" ? "pi" : "omp";
20
+ }
@@ -2,7 +2,6 @@ import { existsSync, readFileSync, statSync } from "node:fs";
2
2
  import { dirname, isAbsolute, resolve } from "node:path";
3
3
 
4
4
  import {
5
- type ConfigHarness,
6
5
  cortexKitUserConfigBasePath,
7
6
  resolveLegacyConfigSourcesForHarness,
8
7
  } from "../config/migrate-config-location";
@@ -13,6 +12,7 @@ import {
13
12
  CTX_REDUCE_LIGHT_DESCRIPTION,
14
13
  CTX_SEARCH_LIGHT_DESCRIPTION,
15
14
  } from "../tools/light-descriptions";
15
+ import type { HarnessId } from "./harness";
16
16
  import { piModelRefToCanonical } from "./harness-provider-map";
17
17
  import { detectConfigFile } from "./jsonc-parser";
18
18
  import {
@@ -88,7 +88,7 @@ export interface PromptSurfaceRuntime {
88
88
  }
89
89
 
90
90
  export interface CreatePromptSurfaceRuntimeOptions {
91
- harness?: ConfigHarness;
91
+ harness?: HarnessId;
92
92
  directory?: string;
93
93
  /** Explicit test/integration seam; production derives the USER config directory. */
94
94
  userConfigDirectory?: string;
@@ -105,7 +105,7 @@ function resolveUserConfigDirectory(options: CreatePromptSurfaceRuntimeOptions):
105
105
  if (options.harness) {
106
106
  const legacy = resolveLegacyConfigSourcesForHarness(
107
107
  options.directory ?? process.cwd(),
108
- options.harness,
108
+ options.harness === "omp" ? "pi" : options.harness,
109
109
  ).user.find((source) => existsSync(source.path));
110
110
  if (legacy) return dirname(legacy.path);
111
111
  }
@@ -7,6 +7,7 @@ import type {
7
7
  DreamTaskBacklogMap,
8
8
  DreamTaskProgress,
9
9
  } from "../features/magic-context/dreamer/task-registry";
10
+ import type { ConfigParseFailure } from "./config-diagnostics";
10
11
  import type { LoggerDiagnostics } from "./logger";
11
12
 
12
13
  export interface TailHygieneStatus {
@@ -181,6 +182,11 @@ export interface StatusDetail extends SidebarSnapshot {
181
182
  * (only meaningful when lastResponseTime > 0); N = live countdown. */
182
183
  cacheRemainingMs: number;
183
184
  cacheExpired: boolean;
185
+ /** Reports whether the displayed TTL came from config, persisted session metadata,
186
+ * or the default; cache scheduling still uses the TTL stored in session metadata. */
187
+ cacheTtlSource?: "config" | "session" | "default";
188
+ cacheTtlModelKey?: string;
189
+ configParseFailures?: ConfigParseFailure[];
184
190
  /** True when cacheTtl is "never" — the idle-TTL heuristic is disabled on
185
191
  * this lane. Redundant with cacheTtlMs === -1; kept as the readable form. */
186
192
  cacheNeverExpires?: boolean;
@@ -4,6 +4,7 @@ import { readFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
 
6
6
  import { log } from "./logger";
7
+ import { PI_IMAGE_NAMES, piHarnessKindFromExecutable } from "./pi-executable";
7
8
 
8
9
  export type ProcessKind = "OpenCode server" | "OpenCode instance (TUI/CLI)" | "Pi" | "process";
9
10
 
@@ -102,8 +103,6 @@ const PI_HARNESS_ARC_MARKERS = [
102
103
  "cljs/dist",
103
104
  "dist/bundle/cli",
104
105
  ];
105
- const PI_IMAGE_NAMES = new Set(["pi", "pi.cmd", "omp", "oh-my-pi"]);
106
-
107
106
  let rpcIdentityReadFileSync: typeof readFileSync = readFileSync;
108
107
  let rpcIdentityExecFileSync: typeof execFileSync = execFileSync;
109
108
  let rpcIdentityProcessKill: typeof process.kill = process.kill;
@@ -346,7 +345,7 @@ function commandHasOpenCodeExecutable(tokens: readonly string[]): number {
346
345
  function commandHasPiExecutable(tokens: readonly string[]): boolean {
347
346
  for (let index = 0; index < tokens.length; index += 1) {
348
347
  const executable = executableName(tokens[index]).replace(/\.(?:exe|cmd)$/, "");
349
- if (["pi", "omp", "oh-my-pi"].includes(executable)) return true;
348
+ if (piHarnessKindFromExecutable(executable) !== undefined) return true;
350
349
  if (["node", "bun", "deno"].includes(executable)) {
351
350
  const script = executableName(tokens[index + 1]).replace(/\.(?:exe|cmd)$/, "");
352
351
  if (
@@ -18,6 +18,7 @@ const STATUS_FIXTURE: StatusDetail = {
18
18
  sessionNoteCount: 1,
19
19
  readySmartNoteCount: 2,
20
20
  cacheTtl: "5m",
21
+ cacheTtlSource: "session",
21
22
  lastTransformError: null,
22
23
  lastDreamerRunAt: null,
23
24
  projectIdentity: "/repo",
@@ -73,7 +74,7 @@ describe("formatStatusDetailMarkdown", () => {
73
74
  - **Mode:** Magic Context compaction
74
75
  - **Active profile:** work
75
76
  - **Usage:** 75.0% (96,000 / 128,000 usable tokens)
76
- - **Cache lane:** live (42s remaining); TTL 5m
77
+ - **Cache TTL: 5m (session)**; live (42s remaining); TTL 5m
77
78
  - **Historian:** running; boundary present; coverage 12
78
79
  - **Memory:** 8 active; 3 injected
79
80
  - **Tags:** 4 active, 1 dropped; 2 pending drops
@@ -1,3 +1,5 @@
1
+ import { formatCacheTtlDisplay } from "./cache-ttl-display";
2
+ import { formatConfigParseStatusLine } from "./config-diagnostics";
1
3
  import type { StatusDetail } from "./rpc-types";
2
4
  import { RUST_MODE_HOST_PATHS_LINE } from "./rust-mode-status";
3
5
 
@@ -33,12 +35,14 @@ export function formatStatusDetailMarkdown(detail: StatusDetail): string {
33
35
  : "Magic Context compaction";
34
36
 
35
37
  const lines = [
38
+ ...(detail.configParseFailures ?? []).map(formatConfigParseStatusLine),
39
+ ...((detail.configParseFailures?.length ?? 0) > 0 ? [""] : []),
36
40
  "## Magic Context Status",
37
41
  "",
38
42
  `- **Mode:** ${mode}`,
39
43
  `- **Active profile:** ${detail.activeProfile ?? "none"}`,
40
44
  `- **Usage:** ${detail.usagePercentage.toFixed(1)}% (${formatCount(detail.inputTokens)} / ${usableLimit})`,
41
- `- **Cache lane:** ${formatCacheLane(detail)}`,
45
+ `- **${formatCacheTtlDisplay({ value: detail.cacheTtl, source: detail.cacheTtlSource ?? "session", modelKey: detail.cacheTtlModelKey })}**; ${formatCacheLane(detail)}`,
42
46
  `- **Historian:** ${[historianState, ...historianDetails].join("; ")}`,
43
47
  ...(detail.hostBackendsModuleSide ? [`- ${RUST_MODE_HOST_PATHS_LINE}`] : []),
44
48
  `- **Memory:** ${formatCount(detail.memoryCount)} active; ${formatCount(detail.memoryBlockCount)} injected`,
@@ -62,6 +62,7 @@ import {
62
62
  byteSize,
63
63
  prependTag,
64
64
  stripTagPrefix,
65
+ stripWellFormedLeadingTagPrefix,
65
66
  } from "../hooks/magic-context/tag-content-primitives";
66
67
  import type { TagTarget } from "../hooks/magic-context/tag-messages";
67
68
  import type { Transcript, TranscriptPart } from "./transcript";
@@ -183,6 +184,8 @@ interface ToolAggregate {
183
184
  inputByteSize: number;
184
185
  /** Persisted input-token count, or null for a legacy/unobserved invocation. */
185
186
  inputTokenCount: number | null;
187
+ /** At least one assistant-side occurrence shares its message with native reasoning. */
188
+ requiresToolArcSkeleton: boolean;
186
189
  }
187
190
 
188
191
  function textIdentityDigest(value: string): string {
@@ -243,6 +246,8 @@ export function tagTranscript(
243
246
  let textOrdinal = 0;
244
247
  let toolResultOrdinal = 0;
245
248
  const parts = message.parts;
249
+ const messageHasNativeReasoning =
250
+ message.info.role === "assistant" && parts.some((part) => part.kind === "thinking");
246
251
  const contentDerivedTextIds =
247
252
  messageId !== undefined && options.textIdentityDriftMessageIds?.has(messageId) === true
248
253
  ? buildContentDerivedTextIds(messageId, parts)
@@ -344,6 +349,7 @@ export function tagTranscript(
344
349
  const existing = toolAggregates.get(aggregateKey);
345
350
  if (existing) {
346
351
  existing.occurrences.push({ message, part, kind: part.kind });
352
+ existing.requiresToolArcSkeleton ||= messageHasNativeReasoning;
347
353
  const canReuseIdentity = reuseIdentity && existing.identityReusable;
348
354
  let text = "";
349
355
  if (canReuseIdentity) {
@@ -462,6 +468,7 @@ export function tagTranscript(
462
468
  toolName: null,
463
469
  inputByteSize: reusableAccounting.inputByteSize,
464
470
  inputTokenCount: reusableAccounting.inputTokenCount,
471
+ requiresToolArcSkeleton: messageHasNativeReasoning,
465
472
  };
466
473
  if (part.kind === "tool_result") {
467
474
  text = part.getText() ?? "";
@@ -526,6 +533,7 @@ export function tagTranscript(
526
533
  inputTokenCount:
527
534
  persistedAccounting?.inputTokenCount ??
528
535
  (part.kind === "tool_use" ? firstInputTokenCount : null),
536
+ requiresToolArcSkeleton: messageHasNativeReasoning,
529
537
  };
530
538
  if (part.kind === "tool_result") {
531
539
  applyGrownToolResultAccounting({
@@ -677,7 +685,14 @@ function applyToolPrefixAndTarget(args: ApplyToolPrefixAndTargetArgs): void {
677
685
  if (args.timing) args.timing.prefix += performance.now() - prefixStart;
678
686
  }
679
687
  const targetStart = args.timing ? performance.now() : 0;
680
- args.targets.set(args.tagId, buildAggregateTarget(args.tagId, args.aggregate.occurrences));
688
+ args.targets.set(
689
+ args.tagId,
690
+ buildAggregateTarget(
691
+ args.tagId,
692
+ args.aggregate.occurrences,
693
+ args.aggregate.requiresToolArcSkeleton,
694
+ ),
695
+ );
681
696
  if (args.timing) args.timing.targets += performance.now() - targetStart;
682
697
  }
683
698
 
@@ -814,6 +829,16 @@ interface TagTextPartArgs {
814
829
  function tagTextPart(args: TagTextPartArgs): void {
815
830
  const identityStart = args.timing ? performance.now() : 0;
816
831
  const text = args.part.getText() ?? "";
832
+ // Pi and OMP can expose provider framing as assistant text blocks. Treat a
833
+ // previously prefixed blank as blank too, so MC never turns inert framing
834
+ // into a tag-bearing part that later strip logic could mistake for content.
835
+ if (
836
+ args.message.info.role === "assistant" &&
837
+ stripWellFormedLeadingTagPrefix(text).trim().length === 0
838
+ ) {
839
+ if (args.timing) args.timing.identity += performance.now() - identityStart;
840
+ return;
841
+ }
817
842
  const contentId = args.contentId;
818
843
  const reusableTagId = args.reuseIdentity
819
844
  ? args.tagger.getTag(args.sessionId, contentId, "message")
@@ -999,7 +1024,11 @@ function setToolContentOrText(part: TranscriptPart, content: string): boolean {
999
1024
  *
1000
1025
  * Mirrors OpenCode's createToolDropTarget semantics in tool-drop-target.ts.
1001
1026
  */
1002
- function buildAggregateTarget(tagId: number, occurrences: ToolOccurrence[]): TagTarget {
1027
+ function buildAggregateTarget(
1028
+ tagId: number,
1029
+ occurrences: ToolOccurrence[],
1030
+ requiresToolArcSkeleton: boolean,
1031
+ ): TagTarget {
1003
1032
  const role = occurrences[0]?.message.info.role ?? "user";
1004
1033
  const messageId = occurrences[0]?.message.info.id;
1005
1034
 
@@ -1079,6 +1108,7 @@ function buildAggregateTarget(tagId: number, occurrences: ToolOccurrence[]): Tag
1079
1108
  canDrop(): boolean {
1080
1109
  return occurrences.length > 0;
1081
1110
  },
1111
+ requiresToolArcSkeleton,
1082
1112
  // Non-mutating read of the invocation input (the tool_use occurrence
1083
1113
  // carries the arguments). Used by smart-drops supersession selection.
1084
1114
  readInput(): Record<string, unknown> | null {
@@ -1,3 +1,5 @@
1
+ import type { HarnessId } from "./harness";
2
+
1
3
  /**
2
4
  * Harness-agnostic transcript interface.
3
5
  *
@@ -229,7 +231,7 @@ export interface Transcript {
229
231
  * compaction marker injection).
230
232
  * - Test assertions confirming the right adapter ran.
231
233
  */
232
- readonly harness: "opencode" | "pi";
234
+ readonly harness: HarnessId;
233
235
 
234
236
  /**
235
237
  * Commit accumulated mutations to the underlying source array.
package/src/tui/index.tsx CHANGED
@@ -10,6 +10,8 @@ import {
10
10
  import packageJson from "../../package.json"
11
11
  import { closeRpc, dismissUpgradeReminder, getAnnouncement, getCompartmentCount, getRpcGeneration, initRpcClient, loadEmbedDetail, loadStatusDetail, loadToastDurationMs, markAnnounced, requestRecomp, requestUpgrade, type EmbedDetail, type StatusDetail } from "./data/context-db"
12
12
  import { startNotificationSocket, stopNotificationSocket, type SocketNotification } from "./data/notification-socket"
13
+ import { formatCacheTtlDisplay } from "../shared/cache-ttl-display"
14
+ import { formatConfigParseStatusLine } from "../shared/config-diagnostics"
13
15
  import { formatThresholdPercent } from "../shared/format-threshold"
14
16
  import { formatTailHygiene } from "../shared/tail-hygiene-status"
15
17
  import { RUST_MODE_HOST_PATHS_LINE } from "../shared/rust-mode-status"
@@ -228,6 +230,10 @@ const StatusDialog = (props: { api: TuiPluginApi; s: StatusDetail }) => {
228
230
  <text fg={t().textMuted}>v{packageJson.version}</text>
229
231
  </box>
230
232
 
233
+ {s().configParseFailures.map((failure) => (
234
+ <text fg={t().error}>{formatConfigParseStatusLine(failure)}</text>
235
+ ))}
236
+
231
237
  <box flexDirection="row" justifyContent="space-between" width="100%">
232
238
  {compactionOff() ? (
233
239
  <text fg={t().accent}>
@@ -378,7 +384,15 @@ const StatusDialog = (props: { api: TuiPluginApi; s: StatusDetail }) => {
378
384
  <box marginTop={1}>
379
385
  <text fg={t().text}><b>Cache TTL</b></text>
380
386
  </box>
381
- <R t={t()} l="Configured" v={s().cacheTtl} />
387
+ <R
388
+ t={t()}
389
+ l="Configured"
390
+ v={formatCacheTtlDisplay({
391
+ value: s().cacheTtl,
392
+ source: s().cacheTtlSource,
393
+ modelKey: s().cacheTtlModelKey,
394
+ }).replace(/^Cache TTL: /, "")}
395
+ />
382
396
  <R t={t()} l="Last response" v={s().lastResponseTime > 0 ? `${Math.round(elapsed() / 1000)}s ago` : "never"} />
383
397
  <R t={t()} l="Remaining" v={s().cacheExpired ? "expired" : s().cacheNeverExpires ? "never (MC never assumes expiry — external cache-keep)" : `${Math.round(s().cacheRemainingMs / 1000)}s`} fg={s().cacheExpired ? t().warning : t().textMuted} />
384
398
  <R t={t()} l="Auto-execute" v={s().cacheExpired ? "yes (expired)" : s().cacheNeverExpires ? `at ≥${formatThresholdPercent(s().executeThreshold)}%` : `at TTL or ≥${formatThresholdPercent(s().executeThreshold)}%`} fg={t().textMuted} />