@cortexkit/opencode-magic-context 0.41.3 → 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 (119) hide show
  1. package/dist/config/index.d.ts +9 -0
  2. package/dist/config/index.d.ts.map +1 -1
  3. package/dist/{conflict-warning-hook-z5sd697e.js → conflict-warning-hook-ejkc9sjn.js} +2 -2
  4. package/dist/{derive-budgets-901a0pk0.js → derive-budgets-bc366cz8.js} +4 -2
  5. package/dist/features/magic-context/dreamer/classify.d.ts.map +1 -1
  6. package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts +13 -0
  7. package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts.map +1 -1
  8. package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
  9. package/dist/features/magic-context/dreamer/task-scheduler.d.ts +3 -0
  10. package/dist/features/magic-context/dreamer/task-scheduler.d.ts.map +1 -1
  11. package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
  12. package/dist/features/magic-context/storage-meta-persisted.d.ts +2 -2
  13. package/dist/hooks/magic-context/command-handler.d.ts +5 -1
  14. package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
  15. package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
  16. package/dist/hooks/magic-context/compartment-runner-types.d.ts +4 -0
  17. package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
  18. package/dist/hooks/magic-context/derive-budgets.d.ts +1 -0
  19. package/dist/hooks/magic-context/derive-budgets.d.ts.map +1 -1
  20. package/dist/hooks/magic-context/emergency-drop.d.ts +3 -1
  21. package/dist/hooks/magic-context/emergency-drop.d.ts.map +1 -1
  22. package/dist/hooks/magic-context/execute-status.d.ts +7 -1
  23. package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
  24. package/dist/hooks/magic-context/heuristic-cleanup.d.ts +4 -3
  25. package/dist/hooks/magic-context/heuristic-cleanup.d.ts.map +1 -1
  26. package/dist/hooks/magic-context/hook-handlers.d.ts +2 -0
  27. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  28. package/dist/hooks/magic-context/hook.d.ts +5 -2
  29. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  30. package/dist/hooks/magic-context/issue-423-fixture.d.ts +37 -0
  31. package/dist/hooks/magic-context/issue-423-fixture.d.ts.map +1 -0
  32. package/dist/hooks/magic-context/issue-424-fixture.d.ts +11 -0
  33. package/dist/hooks/magic-context/issue-424-fixture.d.ts.map +1 -0
  34. package/dist/hooks/magic-context/producer-window-guard.d.ts +8 -0
  35. package/dist/hooks/magic-context/producer-window-guard.d.ts.map +1 -0
  36. package/dist/hooks/magic-context/protected-tail-boundary.d.ts +59 -0
  37. package/dist/hooks/magic-context/protected-tail-boundary.d.ts.map +1 -1
  38. package/dist/hooks/magic-context/read-session-chunk.d.ts +2 -0
  39. package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
  40. package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts +4 -1
  41. package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts.map +1 -1
  42. package/dist/hooks/magic-context/recomp-orchestrator.d.ts +2 -0
  43. package/dist/hooks/magic-context/recomp-orchestrator.d.ts.map +1 -1
  44. package/dist/hooks/magic-context/rust-mode-transform.d.ts +5 -2
  45. package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
  46. package/dist/hooks/magic-context/sentinel.d.ts +3 -3
  47. package/dist/hooks/magic-context/sentinel.d.ts.map +1 -1
  48. package/dist/hooks/magic-context/tag-messages.d.ts +2 -0
  49. package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
  50. package/dist/hooks/magic-context/tail-hygiene-walk.d.ts +8 -3
  51. package/dist/hooks/magic-context/tail-hygiene-walk.d.ts.map +1 -1
  52. package/dist/hooks/magic-context/tool-drop-target.d.ts +1 -0
  53. package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
  54. package/dist/hooks/magic-context/transform-compartment-phase.d.ts +2 -0
  55. package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
  56. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  57. package/dist/hooks/magic-context/transform.d.ts +4 -0
  58. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  59. package/dist/hooks/magic-context/wrapup-orchestrator.d.ts.map +1 -1
  60. package/dist/{index-vmmjdpc0.js → index-0y2mcg5y.js} +282 -41
  61. package/dist/{index-npx8vhmy.js → index-4avmwfp5.js} +1 -1
  62. package/dist/{index-kby1sng5.js → index-avwt563y.js} +8 -1
  63. package/dist/{index-0bycg0np.js → index-fz104x1j.js} +1 -1
  64. package/dist/{index-dn1rwpd8.js → index-mjv9je4e.js} +14 -10
  65. package/dist/index-sd3egckz.js +1583 -0
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +455 -1400
  68. package/dist/plugin/hooks/create-session-hooks.d.ts +3 -0
  69. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  70. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  71. package/dist/{recomp-orchestrator-f5m2wk74.js → recomp-orchestrator-q98r6rga.js} +4 -4
  72. package/dist/shared/cache-ttl-display.d.ts +23 -0
  73. package/dist/shared/cache-ttl-display.d.ts.map +1 -0
  74. package/dist/shared/cache-ttl-seed.d.ts +10 -0
  75. package/dist/shared/cache-ttl-seed.d.ts.map +1 -0
  76. package/dist/shared/config-diagnostics.d.ts +30 -0
  77. package/dist/shared/config-diagnostics.d.ts.map +1 -0
  78. package/dist/shared/config-warning-surface.d.ts +3 -0
  79. package/dist/shared/config-warning-surface.d.ts.map +1 -0
  80. package/dist/shared/data-path.d.ts +10 -9
  81. package/dist/shared/data-path.d.ts.map +1 -1
  82. package/dist/shared/harness-provider-map.d.ts +7 -0
  83. package/dist/shared/harness-provider-map.d.ts.map +1 -1
  84. package/dist/shared/jsonc-parser.d.ts +12 -0
  85. package/dist/shared/jsonc-parser.d.ts.map +1 -1
  86. package/dist/shared/model-suggestion-retry.d.ts +14 -10
  87. package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
  88. package/dist/shared/pi-executable.d.ts +6 -0
  89. package/dist/shared/pi-executable.d.ts.map +1 -0
  90. package/dist/shared/rpc-types.d.ts +6 -0
  91. package/dist/shared/rpc-types.d.ts.map +1 -1
  92. package/dist/shared/rpc-utils.d.ts.map +1 -1
  93. package/dist/shared/status-detail-text.d.ts.map +1 -1
  94. package/dist/shared/tag-transcript.d.ts.map +1 -1
  95. package/dist/{storage-meta-session-3rxx9ekm.js → storage-meta-session-dvwfw778.js} +1 -1
  96. package/package.json +1 -1
  97. package/src/config/index.test.ts +44 -0
  98. package/src/config/index.ts +96 -15
  99. package/src/shared/cache-ttl-display.test.ts +84 -0
  100. package/src/shared/cache-ttl-display.ts +64 -0
  101. package/src/shared/cache-ttl-seed.ts +23 -0
  102. package/src/shared/config-diagnostics.test.ts +29 -0
  103. package/src/shared/config-diagnostics.ts +64 -0
  104. package/src/shared/config-warning-surface.ts +28 -0
  105. package/src/shared/data-path.ts +10 -9
  106. package/src/shared/harness-provider-map.test.ts +16 -0
  107. package/src/shared/harness-provider-map.ts +15 -0
  108. package/src/shared/jsonc-parser.test.ts +14 -1
  109. package/src/shared/jsonc-parser.ts +93 -0
  110. package/src/shared/model-suggestion-retry.ts +143 -28
  111. package/src/shared/pi-executable.ts +20 -0
  112. package/src/shared/rpc-types.ts +6 -0
  113. package/src/shared/rpc-utils.ts +2 -3
  114. package/src/shared/status-detail-text.test.ts +2 -1
  115. package/src/shared/status-detail-text.ts +5 -1
  116. package/src/shared/tag-transcript.ts +21 -2
  117. package/src/tui/index.tsx +15 -1
  118. package/src/tui-compiled/index.tsx +234 -222
  119. package/dist/index-jfpk81yt.js +0 -146
