@yansigit/opencodex 2.36.1-dev.20260829.48 → 2.36.1
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/gui/dist/assets/{ApiKeys-LOOFiZfv.js → ApiKeys-CsmNyf4I.js} +1 -1
- package/gui/dist/assets/{Claude-BwKzpXe3.js → Claude-B4sHsGTD.js} +1 -1
- package/gui/dist/assets/{CodexSet-B5qi9KeE.js → CodexSet-BuAQ4YQn.js} +1 -1
- package/gui/dist/assets/{FileIntegrationPage-B0sUef6W.js → FileIntegrationPage-BC65sEaS.js} +1 -1
- package/gui/dist/assets/{Grok-bsb4n-f4.js → Grok-DNJWlISf.js} +1 -1
- package/gui/dist/assets/{Integrations-CSMwFDvM.js → Integrations-vGJshFZK.js} +2 -2
- package/gui/dist/assets/{IntegrationsOverview-CTORdKJA.js → IntegrationsOverview-DYMkDFHE.js} +1 -1
- package/gui/dist/assets/{Logs-BWYbfELf.js → Logs-Coc7_RPP.js} +1 -1
- package/gui/dist/assets/{Models-CexEtdT1.js → Models-CKVT8Meb.js} +1 -1
- package/gui/dist/assets/{NumberStepper-BapSFQnW.js → NumberStepper-Dug4wR8n.js} +1 -1
- package/gui/dist/assets/{Providers-BwUFeAgA.js → Providers-DyaCHRWL.js} +1 -1
- package/gui/dist/assets/{RestoreDialog-CD8piq90.js → RestoreDialog-BpuvVU2w.js} +1 -1
- package/gui/dist/assets/{Startup-BgX731C2.js → Startup-DAUQqNEN.js} +1 -1
- package/gui/dist/assets/{Storage-Bgq7HphP.js → Storage-DfP95SlJ.js} +1 -1
- package/gui/dist/assets/{Subagents-ChxItbeF.js → Subagents-CMNsZLfk.js} +1 -1
- package/gui/dist/assets/{Usage-BkylzP50.js → Usage-ESFRfBMO.js} +1 -1
- package/gui/dist/assets/{codex-stale-banner-D4gvSWnO.js → codex-stale-banner-W4FhikN7.js} +1 -1
- package/gui/dist/assets/{data-surface-BtO3lwam.js → data-surface-B8PX8rjm.js} +1 -1
- package/gui/dist/assets/{data-surface-pCXChiBf.js → data-surface-CETFXqiA.js} +1 -1
- package/gui/dist/assets/{index-CNopOid3.js → index-DHQHBRhb.js} +3 -3
- package/gui/dist/assets/{model-display-CiUpg8T9.js → model-display-OBTJhQwT.js} +1 -1
- package/gui/dist/assets/{provider-payload-b2jlS-On.js → provider-payload-CJRJUn7w.js} +1 -1
- package/gui/dist/assets/{section-tabs-BP7gEPK6.js → section-tabs-B8sLMiz6.js} +1 -1
- package/gui/dist/assets/shared-DI18-uF7.js +69 -0
- package/gui/dist/index.html +2 -2
- package/package.json +3 -1
- package/src/adapters/base.ts +26 -0
- package/src/adapters/cursor/catalog.ts +541 -0
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +34 -41
- package/src/adapters/cursor/envelope-echo.ts +128 -0
- package/src/adapters/cursor/request-builder.ts +19 -12
- package/src/adapters/cursor/tool-definitions.ts +2 -1
- package/src/adapters/cursor/tool-result-normalize.ts +23 -31
- package/src/adapters/cursor.ts +21 -2
- package/src/adapters/exec-tool-result-normalize.ts +99 -0
- package/src/adapters/google-antigravity-replay.ts +71 -2
- package/src/adapters/google.ts +19 -4
- package/src/adapters/kiro-constants.ts +12 -0
- package/src/adapters/kiro.ts +128 -11
- package/src/adapters/openai-chat.ts +16 -2
- package/src/adapters/openai-responses.ts +15 -2
- package/src/adapters/tool-catalog-nudge.ts +2 -1
- package/src/adapters/xai-web-search.ts +10 -14
- package/src/claude/outbound.ts +14 -3
- package/src/cli/access.ts +46 -3
- package/src/cli/account-api.ts +93 -16
- package/src/cli/account-extended.ts +262 -36
- package/src/cli/account-main.ts +12 -12
- package/src/cli/account.ts +40 -10
- package/src/cli/agent.ts +8 -1
- package/src/cli/capabilities-command.ts +94 -0
- package/src/cli/capabilities.ts +535 -0
- package/src/cli/claude-desktop.ts +31 -11
- package/src/cli/dispatch.ts +195 -27
- package/src/cli/doctor.ts +100 -1
- package/src/cli/help.ts +11 -2
- package/src/cli/index.ts +19 -3
- package/src/cli/inspect.ts +230 -0
- package/src/cli/observe.ts +11 -3
- package/src/cli/registry.ts +34 -2
- package/src/cli/runtime-api.ts +51 -7
- package/src/cli/status.ts +16 -0
- package/src/cli/storage.ts +234 -0
- package/src/cli/system-command.ts +16 -0
- package/src/cli/usage-report.ts +52 -2
- package/src/cli/version-skew.ts +46 -0
- package/src/codex/account-label.ts +21 -0
- package/src/codex/catalog/provider-fetch.ts +4 -0
- package/src/codex/transition-state.ts +12 -3
- package/src/compatibility/openai-responses.ts +9 -1
- package/src/generated/compatibility-version.json +94 -58
- package/src/integrations/ownership-policy.ts +24 -5
- package/src/integrations/ownership.ts +36 -2
- package/src/integrations/state.ts +40 -7
- package/src/integrations/writer.ts +21 -3
- package/src/lib/admin-secrets.ts +24 -0
- package/src/lib/errors.ts +25 -1
- package/src/lib/service-secrets.ts +15 -0
- package/src/oauth/store.ts +16 -6
- package/src/providers/label.ts +34 -1
- package/src/responses/turn-termination.ts +107 -0
- package/src/server/management/logs-usage-routes.ts +5 -27
- package/src/server/management/route-registry.ts +317 -0
- package/src/server/proxy-liveness.ts +27 -4
- package/src/server/request-log.ts +37 -17
- package/src/server/responses/core.ts +80 -4
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/service.ts +34 -0
- package/src/storage/policy-job.ts +14 -4
- package/src/storage/policy.ts +79 -33
- package/src/usage/log.ts +32 -6
- package/src/usage/summary.ts +22 -34
- package/gui/dist/assets/shared-BdYGV-yJ.js +0 -69
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
cursorWireModelIdWithEffort,
|
|
6
6
|
CURSOR_THINKING_MODEL_IDS,
|
|
7
7
|
} from "./effort-map";
|
|
8
|
+
import { parseCursorVariantId } from "./catalog";
|
|
8
9
|
|
|
9
10
|
export interface CursorModelInfo {
|
|
10
11
|
id: string;
|
|
@@ -76,9 +77,16 @@ function stripCursorWirePrefix(id: string): string {
|
|
|
76
77
|
* ordinary `{base}-{effort}` form, or Cursor's current `{base-without-fast}-{effort}-fast` form.
|
|
77
78
|
*/
|
|
78
79
|
export function isCursorModelAvailableForAccount(modelId: string, liveIds: readonly string[]): boolean {
|
|
80
|
+
// Umbrella matching (devlog 260828_cursor_umbrella_catalog): a live suffix
|
|
81
|
+
// id counts toward its BASE — any variant dimension (thinking/fast/effort)
|
|
82
|
+
// proves the account can reach the umbrella. Unknown ids fall back to the
|
|
83
|
+
// legacy exact/suffix comparison so non-cataloged rows keep matching.
|
|
84
|
+
const parsedTarget = parseCursorVariantId(modelId);
|
|
79
85
|
return liveIds.some(raw => {
|
|
80
86
|
const id = stripCursorWirePrefix(raw);
|
|
81
87
|
if (id === modelId) return true;
|
|
88
|
+
const parsedLive = parseCursorVariantId(id);
|
|
89
|
+
if (parsedLive.known && parsedTarget.known && parsedLive.baseId === parsedTarget.baseId) return true;
|
|
82
90
|
for (const effort of CANONICAL_EFFORT_SUFFIXES) {
|
|
83
91
|
if (
|
|
84
92
|
id === `${modelId}-${effort}` ||
|
|
@@ -244,15 +252,13 @@ export function filterCursorConfiguredModelsByLiveDiscovery<T extends { id: stri
|
|
|
244
252
|
|
|
245
253
|
/**
|
|
246
254
|
* Models GetUsableModels advertises but whose every Run returns not_found (catalog honesty,
|
|
247
|
-
* devlog 260826_cursor_responses_gap 060).
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
255
|
+
* devlog 260826_cursor_responses_gap 060). The claude-opus-5 REGULAR wire family is the known
|
|
256
|
+
* case (probes 2026-08-26: 100% not_found while -fast/-thinking succeed) — under the umbrella
|
|
257
|
+
* catalog (devlog 260828) that quarantine moved to the RESOLVER level: the capability marks the
|
|
258
|
+
* regular VARIANT quarantined, the bare slug routes the healthy thinking variant, and the base
|
|
259
|
+
* row stays in the seed. This row-level set stays for future whole-base quarantines.
|
|
252
260
|
*/
|
|
253
|
-
export const CURSOR_KNOWN_UNCALLABLE_MODEL_IDS: ReadonlySet<string> = new Set([
|
|
254
|
-
"claude-opus-5",
|
|
255
|
-
]);
|
|
261
|
+
export const CURSOR_KNOWN_UNCALLABLE_MODEL_IDS: ReadonlySet<string> = new Set([]);
|
|
256
262
|
|
|
257
263
|
export const CURSOR_STATIC_MODELS: readonly CursorModelInfo[] = normalizeCursorModels([
|
|
258
264
|
// Context windows and the model lineup mirror Cursor's public models/pricing docs plus the jawcode
|
|
@@ -265,26 +271,26 @@ export const CURSOR_STATIC_MODELS: readonly CursorModelInfo[] = normalizeCursorM
|
|
|
265
271
|
// gemini/grok/kimi-k2.7/gpt-5-mini are reasoning models in the SOT but are sent bare (no tier picker).
|
|
266
272
|
...CURSOR_ROUTER_MODEL_IDS.map(id => ({ id, contextWindow: CONTEXT_200K, supportsReasoningEffort: false })),
|
|
267
273
|
|
|
268
|
-
|
|
274
|
+
// Umbrella seed (devlog 260828_cursor_umbrella_catalog): one row per BASE
|
|
275
|
+
// model. Thinking merges into the base (the resolver routes the thinking
|
|
276
|
+
// variant); fast / thinking-fast / -1m stay routable as aliases but add no
|
|
277
|
+
// rows. Windows follow CURSOR_CAPABILITIES where the base is cataloged.
|
|
278
|
+
{ id: "claude-sonnet-5", contextWindow: CONTEXT_1M, supportsReasoningEffort: true },
|
|
269
279
|
{ id: "claude-4-sonnet", contextWindow: CONTEXT_200K },
|
|
270
280
|
{ id: "claude-4-sonnet-1m", contextWindow: CONTEXT_1M },
|
|
271
281
|
{ id: "claude-4.5-haiku", contextWindow: CONTEXT_200K },
|
|
272
282
|
{ id: "claude-4.5-sonnet", contextWindow: CONTEXT_200K },
|
|
273
283
|
{ id: "claude-4.5-opus", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
274
|
-
{ id: "claude-4.6-opus", contextWindow:
|
|
275
|
-
{ id: "claude-4.6-sonnet", contextWindow:
|
|
276
|
-
{ id: "claude-opus-4-7", contextWindow:
|
|
277
|
-
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
{ id: "claude-opus-
|
|
283
|
-
{ id: "claude-
|
|
284
|
-
// claude-opus-5 (bare) removed from the seed: GetUsableModels lists it but every Run returns
|
|
285
|
-
// not_found (quarantined via CURSOR_KNOWN_UNCALLABLE_MODEL_IDS; -fast/-thinking families stay).
|
|
286
|
-
{ id: "claude-opus-5-fast", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
287
|
-
{ id: "claude-fable-5", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
284
|
+
{ id: "claude-4.6-opus", contextWindow: CONTEXT_1M, supportsReasoningEffort: true },
|
|
285
|
+
{ id: "claude-4.6-sonnet", contextWindow: CONTEXT_1M, supportsReasoningEffort: true },
|
|
286
|
+
{ id: "claude-opus-4-7", contextWindow: CONTEXT_1M, supportsReasoningEffort: true },
|
|
287
|
+
{ id: "claude-opus-4-8", contextWindow: CONTEXT_1M, supportsReasoningEffort: true },
|
|
288
|
+
// claude-opus-5: regular variant is quarantined (not_found on every Run) but
|
|
289
|
+
// the umbrella row routes the THINKING variant, which is live — so the base
|
|
290
|
+
// row returns to the seed under the umbrella (resolver never sends the
|
|
291
|
+
// quarantined regular wire id for the bare slug).
|
|
292
|
+
{ id: "claude-opus-5", contextWindow: CONTEXT_1M, supportsReasoningEffort: true },
|
|
293
|
+
{ id: "claude-fable-5", contextWindow: CONTEXT_1M, supportsReasoningEffort: true },
|
|
288
294
|
|
|
289
295
|
{ id: "composer-1", contextWindow: CONTEXT_200K },
|
|
290
296
|
{ id: "composer-2.5", contextWindow: CONTEXT_200K },
|
|
@@ -301,17 +307,6 @@ export const CURSOR_STATIC_MODELS: readonly CursorModelInfo[] = normalizeCursorM
|
|
|
301
307
|
{ id: "gemini-3.6-flash", contextWindow: CONTEXT_GEMINI, supportsReasoningEffort: true },
|
|
302
308
|
{ id: "gemini-3.7-flash", contextWindow: CONTEXT_GEMINI, supportsReasoningEffort: true },
|
|
303
309
|
|
|
304
|
-
// Explicit-thinking variants (260825 live roster). Exposed as first-class ids the same way the
|
|
305
|
-
// Opus Fast families were in 831810c13: `isCursorModelAvailableForAccount` matches a base id
|
|
306
|
-
// against `{base}`, `{base}-{effort}` and the family's wire form, and none of those ever
|
|
307
|
-
// matched a `-thinking` id, so every one of these was invisible in the routed catalog.
|
|
308
|
-
// Suffix ORDER differs per family; `cursorWireModelIdWithEffort` owns that mapping.
|
|
309
|
-
...CURSOR_THINKING_MODEL_IDS.map(id => ({
|
|
310
|
-
id,
|
|
311
|
-
contextWindow: CONTEXT_200K,
|
|
312
|
-
supportsReasoningEffort: cursorModelHasEffortTiers(id),
|
|
313
|
-
})),
|
|
314
|
-
|
|
315
310
|
{ id: "gpt-5-codex", contextWindow: CONTEXT_272K },
|
|
316
311
|
{ id: "gpt-5-fast", contextWindow: CONTEXT_272K },
|
|
317
312
|
{ id: "gpt-5-mini", contextWindow: CONTEXT_272K },
|
|
@@ -344,17 +339,15 @@ export const CURSOR_STATIC_MODELS: readonly CursorModelInfo[] = normalizeCursorM
|
|
|
344
339
|
{ id: "kimi-k2.7-code", contextWindow: CONTEXT_262K },
|
|
345
340
|
// kimi-k3: cursor.com/docs/models/kimi-k3; account-verified via GetUsableModels (2026-07-28) —
|
|
346
341
|
// ships only as effort-suffixed kimi-k3-{low,high,max}, so the tier picker is exposed.
|
|
347
|
-
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
//
|
|
351
|
-
{ id: "kimi-k3
|
|
342
|
+
// kimi-k3 folds the old synthetic kimi-k3-1m row into the umbrella: the base
|
|
343
|
+
// is maxModeVerified (user-verified 1M on the Ultra plan, devlog 260826/025),
|
|
344
|
+
// so the ultra effort rung arms Max Mode on the wire and the separate picker
|
|
345
|
+
// row is gone. cursor/kimi-k3-1m stays routable as an alias.
|
|
346
|
+
{ id: "kimi-k3", contextWindow: CONTEXT_1M, supportsReasoningEffort: true },
|
|
352
347
|
|
|
353
348
|
{ id: "grok-4.5", contextWindow: 500_000, supportsReasoningEffort: true },
|
|
354
|
-
{ id: "grok-4.5-fast", contextWindow: 500_000, supportsReasoningEffort: true },
|
|
355
349
|
// 260813 preemptive: grok-4.6 seeded ahead of Cursor's lineup update (mirrors grok-4.5).
|
|
356
350
|
{ id: "grok-4.6", contextWindow: 500_000, supportsReasoningEffort: true },
|
|
357
|
-
{ id: "grok-4.6-fast", contextWindow: 500_000, supportsReasoningEffort: true },
|
|
358
351
|
]);
|
|
359
352
|
|
|
360
353
|
export function cursorModelIds(models: readonly CursorModelInfo[] = CURSOR_STATIC_MODELS): string[] {
|
|
@@ -14,6 +14,14 @@
|
|
|
14
14
|
|
|
15
15
|
const ECHO_MARKERS = ["[Tool Result]", "[Tool Error]", "[tool_result]"] as const;
|
|
16
16
|
const MAX_SNIFF_BYTES = 40;
|
|
17
|
+
/** Mid-stream observer: max leading whitespace on a line before matching disarms. */
|
|
18
|
+
const MAX_MIDSTREAM_LINE_INDENT = 128;
|
|
19
|
+
/** Mid-stream observer: post-marker window watched for call-id corruption. */
|
|
20
|
+
const MIDSTREAM_CORRUPTION_WINDOW = 512;
|
|
21
|
+
/** Mid-stream observer: cumulative scan cap (UTF-16 code units, checked between feeds). */
|
|
22
|
+
export const MAX_MIDSTREAM_SCAN_LENGTH = 512 * 1024;
|
|
23
|
+
/** Mid-stream observer: findings retained per turn. */
|
|
24
|
+
const MAX_MIDSTREAM_FINDINGS = 8;
|
|
17
25
|
const MAX_ROUTING_COMMENTARY_BYTES = 512;
|
|
18
26
|
/** Aggregate quarantine cap: past this, flush and disarm. */
|
|
19
27
|
const MAX_HOLD_BYTES = 8 * 1024;
|
|
@@ -43,6 +51,126 @@ export type EchoSnifferDecision =
|
|
|
43
51
|
| { kind: "flush" }
|
|
44
52
|
| { kind: "echo"; marker: string };
|
|
45
53
|
|
|
54
|
+
export interface MidstreamEchoFinding {
|
|
55
|
+
marker: string;
|
|
56
|
+
/** UTF-16 offset of the marker's line start within the turn's full text. */
|
|
57
|
+
offset: number;
|
|
58
|
+
callIdCorrupt: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Diagnostic-only mid-stream envelope-echo observer (devlog 260828 F1/F2).
|
|
63
|
+
*
|
|
64
|
+
* The prefix sniffer only watches the first ~40 bytes of a turn, but live
|
|
65
|
+
* probing caught grok-4.6 echoing "[Tool Result]" envelope blocks in the
|
|
66
|
+
* MIDDLE of an agent message — after legitimate leading text — one of them
|
|
67
|
+
* carrying a whitespace-spliced call-id ("fc_x mar-y" instead of "fc_x-y").
|
|
68
|
+
* Deltas at that point have already reached the client, so this observer
|
|
69
|
+
* never throws and never withholds output: it records findings so the
|
|
70
|
+
* adapter can emit a structured diagnostic at turn end. Only fixed marker
|
|
71
|
+
* enums, numeric offsets, and corruption booleans are retained — never
|
|
72
|
+
* content bytes.
|
|
73
|
+
*/
|
|
74
|
+
export class CursorMidstreamEchoObserver {
|
|
75
|
+
private lineBuffer = "";
|
|
76
|
+
private lineStartOffset = 0;
|
|
77
|
+
private totalLength = 0;
|
|
78
|
+
private disarmed = false;
|
|
79
|
+
private lineDisarmed = false;
|
|
80
|
+
private corruptionWatch: { finding: MidstreamEchoFinding; remaining: number; window: string } | undefined;
|
|
81
|
+
private readonly recorded: MidstreamEchoFinding[] = [];
|
|
82
|
+
|
|
83
|
+
feed(textDelta: string): void {
|
|
84
|
+
if (this.disarmed && !this.corruptionWatch) return;
|
|
85
|
+
let index = 0;
|
|
86
|
+
while (index < textDelta.length) {
|
|
87
|
+
const newline = textDelta.indexOf("\n", index);
|
|
88
|
+
const segment = newline === -1 ? textDelta.slice(index) : textDelta.slice(index, newline);
|
|
89
|
+
if (this.corruptionWatch) this.watchCorruption(segment + (newline === -1 ? "" : "\n"));
|
|
90
|
+
if (!this.disarmed && !this.lineDisarmed && segment.length > 0) {
|
|
91
|
+
this.lineBuffer += segment;
|
|
92
|
+
if (this.lineBuffer.length > MAX_MIDSTREAM_LINE_INDENT + 32) {
|
|
93
|
+
// Bound per-line work: nothing beyond the indent cap + longest marker can match.
|
|
94
|
+
this.lineDisarmed = !this.lineMatchesPrefixSoFar();
|
|
95
|
+
this.lineBuffer = this.lineBuffer.slice(0, MAX_MIDSTREAM_LINE_INDENT + 32);
|
|
96
|
+
}
|
|
97
|
+
this.checkLine();
|
|
98
|
+
}
|
|
99
|
+
if (newline === -1) break;
|
|
100
|
+
this.lineBuffer = "";
|
|
101
|
+
this.lineDisarmed = false;
|
|
102
|
+
this.lineStartOffset = this.totalLength + newline + 1;
|
|
103
|
+
index = newline + 1;
|
|
104
|
+
}
|
|
105
|
+
this.totalLength += textDelta.length;
|
|
106
|
+
if (this.totalLength > MAX_MIDSTREAM_SCAN_LENGTH) this.disarmed = true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
findings(): readonly MidstreamEchoFinding[] {
|
|
110
|
+
if (this.corruptionWatch) {
|
|
111
|
+
this.settleCorruption();
|
|
112
|
+
}
|
|
113
|
+
return this.recorded;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private lineMatchesPrefixSoFar(): boolean {
|
|
117
|
+
const probe = this.lineBuffer.replace(/^[ \t]*/, "");
|
|
118
|
+
return ECHO_MARKERS.some(marker => probe.startsWith(marker) || marker.startsWith(probe));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private checkLine(): void {
|
|
122
|
+
const indentMatch = /^[ \t]*/.exec(this.lineBuffer);
|
|
123
|
+
const indent = indentMatch ? indentMatch[0].length : 0;
|
|
124
|
+
if (indent > MAX_MIDSTREAM_LINE_INDENT) {
|
|
125
|
+
this.lineDisarmed = true;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const probe = this.lineBuffer.slice(indent);
|
|
129
|
+
for (const marker of ECHO_MARKERS) {
|
|
130
|
+
if (probe.startsWith(marker)) {
|
|
131
|
+
// The prefix sniffer owns the very start of the turn; only offsets past
|
|
132
|
+
// its window count as mid-stream.
|
|
133
|
+
if (this.lineStartOffset === 0) {
|
|
134
|
+
this.lineDisarmed = true;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const finding: MidstreamEchoFinding = {
|
|
138
|
+
marker,
|
|
139
|
+
offset: this.lineStartOffset,
|
|
140
|
+
callIdCorrupt: false,
|
|
141
|
+
};
|
|
142
|
+
this.corruptionWatch = { finding, remaining: MIDSTREAM_CORRUPTION_WINDOW, window: "" };
|
|
143
|
+
this.lineDisarmed = true;
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (!ECHO_MARKERS.some(marker => marker.startsWith(probe)) && probe.length > 0) {
|
|
148
|
+
this.lineDisarmed = true;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private watchCorruption(text: string): void {
|
|
153
|
+
const watch = this.corruptionWatch;
|
|
154
|
+
if (!watch) return;
|
|
155
|
+
const take = Math.min(watch.remaining, text.length);
|
|
156
|
+
watch.window += text.slice(0, take);
|
|
157
|
+
watch.remaining -= take;
|
|
158
|
+
if (watch.remaining <= 0) this.settleCorruption();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private settleCorruption(): void {
|
|
162
|
+
const watch = this.corruptionWatch;
|
|
163
|
+
if (!watch) return;
|
|
164
|
+
const window = watch.window;
|
|
165
|
+
watch.finding.callIdCorrupt =
|
|
166
|
+
/fc_[0-9a-f]+[ \t]+mar-/.test(window)
|
|
167
|
+
|| /call_id: \S+[ \t]+\S+_0\b/.test(window);
|
|
168
|
+
if (this.recorded.length < MAX_MIDSTREAM_FINDINGS) this.recorded.push(watch.finding);
|
|
169
|
+
// Window text is discarded here; only booleans/offsets survive.
|
|
170
|
+
this.corruptionWatch = undefined;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
46
174
|
/**
|
|
47
175
|
* Incremental envelope-prefix sniffer. Leading whitespace is tolerated so a
|
|
48
176
|
* marker copied after a newline is still caught.
|
|
@@ -12,7 +12,7 @@ import type { CursorRequestMessage, CursorRequestedModelParameter, CursorRunRequ
|
|
|
12
12
|
import { cursorCheckpointModelAffinityId, cursorWireModelSelection, type CursorRoutingLevel } from "./discovery";
|
|
13
13
|
import { cursorUltraBaseModelId } from "./discovery";
|
|
14
14
|
import { decodeCursorCallId } from "./call-id";
|
|
15
|
-
import {
|
|
15
|
+
import { cursorGrokFastSelection, resolveCursorSelection } from "./catalog";
|
|
16
16
|
import {
|
|
17
17
|
cursorMcpToolEncodedSize,
|
|
18
18
|
cursorMcpToolsEncodedSize,
|
|
@@ -192,20 +192,22 @@ function normalizeCursorModelId(modelId: string, reasoning?: string): {
|
|
|
192
192
|
routingLevel?: CursorRoutingLevel;
|
|
193
193
|
maxMode?: boolean;
|
|
194
194
|
} {
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
195
|
+
// Router ids (auto / auto-<level>) keep their dedicated wire selection.
|
|
196
|
+
const selection = cursorWireModelSelection(modelId);
|
|
197
|
+
if (selection.routingLevel !== undefined || selection.modelId === "default") return selection;
|
|
198
|
+
// Umbrella catalog resolution (devlog 260828_cursor_umbrella_catalog): one
|
|
199
|
+
// resolver owns effort composition, variant dimensions, the synthetic -1m
|
|
200
|
+
// marker (ultra -> Max Mode, evidence-gated), and the cursor- wire prefix.
|
|
200
201
|
const id = selection.modelId;
|
|
201
|
-
|
|
202
|
-
|
|
202
|
+
// Grok Fast stays parameterized: current Cursor clients send the base id
|
|
203
|
+
// plus effort/fast parameters instead of the flattened -fast id.
|
|
204
|
+
const grokFast = cursorGrokFastSelection(id, reasoning);
|
|
205
|
+
if (grokFast) {
|
|
203
206
|
return {
|
|
204
207
|
...selection,
|
|
205
|
-
|
|
206
|
-
modelId: id.slice(0, -"-fast".length),
|
|
208
|
+
modelId: grokFast.wireBaseId,
|
|
207
209
|
requestedModelParameters: [
|
|
208
|
-
{ id: "effort", value:
|
|
210
|
+
{ id: "effort", value: grokFast.effort },
|
|
209
211
|
{ id: "fast", value: "true" },
|
|
210
212
|
],
|
|
211
213
|
};
|
|
@@ -217,7 +219,12 @@ function normalizeCursorModelId(modelId: string, reasoning?: string): {
|
|
|
217
219
|
requestedModelParameters: [{ id: "fast", value: "false" }],
|
|
218
220
|
};
|
|
219
221
|
}
|
|
220
|
-
|
|
222
|
+
const resolved = resolveCursorSelection(id, reasoning);
|
|
223
|
+
return {
|
|
224
|
+
...selection,
|
|
225
|
+
...(resolved.maxMode ? { maxMode: true } : {}),
|
|
226
|
+
modelId: resolved.wireId,
|
|
227
|
+
};
|
|
221
228
|
}
|
|
222
229
|
|
|
223
230
|
function contentPartToText(part: OcxContentPart | OcxAssistantContentPart): string | undefined {
|
|
@@ -3,6 +3,7 @@ import { ValueSchema } from "@bufbuild/protobuf/wkt";
|
|
|
3
3
|
import type { OcxRequestOptions, OcxTool } from "../../types";
|
|
4
4
|
import { namespacedToolName, toolChoiceAliases } from "../../types";
|
|
5
5
|
import { McpToolDefinitionSchema, McpToolsSchema, type McpToolDefinition } from "./gen/agent_pb";
|
|
6
|
+
import { CODE_MODE_RESULT_ECHO_SENTENCE } from "../exec-tool-result-normalize";
|
|
6
7
|
|
|
7
8
|
export const OCX_RESPONSES_TOOL_PROVIDER = "opencodex-responses";
|
|
8
9
|
export const CODEX_EXEC_COMMAND_TOOL = "exec_command";
|
|
@@ -685,7 +686,7 @@ export function buildCursorToolGuidanceSystemNote(
|
|
|
685
686
|
? `\`${CODEX_UNIFIED_EXEC_TOOL}\` is Codex code mode: its body is JavaScript evaluated in a V8 isolate, not a shell command and not Node. Shell, file edits, and MCP are nested helpers called INSIDE that body as \`await tools.<name>(...)\`, for example \`await tools.exec_command({cmd: \"ls\"})\`. Read the tool description and the isolate global \`ALL_TOOLS\` (not \`tools.ALL_TOOLS\`) for helpers this turn provides; absence from the top-level catalog or from \`exec\`'s description is not absence. Those nested helpers are not themselves top-level tools, so do not call \`exec_command\` or \`shell_command\` at the top level here${codeModeOtherTopLevelNames.length > 0 ? `; every other tool this turn lists, including ${quotedNames(codeModeOtherTopLevelNames)}, remains callable at the top level as usual` : ""}. Nested \`tools.apply_patch(input)\` is host-executed: the string must begin exactly with \`*** Begin Patch\` and end with \`*** End Patch\` (no trailing \`***\` on those lines). OpenCodex does not rewrite JavaScript inside exec, so a decorated \`*** Begin Patch ***\` envelope is rejected by Codex before the file is touched.`
|
|
686
687
|
: undefined,
|
|
687
688
|
codeMode
|
|
688
|
-
?
|
|
689
|
+
? CODE_MODE_RESULT_ECHO_SENTENCE + " There is no `require`, no `module`, and no filesystem or network globals; reach the host only through the nested helpers."
|
|
689
690
|
: undefined,
|
|
690
691
|
codeMode
|
|
691
692
|
? 'When commands require network access, file writes outside workspace, or fail due to sandbox/permission restrictions, pass `sandbox_permissions: "require_escalated"` and a clear `justification: "..."` to `tools.exec_command`.'
|
|
@@ -9,6 +9,23 @@
|
|
|
9
9
|
* dropping images oldest-first — see toolCallStep in protobuf-request.ts).
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import {
|
|
13
|
+
EMPTY_EXEC_OUTPUT_MESSAGE,
|
|
14
|
+
EMPTY_EXEC_OUTPUT_REGEX,
|
|
15
|
+
FAILED_EXEC_OUTPUT_MESSAGE,
|
|
16
|
+
FAILED_EXEC_OUTPUT_REGEX,
|
|
17
|
+
isCodexExecBridgeTool,
|
|
18
|
+
} from "../exec-tool-result-normalize";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Cursor treats a failed-but-empty wrapper as an empty result too (its Computer Use branch marks
|
|
22
|
+
* such results `isError` separately). The shared success regex deliberately excludes
|
|
23
|
+
* `Script failed`, so restore that arm here rather than widening the shared one.
|
|
24
|
+
*/
|
|
25
|
+
function isEmptyOrFailedExecWrapper(text: string): boolean {
|
|
26
|
+
return EMPTY_EXEC_OUTPUT_REGEX.test(text) || FAILED_EXEC_OUTPUT_REGEX.test(text);
|
|
27
|
+
}
|
|
28
|
+
|
|
12
29
|
const COMPUTER_USE_TOOL_NAMES = new Set([
|
|
13
30
|
"node_repl",
|
|
14
31
|
"node_repl__js",
|
|
@@ -29,31 +46,6 @@ function isNodeReplOrComputerUseTool(toolName?: string, toolNamespace?: string):
|
|
|
29
46
|
return lower.startsWith("mcp__node_repl") || lower.startsWith("mcp__computer_use");
|
|
30
47
|
}
|
|
31
48
|
|
|
32
|
-
/**
|
|
33
|
-
* Codex exec / shell-bridge tool names (flat and MCP-prefixed display aliases). An empty result
|
|
34
|
-
* here is almost always a code-mode cell that never called text()/notify() — the cursor model
|
|
35
|
-
* reads the blank [tool_result], concludes prior results were lost, and spirals into
|
|
36
|
-
* re-orientation retries (devlog 260826_cursor_responses_gap, live subagent transcripts).
|
|
37
|
-
*/
|
|
38
|
-
function isCodexExecBridgeTool(toolName?: string, toolNamespace?: string): boolean {
|
|
39
|
-
if (toolNamespace && toolNamespace.includes("opencodex-responses")) return true;
|
|
40
|
-
if (!toolName) return false;
|
|
41
|
-
const lower = toolName.toLowerCase();
|
|
42
|
-
return (
|
|
43
|
-
lower === "exec"
|
|
44
|
-
|| lower === "exec_command"
|
|
45
|
-
|| lower === "shell_command"
|
|
46
|
-
// Codex CLI/desktop native tool names: the multi-round "이전 출력이 비어 있어 처음부터"
|
|
47
|
-
// restart loop reproduced via codex exec because `shell` was not in this set
|
|
48
|
-
// (devlog 260826 gap-8 QA round 2).
|
|
49
|
-
|| lower === "shell"
|
|
50
|
-
|| lower === "local_shell"
|
|
51
|
-
|| lower === "container.exec"
|
|
52
|
-
|| lower.startsWith("mcp_opencodex-responses_")
|
|
53
|
-
|| lower.startsWith("mcp__opencodex-responses__")
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
49
|
/** Failure states the Computer Use / node_repl runtime reports as PLAIN TEXT inside a non-error result. */
|
|
58
50
|
const RUNTIME_FAILURE_GUIDANCE: ReadonlyArray<{ marker: string; guidance: string }> = [
|
|
59
51
|
{
|
|
@@ -90,9 +82,6 @@ const RUNTIME_FAILURE_GUIDANCE: ReadonlyArray<{ marker: string; guidance: string
|
|
|
90
82
|
},
|
|
91
83
|
];
|
|
92
84
|
|
|
93
|
-
/** Matches exec wrappers whose only payload is an empty-output marker. */
|
|
94
|
-
const EMPTY_EXEC_OUTPUT_REGEX = /^(?:(?:Script completed|Script failed|Command finished|Execution finished)[^\n]*\n+)?(?:Wall time[^\n]*\n+)?(?:Output:\s*)?(?:<empty>)?\s*$/;
|
|
95
|
-
|
|
96
85
|
export interface NormalizedToolResultText {
|
|
97
86
|
text: string;
|
|
98
87
|
isError: boolean;
|
|
@@ -114,16 +103,19 @@ export function normalizeCursorToolResultText(
|
|
|
114
103
|
): NormalizedToolResultText {
|
|
115
104
|
const isError = options.isError === true;
|
|
116
105
|
const computerUse = isNodeReplOrComputerUseTool(options.toolName, options.toolNamespace);
|
|
117
|
-
if (computerUse &&
|
|
106
|
+
if (computerUse && isEmptyOrFailedExecWrapper(text.trim())) {
|
|
118
107
|
return {
|
|
119
108
|
text: "[empty output: the tool ran but produced no stdout or return value. Verify application state with get_app_state, or make the script emit output.]",
|
|
120
109
|
isError: true,
|
|
121
110
|
changed: true,
|
|
122
111
|
};
|
|
123
112
|
}
|
|
124
|
-
if (isCodexExecBridgeTool(options.toolName, options.toolNamespace) &&
|
|
113
|
+
if (isCodexExecBridgeTool(options.toolName, options.toolNamespace) && isEmptyOrFailedExecWrapper(text.trim())) {
|
|
125
114
|
return {
|
|
126
|
-
|
|
115
|
+
// A `Script failed` wrapper is empty but NOT a success: reporting it as an empty success
|
|
116
|
+
// would erase the only failure signal. Text classification stays separate from Cursor's
|
|
117
|
+
// isError policy, which the Computer Use branch above owns.
|
|
118
|
+
text: FAILED_EXEC_OUTPUT_REGEX.test(text.trim()) ? FAILED_EXEC_OUTPUT_MESSAGE : EMPTY_EXEC_OUTPUT_MESSAGE,
|
|
127
119
|
isError: false,
|
|
128
120
|
changed: true,
|
|
129
121
|
};
|
package/src/adapters/cursor.ts
CHANGED
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
CURSOR_ECHO_RETRY_CONTINUATION_TEXT,
|
|
42
42
|
CURSOR_ROUTING_COMMENTARY_RETRY_TEXT,
|
|
43
43
|
CursorEnvelopeEchoSniffer,
|
|
44
|
+
CursorMidstreamEchoObserver,
|
|
44
45
|
CursorRoutingCommentaryError,
|
|
45
46
|
CursorRoutingCommentarySniffer,
|
|
46
47
|
CursorToolResultEchoError,
|
|
@@ -256,6 +257,9 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
256
257
|
isCursorExternalWireModel(activeRequest.modelId)
|
|
257
258
|
&& (_parsed.context.messages ?? []).some(message => message.role === "toolResult");
|
|
258
259
|
const echoSniffer = armEchoSniffer ? new CursorEnvelopeEchoSniffer() : undefined;
|
|
260
|
+
// Mid-stream observer (devlog 260828 F1/F2): diagnostic-only; armed with the
|
|
261
|
+
// prefix sniffer because both fire on flattened tool-result replay priming.
|
|
262
|
+
const midstreamObserver = armEchoSniffer ? new CursorMidstreamEchoObserver() : undefined;
|
|
259
263
|
const armRoutingCommentarySniffer =
|
|
260
264
|
isCursorExternalWireModel(activeRequest.modelId)
|
|
261
265
|
&& (
|
|
@@ -266,10 +270,16 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
266
270
|
? new CursorRoutingCommentarySniffer()
|
|
267
271
|
: undefined;
|
|
268
272
|
let guardHeld: AdapterEvent[] = [];
|
|
273
|
+
// Exactly-once observation: every client-bound text delta passes through here
|
|
274
|
+
// exactly once — held deltas only on release, ordinary deltas at emit time.
|
|
275
|
+
const emitTextObserved = (event: AdapterEvent): void => {
|
|
276
|
+
if (event.type === "text_delta") midstreamObserver?.feed(event.text);
|
|
277
|
+
emit(event);
|
|
278
|
+
};
|
|
269
279
|
const releaseGuardHeld = () => {
|
|
270
280
|
for (const held of guardHeld) {
|
|
271
281
|
if (held.type !== "heartbeat") emittedOutput = true;
|
|
272
|
-
|
|
282
|
+
emitTextObserved(held);
|
|
273
283
|
}
|
|
274
284
|
guardHeld = [];
|
|
275
285
|
};
|
|
@@ -348,6 +358,15 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
348
358
|
}
|
|
349
359
|
if (event.type !== "heartbeat") emittedOutput = true;
|
|
350
360
|
if (event.type === "done") {
|
|
361
|
+
for (const finding of midstreamObserver?.findings() ?? []) {
|
|
362
|
+
debugProviderDiagnostic("cursor", "midstream-envelope-echo", {
|
|
363
|
+
wireModel: activeRequest.modelId,
|
|
364
|
+
conversationHash: activeRequest.conversationId.slice(0, 16),
|
|
365
|
+
marker: finding.marker,
|
|
366
|
+
offset: finding.offset,
|
|
367
|
+
callIdCorrupt: finding.callIdCorrupt,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
351
370
|
commitCapturedCheckpoint(activeRequest);
|
|
352
371
|
const inheritedCursor = _parsed._providerContinuation?.cursor;
|
|
353
372
|
const isolatedOrCompaction =
|
|
@@ -367,7 +386,7 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
367
386
|
: undefined;
|
|
368
387
|
emit(providerState ? { ...event, providerState } : event);
|
|
369
388
|
} else {
|
|
370
|
-
|
|
389
|
+
emitTextObserved(event);
|
|
371
390
|
}
|
|
372
391
|
}
|
|
373
392
|
},
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-neutral empty-exec-output normalization.
|
|
3
|
+
*
|
|
4
|
+
* A code-mode `exec` cell that never calls `text()`/`notify()` returns nothing: the last
|
|
5
|
+
* expression value is NOT echoed automatically. The routed model reads a blank tool result,
|
|
6
|
+
* concludes its earlier output was lost, and burns turns re-running the same call or restarting
|
|
7
|
+
* the task from scratch. Naming that state explicitly is what breaks the loop.
|
|
8
|
+
*
|
|
9
|
+
* This module owns the shared detection so every adapter reports the same thing. Cursor keeps its
|
|
10
|
+
* own wrapper (`normalizeCursorToolResultText`) for Computer Use precedence and isError policy;
|
|
11
|
+
* Kiro consumes this helper directly.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Matches exec wrappers whose only payload is an empty-output marker.
|
|
16
|
+
*
|
|
17
|
+
* `Script failed` is deliberately NOT in this set. A failed cell with no captured output is still
|
|
18
|
+
* a FAILURE, and the success guidance below ("not a blocked tool", "do not re-run") would erase the
|
|
19
|
+
* only signal that anything went wrong — reachable through Responses history, where
|
|
20
|
+
* `function_call_output` is parsed with `isError: false`. Cursor keeps its own broader regex for
|
|
21
|
+
* Computer Use, where a failed wrapper is separately marked `isError`.
|
|
22
|
+
*/
|
|
23
|
+
export const EMPTY_EXEC_OUTPUT_REGEX = /^(?:(?:Script completed|Command finished|Execution finished)[^\n]*\n+)?(?:Wall time[^\n]*\n+)?(?:Output:\s*)?(?:<empty>)?\s*$/;
|
|
24
|
+
|
|
25
|
+
/** Wrapper for a cell that FAILED without emitting output: empty, but not a success. */
|
|
26
|
+
export const FAILED_EXEC_OUTPUT_REGEX = /^Script failed[^\n]*\n*(?:Wall time[^\n]*\n*)?(?:Output:\s*)?(?:<empty>)?\s*$/;
|
|
27
|
+
|
|
28
|
+
/** Guidance for a failed cell whose output was empty: the failure must survive normalization. */
|
|
29
|
+
export const FAILED_EXEC_OUTPUT_MESSAGE =
|
|
30
|
+
"[exec failed with no captured output: the cell raised before emitting anything. This is a real failure, not an empty success — inspect the call for a thrown error or syntax problem before retrying.]";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The guidance itself. Worded to close all three wrong conclusions a model draws from a blank
|
|
34
|
+
* result: that context was lost, that the tool is blocked, and that retrying will differ.
|
|
35
|
+
*/
|
|
36
|
+
export const EMPTY_EXEC_OUTPUT_MESSAGE =
|
|
37
|
+
"[empty output: the exec cell completed but emitted nothing. This is NOT lost context and NOT a blocked tool — in code mode call text(...) or notify(...) on any value you need to see (a bare await tools.exec_command(...) is not echoed automatically); in shell mode the command simply printed nothing. Do not re-run the same call expecting different output.]";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The SAME rule stated BEFORE the first call, for the code-mode tool-catalog nudge.
|
|
41
|
+
*
|
|
42
|
+
* `EMPTY_EXEC_OUTPUT_MESSAGE` above is a repair: it fires only after a model has already spent a
|
|
43
|
+
* call and read a blank result. That recovers the turn but cannot prevent the wasted round trip,
|
|
44
|
+
* and the model still has to guess whether its command failed or its output was merely dropped.
|
|
45
|
+
* Stating the echo rule up front removes the failure instead of explaining it afterwards.
|
|
46
|
+
*
|
|
47
|
+
* Kept beside the recovery text on purpose: the two are one pair guarding one defect, and wording
|
|
48
|
+
* that drifts apart is how a model gets told two different things about the same isolate.
|
|
49
|
+
*/
|
|
50
|
+
export const CODE_MODE_RESULT_ECHO_SENTENCE =
|
|
51
|
+
"Nothing in the isolate is echoed automatically: a bare trailing `await tools.<name>(...)` or final expression value is DISCARDED, and the cell reports empty output. Pass anything you need to read to `text(...)` (or `notify(...)`) in the same cell — for example `text(JSON.stringify(await tools.exec_command({cmd: \"ls\"})))` — and treat an empty result as your own missing `text(...)` call rather than a failed command or lost context.";
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Codex exec / shell-bridge tool names (flat and MCP-prefixed display aliases). An empty result
|
|
55
|
+
* here is almost always a code-mode cell that never called text()/notify().
|
|
56
|
+
*/
|
|
57
|
+
export function isCodexExecBridgeTool(toolName?: string, toolNamespace?: string): boolean {
|
|
58
|
+
if (toolNamespace && toolNamespace.includes("opencodex-responses")) return true;
|
|
59
|
+
if (!toolName) return false;
|
|
60
|
+
const lower = toolName.toLowerCase();
|
|
61
|
+
return (
|
|
62
|
+
lower === "exec"
|
|
63
|
+
|| lower === "exec_command"
|
|
64
|
+
|| lower === "shell_command"
|
|
65
|
+
// Codex CLI/desktop native tool names: the multi-round "이전 출력이 비어 있어 처음부터"
|
|
66
|
+
// restart loop reproduced via codex exec because `shell` was not in this set
|
|
67
|
+
// (devlog 260826 gap-8 QA round 2).
|
|
68
|
+
|| lower === "shell"
|
|
69
|
+
|| lower === "local_shell"
|
|
70
|
+
|| lower === "container.exec"
|
|
71
|
+
|| lower.startsWith("mcp_opencodex-responses_")
|
|
72
|
+
|| lower.startsWith("mcp__opencodex-responses__")
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** True when this result is an exec-bridge call that produced no usable output. */
|
|
77
|
+
export function isEmptyExecToolResult(
|
|
78
|
+
text: string,
|
|
79
|
+
options: { toolName?: string; toolNamespace?: string } = {},
|
|
80
|
+
): boolean {
|
|
81
|
+
return isCodexExecBridgeTool(options.toolName, options.toolNamespace)
|
|
82
|
+
&& EMPTY_EXEC_OUTPUT_REGEX.test(text.trim());
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Returns the guidance text when this is an empty exec-bridge result, else `undefined` so the
|
|
87
|
+
* caller keeps its own fallback. Undefined rather than the original text: an adapter must be able
|
|
88
|
+
* to tell "not my case" from "normalized to the same string".
|
|
89
|
+
*/
|
|
90
|
+
export function normalizeEmptyExecToolResultText(
|
|
91
|
+
text: string,
|
|
92
|
+
options: { toolName?: string; toolNamespace?: string } = {},
|
|
93
|
+
): string | undefined {
|
|
94
|
+
if (!isCodexExecBridgeTool(options.toolName, options.toolNamespace)) return undefined;
|
|
95
|
+
const trimmed = text.trim();
|
|
96
|
+
// Failure first: a failed wrapper must never be described as an empty success.
|
|
97
|
+
if (FAILED_EXEC_OUTPUT_REGEX.test(trimmed)) return FAILED_EXEC_OUTPUT_MESSAGE;
|
|
98
|
+
return EMPTY_EXEC_OUTPUT_REGEX.test(trimmed) ? EMPTY_EXEC_OUTPUT_MESSAGE : undefined;
|
|
99
|
+
}
|