@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.
- package/dist/config/index.d.ts +9 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/{conflict-warning-hook-z5sd697e.js → conflict-warning-hook-ejkc9sjn.js} +2 -2
- package/dist/{derive-budgets-901a0pk0.js → derive-budgets-bc366cz8.js} +4 -2
- package/dist/features/magic-context/dreamer/classify.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts +13 -0
- package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-scheduler.d.ts +3 -0
- package/dist/features/magic-context/dreamer/task-scheduler.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-persisted.d.ts +2 -2
- package/dist/hooks/magic-context/command-handler.d.ts +5 -1
- package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-types.d.ts +4 -0
- package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
- package/dist/hooks/magic-context/derive-budgets.d.ts +1 -0
- package/dist/hooks/magic-context/derive-budgets.d.ts.map +1 -1
- package/dist/hooks/magic-context/emergency-drop.d.ts +3 -1
- package/dist/hooks/magic-context/emergency-drop.d.ts.map +1 -1
- package/dist/hooks/magic-context/execute-status.d.ts +7 -1
- package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
- package/dist/hooks/magic-context/heuristic-cleanup.d.ts +4 -3
- package/dist/hooks/magic-context/heuristic-cleanup.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook-handlers.d.ts +2 -0
- package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook.d.ts +5 -2
- package/dist/hooks/magic-context/hook.d.ts.map +1 -1
- package/dist/hooks/magic-context/issue-423-fixture.d.ts +37 -0
- package/dist/hooks/magic-context/issue-423-fixture.d.ts.map +1 -0
- package/dist/hooks/magic-context/issue-424-fixture.d.ts +11 -0
- package/dist/hooks/magic-context/issue-424-fixture.d.ts.map +1 -0
- package/dist/hooks/magic-context/producer-window-guard.d.ts +8 -0
- package/dist/hooks/magic-context/producer-window-guard.d.ts.map +1 -0
- package/dist/hooks/magic-context/protected-tail-boundary.d.ts +59 -0
- package/dist/hooks/magic-context/protected-tail-boundary.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-chunk.d.ts +2 -0
- package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts +4 -1
- package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts.map +1 -1
- package/dist/hooks/magic-context/recomp-orchestrator.d.ts +2 -0
- package/dist/hooks/magic-context/recomp-orchestrator.d.ts.map +1 -1
- package/dist/hooks/magic-context/rust-mode-transform.d.ts +5 -2
- package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/sentinel.d.ts +3 -3
- package/dist/hooks/magic-context/sentinel.d.ts.map +1 -1
- package/dist/hooks/magic-context/tag-messages.d.ts +2 -0
- package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
- package/dist/hooks/magic-context/tail-hygiene-walk.d.ts +8 -3
- package/dist/hooks/magic-context/tail-hygiene-walk.d.ts.map +1 -1
- package/dist/hooks/magic-context/tool-drop-target.d.ts +1 -0
- package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts +2 -0
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +4 -0
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/wrapup-orchestrator.d.ts.map +1 -1
- package/dist/{index-vmmjdpc0.js → index-0y2mcg5y.js} +282 -41
- package/dist/{index-npx8vhmy.js → index-4avmwfp5.js} +1 -1
- package/dist/{index-kby1sng5.js → index-avwt563y.js} +8 -1
- package/dist/{index-0bycg0np.js → index-fz104x1j.js} +1 -1
- package/dist/{index-dn1rwpd8.js → index-mjv9je4e.js} +14 -10
- package/dist/index-sd3egckz.js +1583 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +455 -1400
- package/dist/plugin/hooks/create-session-hooks.d.ts +3 -0
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/{recomp-orchestrator-f5m2wk74.js → recomp-orchestrator-q98r6rga.js} +4 -4
- package/dist/shared/cache-ttl-display.d.ts +23 -0
- package/dist/shared/cache-ttl-display.d.ts.map +1 -0
- package/dist/shared/cache-ttl-seed.d.ts +10 -0
- package/dist/shared/cache-ttl-seed.d.ts.map +1 -0
- package/dist/shared/config-diagnostics.d.ts +30 -0
- package/dist/shared/config-diagnostics.d.ts.map +1 -0
- package/dist/shared/config-warning-surface.d.ts +3 -0
- package/dist/shared/config-warning-surface.d.ts.map +1 -0
- package/dist/shared/data-path.d.ts +10 -9
- package/dist/shared/data-path.d.ts.map +1 -1
- package/dist/shared/harness-provider-map.d.ts +7 -0
- package/dist/shared/harness-provider-map.d.ts.map +1 -1
- package/dist/shared/jsonc-parser.d.ts +12 -0
- package/dist/shared/jsonc-parser.d.ts.map +1 -1
- package/dist/shared/model-suggestion-retry.d.ts +14 -10
- package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
- package/dist/shared/pi-executable.d.ts +6 -0
- package/dist/shared/pi-executable.d.ts.map +1 -0
- package/dist/shared/rpc-types.d.ts +6 -0
- package/dist/shared/rpc-types.d.ts.map +1 -1
- package/dist/shared/rpc-utils.d.ts.map +1 -1
- package/dist/shared/status-detail-text.d.ts.map +1 -1
- package/dist/shared/tag-transcript.d.ts.map +1 -1
- package/dist/{storage-meta-session-3rxx9ekm.js → storage-meta-session-dvwfw778.js} +1 -1
- package/package.json +1 -1
- package/src/config/index.test.ts +44 -0
- package/src/config/index.ts +96 -15
- package/src/shared/cache-ttl-display.test.ts +84 -0
- package/src/shared/cache-ttl-display.ts +64 -0
- package/src/shared/cache-ttl-seed.ts +23 -0
- package/src/shared/config-diagnostics.test.ts +29 -0
- package/src/shared/config-diagnostics.ts +64 -0
- package/src/shared/config-warning-surface.ts +28 -0
- package/src/shared/data-path.ts +10 -9
- package/src/shared/harness-provider-map.test.ts +16 -0
- package/src/shared/harness-provider-map.ts +15 -0
- package/src/shared/jsonc-parser.test.ts +14 -1
- package/src/shared/jsonc-parser.ts +93 -0
- package/src/shared/model-suggestion-retry.ts +143 -28
- package/src/shared/pi-executable.ts +20 -0
- package/src/shared/rpc-types.ts +6 -0
- package/src/shared/rpc-utils.ts +2 -3
- package/src/shared/status-detail-text.test.ts +2 -1
- package/src/shared/status-detail-text.ts +5 -1
- package/src/shared/tag-transcript.ts +21 -2
- package/src/tui/index.tsx +15 -1
- package/src/tui-compiled/index.tsx +234 -222
- 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
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
|
460
|
-
*
|
|
461
|
-
*
|
|
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 (
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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
|
-
|
|
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 (
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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: ${
|
|
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
|
+
}
|
package/src/shared/rpc-types.ts
CHANGED
|
@@ -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;
|
package/src/shared/rpc-utils.ts
CHANGED
|
@@ -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 (
|
|
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
|
|
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
|
-
`-
|
|
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(
|
|
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(
|
|
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
|
|
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} />
|