@@ -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 = {},
@@ -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
+ }
@@ -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`,
@@ -184,6 +184,8 @@ interface ToolAggregate {
184
184
  inputByteSize: number;
185
185
  /** Persisted input-token count, or null for a legacy/unobserved invocation. */
186
186
  inputTokenCount: number | null;
187
+ /** At least one assistant-side occurrence shares its message with native reasoning. */
188
+ requiresToolArcSkeleton: boolean;
187
189
  }
188
190
 
189
191
  function textIdentityDigest(value: string): string {
@@ -244,6 +246,8 @@ export function tagTranscript(
244
246
  let textOrdinal = 0;
245
247
  let toolResultOrdinal = 0;
246
248
  const parts = message.parts;
249
+ const messageHasNativeReasoning =
250
+ message.info.role === "assistant" && parts.some((part) => part.kind === "thinking");
247
251
  const contentDerivedTextIds =
248
252
  messageId !== undefined && options.textIdentityDriftMessageIds?.has(messageId) === true
249
253
  ? buildContentDerivedTextIds(messageId, parts)
@@ -345,6 +349,7 @@ export function tagTranscript(
345
349
  const existing = toolAggregates.get(aggregateKey);
346
350
  if (existing) {
347
351
  existing.occurrences.push({ message, part, kind: part.kind });
352
+ existing.requiresToolArcSkeleton ||= messageHasNativeReasoning;
348
353
  const canReuseIdentity = reuseIdentity && existing.identityReusable;
349
354
  let text = "";
350
355
  if (canReuseIdentity) {
@@ -463,6 +468,7 @@ export function tagTranscript(
463
468
  toolName: null,
464
469
  inputByteSize: reusableAccounting.inputByteSize,
465
470
  inputTokenCount: reusableAccounting.inputTokenCount,
471
+ requiresToolArcSkeleton: messageHasNativeReasoning,
466
472
  };
467
473
  if (part.kind === "tool_result") {
468
474
  text = part.getText() ?? "";
@@ -527,6 +533,7 @@ export function tagTranscript(
527
533
  inputTokenCount:
528
534
  persistedAccounting?.inputTokenCount ??
529
535
  (part.kind === "tool_use" ? firstInputTokenCount : null),
536
+ requiresToolArcSkeleton: messageHasNativeReasoning,
530
537
  };
531
538
  if (part.kind === "tool_result") {
532
539
  applyGrownToolResultAccounting({
@@ -678,7 +685,14 @@ function applyToolPrefixAndTarget(args: ApplyToolPrefixAndTargetArgs): void {
678
685
  if (args.timing) args.timing.prefix += performance.now() - prefixStart;
679
686
  }
680
687
  const targetStart = args.timing ? performance.now() : 0;
681
- 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
+ );
682
696
  if (args.timing) args.timing.targets += performance.now() - targetStart;
683
697
  }
684
698
 
@@ -1010,7 +1024,11 @@ function setToolContentOrText(part: TranscriptPart, content: string): boolean {
1010
1024
  *
1011
1025
  * Mirrors OpenCode's createToolDropTarget semantics in tool-drop-target.ts.
1012
1026
  */
1013
- function buildAggregateTarget(tagId: number, occurrences: ToolOccurrence[]): TagTarget {
1027
+ function buildAggregateTarget(
1028
+ tagId: number,
1029
+ occurrences: ToolOccurrence[],
1030
+ requiresToolArcSkeleton: boolean,
1031
+ ): TagTarget {
1014
1032
  const role = occurrences[0]?.message.info.role ?? "user";
1015
1033
  const messageId = occurrences[0]?.message.info.id;
1016
1034
 
@@ -1090,6 +1108,7 @@ function buildAggregateTarget(tagId: number, occurrences: ToolOccurrence[]): Tag
1090
1108
  canDrop(): boolean {
1091
1109
  return occurrences.length > 0;
1092
1110
  },
1111
+ requiresToolArcSkeleton,
1093
1112
  // Non-mutating read of the invocation input (the tool_use occurrence
1094
1113
  // carries the arguments). Used by smart-drops supersession selection.
1095
1114
  readInput(): Record<string, unknown> | null {
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} />