@bitkyc08/opencodex 2.26.0 → 2.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/{index-RL6b1bTV.js → index-7jlKgmJd.js} +14 -14
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +1 -1
- package/src/adapters/base.ts +14 -2
- package/src/adapters/command-code.ts +4 -3
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/live-transport.ts +14 -1
- package/src/adapters/google.ts +1 -1
- package/src/adapters/openai-chat.ts +38 -6
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/bridge.ts +11 -5
- package/src/cli/doctor.ts +76 -0
- package/src/cli/help.ts +2 -0
- package/src/cli/models.ts +13 -6
- package/src/codex/app-server-processes.ts +269 -37
- package/src/codex/auth-context.ts +53 -1
- package/src/codex/catalog/aggregation.ts +3 -0
- package/src/codex/catalog/parsing.ts +20 -3
- package/src/codex/catalog/provider-fetch.ts +8 -0
- package/src/codex/catalog/sync.ts +6 -4
- package/src/codex/log-guard/path-safety.ts +52 -3
- package/src/codex/native-profile-startup.ts +100 -2
- package/src/codex/user-identity.ts +21 -1
- package/src/config/provider-name.ts +24 -0
- package/src/config.ts +11 -24
- package/src/generated/compatibility-version.json +73 -45
- package/src/images/loop.ts +11 -4
- package/src/lib/state-store-registrations.ts +8 -2
- package/src/providers/antigravity-models.ts +70 -5
- package/src/providers/derive.ts +12 -2
- package/src/providers/registry.ts +46 -1
- package/src/providers/service-tier.ts +34 -7
- package/src/responses/parser.ts +56 -2
- package/src/responses/state.ts +162 -5
- package/src/router.ts +10 -3
- package/src/routing/compatibility/behavior.ts +3 -3
- package/src/routing/profile.ts +1 -1
- package/src/server/index.ts +5 -0
- package/src/server/management/shared.ts +3 -1
- package/src/server/responses/collaboration.ts +34 -9
- package/src/server/responses/core.ts +13 -4
- package/src/server/responses/input-admission.ts +7 -2
- package/src/service-manager-probe.ts +99 -0
- package/src/service.ts +86 -6
- package/src/tray/windows.ts +25 -5
- package/src/types/accounts.ts +37 -0
- package/src/types/config.ts +818 -0
- package/src/types/provider.ts +521 -0
- package/src/types/request.ts +358 -0
- package/src/types/tools.ts +131 -0
- package/src/types/wire.ts +80 -0
- package/src/types.ts +103 -1883
- package/src/usage/cost.ts +37 -1
- package/src/web-search/loop.ts +11 -4
package/gui/dist/index.html
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
} catch (e) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-7jlKgmJd.js"></script>
|
|
20
20
|
<link rel="stylesheet" crossorigin href="/assets/index-DQsMZzI5.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
package/package.json
CHANGED
|
@@ -743,7 +743,7 @@ function toolsToAnthropicFormat(parsed: OcxParsedRequest, toolNames: { toWire: (
|
|
|
743
743
|
? new Set(parsed.options.toolChoice.allowedTools)
|
|
744
744
|
: undefined;
|
|
745
745
|
const tools = allowed
|
|
746
|
-
? parsed.context.tools.filter(t => toolAllowedByChoice(t, allowed))
|
|
746
|
+
? parsed.context.tools.filter(t => toolAllowedByChoice(t, allowed, parsed.context.tools))
|
|
747
747
|
: parsed.context.tools;
|
|
748
748
|
if (tools.length === 0) return undefined;
|
|
749
749
|
const converted = tools.map(t => ({
|
package/src/adapters/base.ts
CHANGED
|
@@ -39,8 +39,20 @@ export interface ProviderAdapter {
|
|
|
39
39
|
|
|
40
40
|
fetchResponse?(request: AdapterRequest, ctx?: AdapterFetchContext): Promise<Response>;
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Parse one upstream response. `tierMetadata` is the same live observer returned on the
|
|
44
|
+
* corresponding AdapterRequest; adapters that receive a documented tier echo may update it.
|
|
45
|
+
*/
|
|
46
|
+
parseStream(
|
|
47
|
+
response: Response,
|
|
48
|
+
budget: TranslatorBudget,
|
|
49
|
+
tierMetadata?: AdapterTierMetadata,
|
|
50
|
+
): AsyncGenerator<AdapterEvent>;
|
|
51
|
+
parseResponse?(
|
|
52
|
+
response: Response,
|
|
53
|
+
budget: TranslatorBudget,
|
|
54
|
+
tierMetadata?: AdapterTierMetadata,
|
|
55
|
+
): Promise<AdapterEvent[]>;
|
|
44
56
|
runTurn?(
|
|
45
57
|
parsed: OcxParsedRequest,
|
|
46
58
|
incoming: IncomingMeta,
|
|
@@ -3,7 +3,7 @@ import { execFile as execFileCallback } from "node:child_process";
|
|
|
3
3
|
import { promisify } from "node:util";
|
|
4
4
|
import { opendir } from "node:fs/promises";
|
|
5
5
|
import type { AdapterEvent, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTool, OcxUsage } from "../types";
|
|
6
|
-
import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName, toolAllowedByChoice
|
|
6
|
+
import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName, toolAllowedByChoice } from "../types";
|
|
7
7
|
import type { AdapterFetchContext, AdapterRequest, ProviderAdapter } from "./base";
|
|
8
8
|
import type { TranslatorBudget } from "../lib/translator-budget";
|
|
9
9
|
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
@@ -157,10 +157,11 @@ function visibleTools(parsed: OcxParsedRequest): OcxTool[] {
|
|
|
157
157
|
const tools = parsed.context.tools ?? [];
|
|
158
158
|
if (isAllowedToolChoice(choice)) {
|
|
159
159
|
const allowed = new Set(choice.allowedTools);
|
|
160
|
-
return tools.filter(tool => toolAllowedByChoice(tool, allowed));
|
|
160
|
+
return tools.filter(tool => toolAllowedByChoice(tool, allowed, tools));
|
|
161
161
|
}
|
|
162
162
|
if (choice && typeof choice !== "string") {
|
|
163
|
-
|
|
163
|
+
const selected = resolveToolChoiceWireName(tools, choice.name);
|
|
164
|
+
return tools.filter(tool => namespacedToolName(tool.namespace, tool.name) === selected);
|
|
164
165
|
}
|
|
165
166
|
return tools;
|
|
166
167
|
}
|
|
@@ -85,6 +85,21 @@ export function isCursorBenignCancelError(value: unknown): boolean {
|
|
|
85
85
|
return false;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* True when the turn was torn down by an `AbortSignal` rather than by a transport fault.
|
|
90
|
+
*
|
|
91
|
+
* This is deliberately NOT part of `isCursorBenignCancelError`: an abort mid-turn is a real
|
|
92
|
+
* failure and must still surface. It is only meaningful in combination with a terminal frame
|
|
93
|
+
* having already been emitted, where it means "the answer landed and then the connection went
|
|
94
|
+
* away" (#1527).
|
|
95
|
+
*/
|
|
96
|
+
export function isCursorAbortError(value: unknown): boolean {
|
|
97
|
+
const message = errorMessage(value).toLowerCase();
|
|
98
|
+
if (message.includes("cursor request was aborted")) return true;
|
|
99
|
+
const name = (value as { name?: unknown })?.name;
|
|
100
|
+
return typeof name === "string" && name === "AbortError";
|
|
101
|
+
}
|
|
102
|
+
|
|
88
103
|
/**
|
|
89
104
|
* True when Cursor Connect rejected the turn with invalid_argument.
|
|
90
105
|
* Seen after stepCompleted on brittle external-model continuations.
|
|
@@ -48,7 +48,7 @@ import {
|
|
|
48
48
|
type InteractionResponse,
|
|
49
49
|
} from "./gen/agent_pb";
|
|
50
50
|
import { debugProviderDiagnostic } from "../../lib/debug";
|
|
51
|
-
import { classifyCursorError, CursorUnexpectedCancelError, isCursorBenignCancelError, safeCursorErrorMessage } from "./cursor-errors";
|
|
51
|
+
import { classifyCursorError, CursorUnexpectedCancelError, isCursorAbortError, isCursorBenignCancelError, safeCursorErrorMessage } from "./cursor-errors";
|
|
52
52
|
import { mcpArgsFromToolCall } from "./protobuf-events";
|
|
53
53
|
import { OCX_RESPONSES_TOOL_PROVIDER } from "./tool-definitions";
|
|
54
54
|
import {
|
|
@@ -650,6 +650,18 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
650
650
|
// A CANCEL is benign only on the client-tool suspend path (expectedClose); an
|
|
651
651
|
// unexpected server-side NGHTTP2_CANCEL must surface as a real transport error.
|
|
652
652
|
if (this.expectedClose && isCursorBenignCancelError(failure)) return;
|
|
653
|
+
// A teardown error arriving AFTER the turn's terminal frame describes the connection,
|
|
654
|
+
// not the turn: the answer is committed and every queued message has been yielded.
|
|
655
|
+
//
|
|
656
|
+
// Narrow on purpose. A benign cancel after a terminal is already swallowed one layer
|
|
657
|
+
// up (`cursor.ts:183`), so widening this to every post-terminal error would change
|
|
658
|
+
// what the adapter sees for genuine faults. What it does cover is the abort case
|
|
659
|
+
// from #1527: `signal.abort` fires `failAndClear(new Error("Cursor request was
|
|
660
|
+
// aborted"))`, which is NOT benign (`cursor-errors.ts:74`), so an ordinary completed
|
|
661
|
+
// turn that is then torn down still surfaced as `turn-failed` with
|
|
662
|
+
// `expectedClose:false`. Only `cancelCursorRun()` sets `expectedClose`, so a normal
|
|
663
|
+
// completion never qualified for the branch above.
|
|
664
|
+
if (this.emittedTerminal && isCursorAbortError(failure)) return;
|
|
653
665
|
throw attachPartialUsage(classifyTurnFailure(failure), state);
|
|
654
666
|
}
|
|
655
667
|
if (done) break;
|
|
@@ -659,6 +671,7 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
659
671
|
}
|
|
660
672
|
if (failure) {
|
|
661
673
|
if (this.expectedClose && isCursorBenignCancelError(failure)) return;
|
|
674
|
+
if (this.emittedTerminal && isCursorAbortError(failure)) return;
|
|
662
675
|
throw attachPartialUsage(classifyTurnFailure(failure), state);
|
|
663
676
|
}
|
|
664
677
|
}
|
package/src/adapters/google.ts
CHANGED
|
@@ -262,7 +262,7 @@ function toolsToGeminiFormat(parsed: OcxParsedRequest): unknown[] | undefined {
|
|
|
262
262
|
? new Set(parsed.options.toolChoice.allowedTools)
|
|
263
263
|
: undefined;
|
|
264
264
|
const tools = allowed
|
|
265
|
-
? parsed.context.tools.filter(t => toolAllowedByChoice(t, allowed))
|
|
265
|
+
? parsed.context.tools.filter(t => toolAllowedByChoice(t, allowed, parsed.context.tools))
|
|
266
266
|
: parsed.context.tools;
|
|
267
267
|
if (tools.length === 0) return undefined;
|
|
268
268
|
return [{
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
import {
|
|
20
20
|
canonicalFastTierMarker,
|
|
21
21
|
createAdapterTierMetadata,
|
|
22
|
+
type AdapterTierMetadata,
|
|
22
23
|
} from "../providers/fastwire";
|
|
23
24
|
import { openaiChatCompletionsUrl } from "./openai-chat-url";
|
|
24
25
|
import { stripResponsesOnlyEncryptedMarker } from "./responses-tool-schema";
|
|
@@ -1156,7 +1157,11 @@ function normalizeXaiToolParameters(parameters: unknown): Record<string, unknown
|
|
|
1156
1157
|
if (!isXaiObjectSchema(parameters)) return undefined;
|
|
1157
1158
|
const resolved = resolveXaiSchemaRefs(parameters, parameters);
|
|
1158
1159
|
if (!isXaiObjectSchema(resolved)) return undefined;
|
|
1159
|
-
|
|
1160
|
+
|
|
1161
|
+
const normalizedRoot = { ...resolved };
|
|
1162
|
+
delete normalizedRoot.$schema;
|
|
1163
|
+
|
|
1164
|
+
const variants = expandXaiRootObjectSchemas(normalizedRoot);
|
|
1160
1165
|
if (!variants) return undefined;
|
|
1161
1166
|
if (variants.length === 1) {
|
|
1162
1167
|
return xaiVariantIsConcreteObject(variants[0]) ? variants[0] : undefined;
|
|
@@ -1166,7 +1171,7 @@ function normalizeXaiToolParameters(parameters: unknown): Record<string, unknown
|
|
|
1166
1171
|
if (!additionalProperties.ok) return undefined;
|
|
1167
1172
|
if (!xaiPropertyMergeIsLossless(variants)) return undefined;
|
|
1168
1173
|
|
|
1169
|
-
const metadata = Object.fromEntries(Object.entries(
|
|
1174
|
+
const metadata = Object.fromEntries(Object.entries(normalizedRoot).filter(([key]) => key !== "oneOf" && key !== "anyOf" && key !== "type"));
|
|
1170
1175
|
delete metadata.properties;
|
|
1171
1176
|
delete metadata.required;
|
|
1172
1177
|
delete metadata.additionalProperties;
|
|
@@ -1180,6 +1185,7 @@ function normalizeXaiToolParameters(parameters: unknown): Record<string, unknown
|
|
|
1180
1185
|
propertyValues.set(name, values);
|
|
1181
1186
|
}
|
|
1182
1187
|
}
|
|
1188
|
+
|
|
1183
1189
|
const properties = Object.fromEntries(
|
|
1184
1190
|
[...propertyValues].map(([name, values]) => [name, mergeXaiPropertySchemas(values)]),
|
|
1185
1191
|
);
|
|
@@ -1196,7 +1202,7 @@ function normalizeXaiToolParameters(parameters: unknown): Record<string, unknown
|
|
|
1196
1202
|
|
|
1197
1203
|
function toolsToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderConfig): unknown[] | undefined {
|
|
1198
1204
|
if (!parsed.context.tools || parsed.context.tools.length === 0) return undefined;
|
|
1199
|
-
const tools = parsed.context.tools.filter(toolChoiceToolPredicate(parsed.options.toolChoice));
|
|
1205
|
+
const tools = parsed.context.tools.filter(toolChoiceToolPredicate(parsed.options.toolChoice, parsed.context.tools));
|
|
1200
1206
|
if (tools.length === 0) return undefined;
|
|
1201
1207
|
const xaiTarget = isXaiSchemaTarget(provider);
|
|
1202
1208
|
const formatted = tools.flatMap(t => {
|
|
@@ -1478,7 +1484,11 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
1478
1484
|
};
|
|
1479
1485
|
},
|
|
1480
1486
|
|
|
1481
|
-
async *parseStream(
|
|
1487
|
+
async *parseStream(
|
|
1488
|
+
response: Response,
|
|
1489
|
+
budget: TranslatorBudget,
|
|
1490
|
+
tierMetadata?: AdapterTierMetadata,
|
|
1491
|
+
): AsyncGenerator<AdapterEvent> {
|
|
1482
1492
|
if (!response.body) {
|
|
1483
1493
|
yield { type: "error", message: "No response body" };
|
|
1484
1494
|
return;
|
|
@@ -1547,11 +1557,15 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
1547
1557
|
try {
|
|
1548
1558
|
parsed = JSON.parse(payload);
|
|
1549
1559
|
} catch {
|
|
1560
|
+
tierMetadata?.markResponseUnparseable();
|
|
1550
1561
|
yield { type: "error", message: "malformed upstream SSE data frame" };
|
|
1551
1562
|
return "terminate";
|
|
1552
1563
|
}
|
|
1553
1564
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return "continue";
|
|
1554
1565
|
const chunk = parsed as Record<string, unknown>;
|
|
1566
|
+
if (Object.hasOwn(chunk, "service_tier")) {
|
|
1567
|
+
tierMetadata?.observeResponseServiceTier(chunk.service_tier);
|
|
1568
|
+
}
|
|
1555
1569
|
|
|
1556
1570
|
if (chunk.error !== undefined && chunk.error !== null) {
|
|
1557
1571
|
const event = upstreamErrorEvent(chunk.error, pendingUsage);
|
|
@@ -1752,8 +1766,26 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
1752
1766
|
}
|
|
1753
1767
|
},
|
|
1754
1768
|
|
|
1755
|
-
async parseResponse(
|
|
1756
|
-
|
|
1769
|
+
async parseResponse(
|
|
1770
|
+
response: Response,
|
|
1771
|
+
budget: TranslatorBudget,
|
|
1772
|
+
tierMetadata?: AdapterTierMetadata,
|
|
1773
|
+
): Promise<AdapterEvent[]> {
|
|
1774
|
+
let parsed: unknown;
|
|
1775
|
+
try {
|
|
1776
|
+
parsed = await response.json();
|
|
1777
|
+
} catch (error) {
|
|
1778
|
+
tierMetadata?.markResponseUnparseable();
|
|
1779
|
+
throw error;
|
|
1780
|
+
}
|
|
1781
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1782
|
+
tierMetadata?.markResponseUnparseable();
|
|
1783
|
+
throw new Error("upstream response was not a JSON object");
|
|
1784
|
+
}
|
|
1785
|
+
const json = parsed as Record<string, unknown>;
|
|
1786
|
+
if (Object.hasOwn(json, "service_tier")) {
|
|
1787
|
+
tierMetadata?.observeResponseServiceTier(json.service_tier);
|
|
1788
|
+
}
|
|
1757
1789
|
const responseBytes = new TextEncoder().encode(JSON.stringify(json)).byteLength;
|
|
1758
1790
|
budget.chargeRetained(responseBytes, { kind: "retained_collectors" });
|
|
1759
1791
|
try {
|
|
@@ -135,7 +135,7 @@ export function buildNonOpenAIToolCatalogNudgeForTools(
|
|
|
135
135
|
toolChoice?: OcxRequestOptions["toolChoice"],
|
|
136
136
|
toWireName: (tool: Pick<OcxTool, "namespace" | "name">) => string = tool => namespacedToolName(tool.namespace, tool.name),
|
|
137
137
|
): string | undefined {
|
|
138
|
-
const visible = tools?.filter(toolChoiceToolPredicate(toolChoice));
|
|
138
|
+
const visible = tools?.filter(toolChoiceToolPredicate(toolChoice, tools));
|
|
139
139
|
const visibleNames = visible?.map(toWireName);
|
|
140
140
|
// Decide code mode from the tool OBJECTS, while the `freeform` flag still exists — reducing
|
|
141
141
|
// to wire names first throws away the only thing that distinguishes Codex's JavaScript
|
package/src/bridge.ts
CHANGED
|
@@ -167,7 +167,7 @@ export type ResponsesTerminalStatus = "completed" | "failed" | "incomplete";
|
|
|
167
167
|
export function bridgeToResponsesSSE(
|
|
168
168
|
events: AsyncIterable<AdapterEvent>,
|
|
169
169
|
modelId: string,
|
|
170
|
-
toolNsMap?: Map<string, { namespace: string; name: string }>,
|
|
170
|
+
toolNsMap?: Map<string, { namespace: string; name: string; freeform?: true }>,
|
|
171
171
|
freeformToolNames?: Set<string>,
|
|
172
172
|
toolSearchToolNames?: Set<string>,
|
|
173
173
|
onCancel?: () => void,
|
|
@@ -1050,7 +1050,9 @@ export function bridgeToResponsesSSE(
|
|
|
1050
1050
|
}
|
|
1051
1051
|
const ns = mapped?.namespace;
|
|
1052
1052
|
const toolSearch = toolSearchToolNames?.has(realName) ?? false;
|
|
1053
|
-
const freeform = !toolSearch && (
|
|
1053
|
+
const freeform = !toolSearch && (mapped
|
|
1054
|
+
? mapped.freeform === true
|
|
1055
|
+
: (freeformToolNames?.has(realName) ?? false));
|
|
1054
1056
|
const itemId = `${toolSearch ? "tsc" : freeform ? "ctc" : "fc"}_${uuid()}`;
|
|
1055
1057
|
const item = toolSearch
|
|
1056
1058
|
? { type: "tool_search_call", id: itemId, call_id: event.id, execution: "client", arguments: {}, status: "in_progress" }
|
|
@@ -1451,7 +1453,7 @@ function buildResponseJSONWithBudget(
|
|
|
1451
1453
|
modelId: string,
|
|
1452
1454
|
options?: {
|
|
1453
1455
|
hideThinkingSummary?: boolean;
|
|
1454
|
-
toolNsMap?: Map<string, { namespace: string; name: string }>;
|
|
1456
|
+
toolNsMap?: Map<string, { namespace: string; name: string; freeform?: true }>;
|
|
1455
1457
|
/** Request-visible tool names. When present, an upstream call outside this set fails closed. */
|
|
1456
1458
|
declaredToolNames?: ReadonlySet<string>;
|
|
1457
1459
|
/** Declared parameter schema per tool name; repairs integral-float integer args (#1611). */
|
|
@@ -1632,7 +1634,9 @@ function buildResponseJSONWithBudget(
|
|
|
1632
1634
|
const realName = mapped?.name ?? currentToolCallName;
|
|
1633
1635
|
const ns = mapped?.namespace;
|
|
1634
1636
|
const toolSearch = options?.toolSearchToolNames?.has(realName) ?? false;
|
|
1635
|
-
const freeform = !toolSearch && (
|
|
1637
|
+
const freeform = !toolSearch && (mapped
|
|
1638
|
+
? mapped.freeform === true
|
|
1639
|
+
: (options?.freeformToolNames?.has(realName) ?? false));
|
|
1636
1640
|
// #1611: same integral-float repair as the streaming path. Keyed by the wire name
|
|
1637
1641
|
// the request declared, which is the pre-namespace-mapping `currentToolCallName`.
|
|
1638
1642
|
const coercedArgs = coerceIntegerToolArguments(
|
|
@@ -1796,7 +1800,9 @@ function buildResponseJSONWithBudget(
|
|
|
1796
1800
|
const mapped = options?.toolNsMap?.get(currentToolCallName);
|
|
1797
1801
|
const realName = mapped?.name ?? currentToolCallName;
|
|
1798
1802
|
const toolSearch = options?.toolSearchToolNames?.has(realName) ?? false;
|
|
1799
|
-
const freeform = !toolSearch && (
|
|
1803
|
+
const freeform = !toolSearch && (mapped
|
|
1804
|
+
? mapped.freeform === true
|
|
1805
|
+
: (options?.freeformToolNames?.has(realName) ?? false));
|
|
1800
1806
|
if (!freeform && !toolSearch) {
|
|
1801
1807
|
flushToolCall("incomplete");
|
|
1802
1808
|
errorEvent = {
|
package/src/cli/doctor.ts
CHANGED
|
@@ -25,6 +25,11 @@ import { collectOrcaCodexHomeDiagnostic, resolveCodexHomeDir as resolveCodexHome
|
|
|
25
25
|
import { scanCodexAgentRolesWithTomlModelFallback } from "../codex/subagent-model-fallback";
|
|
26
26
|
import { findCodexOnPath, isWindowsInteropDir } from "../codex/shim";
|
|
27
27
|
import { countPendingOpencodexHistory } from "../codex/history-provider";
|
|
28
|
+
import {
|
|
29
|
+
inspectAbandonedResponseStateTemps,
|
|
30
|
+
reclaimAbandonedResponseStateTemps,
|
|
31
|
+
type ResponseStateTempRecoveryResult,
|
|
32
|
+
} from "../responses/state";
|
|
28
33
|
import {
|
|
29
34
|
CodexUserIdentityRefusal,
|
|
30
35
|
probeCodexCoordinatorNamespace,
|
|
@@ -678,6 +683,57 @@ export async function fetchServiceMemory(
|
|
|
678
683
|
|
|
679
684
|
const mb = (bytes: number): string => `${Math.round(bytes / (1024 * 1024))}MB`;
|
|
680
685
|
|
|
686
|
+
export const RECLAIM_RESPONSE_TEMPS_FLAG = "--reclaim-response-temps";
|
|
687
|
+
/** Matches the dry run's entry bound so report and reclaim agree on a large backlog. */
|
|
688
|
+
const RESPONSE_TEMP_RECLAIM_MAX_CLEANUPS = 4_096;
|
|
689
|
+
/** Names the subsystem: other components mint temps with the same shape and are not covered. */
|
|
690
|
+
const CLEAN_RESPONSE_TEMP_LINE = " ok No abandoned response-state temp files.";
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Render the abandoned-temp section (testable without console capture).
|
|
694
|
+
*
|
|
695
|
+
* Report is the DEFAULT and reclaim is opt-in: `doctor` is a diagnostic an operator runs
|
|
696
|
+
* to understand a machine, so deleting files as a side effect of asking a question is the
|
|
697
|
+
* wrong default even for cache files.
|
|
698
|
+
*
|
|
699
|
+
* Counts come from `eligible`/`eligibleBytes`, never `matched`: `matched` is incremented
|
|
700
|
+
* before the file-type, age, boot-floor, and liveness gates, so reporting it would tell an
|
|
701
|
+
* operator that live-pid temps and young temps are "abandoned".
|
|
702
|
+
*/
|
|
703
|
+
export function formatResponseTempLines(
|
|
704
|
+
result: ResponseStateTempRecoveryResult,
|
|
705
|
+
reclaimed: boolean,
|
|
706
|
+
): string[] {
|
|
707
|
+
if (reclaimed) {
|
|
708
|
+
if (result.removed === 0 && result.failed === 0) return [CLEAN_RESPONSE_TEMP_LINE];
|
|
709
|
+
const lines = [` ok Reclaimed ${result.removed} abandoned response-state temp file(s), ${mb(result.bytesRemoved)} freed.`];
|
|
710
|
+
if (result.failed > 0) {
|
|
711
|
+
// Never "retried automatically": this command exists for the operator whose proxy will
|
|
712
|
+
// NOT start, and in that state nothing retries anything.
|
|
713
|
+
lines.push(` !! ${result.failed} file(s) could not be removed (in use or locked). Retried on the next reclaim — automatically while the proxy runs, otherwise re-run this command.`);
|
|
714
|
+
}
|
|
715
|
+
// `truncated`, not `eligible > removed + failed`: outside a dry run every eligible entry
|
|
716
|
+
// is unlinked or failed on the same iteration it is counted, so those two are always
|
|
717
|
+
// equal and the comparison never fired. An operator with a backlog past the budget was
|
|
718
|
+
// told the reclaim had finished.
|
|
719
|
+
if (result.truncated) {
|
|
720
|
+
lines.push(" !! Cleanup budget reached; files remain. Run the command again to continue.");
|
|
721
|
+
}
|
|
722
|
+
return lines;
|
|
723
|
+
}
|
|
724
|
+
if (result.eligible === 0) return [CLEAN_RESPONSE_TEMP_LINE];
|
|
725
|
+
const lines = [
|
|
726
|
+
` !! ${result.eligible} abandoned response-state temp file(s), ${mb(result.eligibleBytes)} reclaimable.`,
|
|
727
|
+
" These are interrupted snapshot writes (continuation cache only) and are safe to remove.",
|
|
728
|
+
" Reclaim them with: ocx doctor --reclaim-response-temps",
|
|
729
|
+
];
|
|
730
|
+
// The dry run skips the cleanup budget but is still bounded by the entry cap, so a large
|
|
731
|
+
// enough backlog makes this a floor rather than a total. Say so instead of letting an
|
|
732
|
+
// operator size the problem from a truncated count.
|
|
733
|
+
if (result.truncated) lines.push(" Scan stopped at its entry budget; the real total is higher.");
|
|
734
|
+
return lines;
|
|
735
|
+
}
|
|
736
|
+
|
|
681
737
|
/** Render the doctor "Memory / runtime" section lines (testable without console capture). */
|
|
682
738
|
export function formatServiceMemoryLines(report: ServiceMemoryReport): string[] {
|
|
683
739
|
const lines: string[] = [];
|
|
@@ -805,6 +861,26 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
805
861
|
console.log(` ${row.exists ? "ok " : "-- "} ${row.label}: ${row.path}${flags ? ` (${flags})` : ""}`);
|
|
806
862
|
}
|
|
807
863
|
|
|
864
|
+
// Runs without the proxy on purpose: the worst accumulation happens when the proxy will
|
|
865
|
+
// not start, which is exactly when the in-process periodic reclaim never ticks.
|
|
866
|
+
const reclaimTemps = args.includes(RECLAIM_RESPONSE_TEMPS_FLAG);
|
|
867
|
+
console.log("\nResponse-state temp files");
|
|
868
|
+
// A typo must not silently degrade into "nothing to reclaim" — the operator would read the
|
|
869
|
+
// report as an answer to a question they never actually asked.
|
|
870
|
+
for (const arg of args) {
|
|
871
|
+
if (arg !== RECLAIM_RESPONSE_TEMPS_FLAG && /^--reclaim/.test(arg)) {
|
|
872
|
+
console.log(` !! Unrecognized flag ${arg}; did you mean ${RECLAIM_RESPONSE_TEMPS_FLAG}? Reporting only.`);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
for (const line of formatResponseTempLines(
|
|
876
|
+
// The reclaim budget matches the report budget: a report bounded by entries and a removal
|
|
877
|
+
// bounded by a smaller cleanup cap would tell an operator 816 and then silently free 512.
|
|
878
|
+
reclaimTemps
|
|
879
|
+
? reclaimAbandonedResponseStateTemps({ maxCleanups: RESPONSE_TEMP_RECLAIM_MAX_CLEANUPS })
|
|
880
|
+
: inspectAbandonedResponseStateTemps(),
|
|
881
|
+
reclaimTemps,
|
|
882
|
+
)) console.log(line);
|
|
883
|
+
|
|
808
884
|
const orcaHome = collectOrcaCodexHomeDiagnostic();
|
|
809
885
|
console.log("\nCodex app home targeting");
|
|
810
886
|
console.log(` ${orcaHome.mismatch ? "!! " : "ok "} Effective Codex home: ${orcaHome.effectiveCodexHome}`);
|
package/src/cli/help.ts
CHANGED
|
@@ -36,6 +36,8 @@ Usage:
|
|
|
36
36
|
Refresh Codex's model cache from the active catalog
|
|
37
37
|
ocx status Check proxy server status
|
|
38
38
|
ocx doctor Diagnose environment/network issues (WSL, proxy, ChatGPT reachability)
|
|
39
|
+
ocx doctor --reclaim-response-temps
|
|
40
|
+
Reclaim abandoned response-state temp files (works without a running proxy)
|
|
39
41
|
ocx debug <scope> provider/usage/injection/claude on|off|status|reset
|
|
40
42
|
ocx login <provider> OAuth or API-key provider login
|
|
41
43
|
ocx logout <provider> Remove a stored OAuth login
|
package/src/cli/models.ts
CHANGED
|
@@ -5,11 +5,11 @@ import { randomUUID } from "node:crypto";
|
|
|
5
5
|
import { createInterface } from "node:readline/promises";
|
|
6
6
|
import { syncModelsToCodex } from "../codex/sync";
|
|
7
7
|
import { hasOwnProvider, isValidProviderName, loadConfig, saveConfig } from "../config";
|
|
8
|
-
import { canonicalizeReasoningEfforts, isDeclaredReasoningEffort } from "../reasoning-effort";
|
|
8
|
+
import { canonicalizeReasoningEfforts, isDeclaredReasoningEffort, modelRecordValue } from "../reasoning-effort";
|
|
9
9
|
import { encodedModelIdCollides, routedSlug, slugEquals } from "../providers/slug-codec";
|
|
10
10
|
import { knownModelIdsForProvider } from "../router";
|
|
11
11
|
import { findLiveProxy } from "../server/proxy-liveness";
|
|
12
|
-
import type
|
|
12
|
+
import { modelInList, type OcxConfig, type OcxCustomModel } from "../types";
|
|
13
13
|
|
|
14
14
|
const ADD_USAGE = "Usage: ocx models add <provider> <modelId> [--display-name <name>] [--context-window <tokens>] [--modalities text,image,audio] [--reasoning-efforts <none,minimal,low,medium,high,xhigh,max,ultra>] [--default-reasoning-effort <level>]";
|
|
15
15
|
const REMOVE_USAGE = "Usage: ocx models remove <customId|provider/modelId> [--yes]";
|
|
@@ -98,15 +98,22 @@ function collectModels(config: OcxConfig, providerFilter?: string): ModelEntry[]
|
|
|
98
98
|
if (seen.has(model)) return;
|
|
99
99
|
seen.add(model);
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
// Resolve exactly as the runtime does, or this command reports capabilities the
|
|
102
|
+
// proxy will not honour: `isModelTextOnly` matches noVisionModels with modelInList
|
|
103
|
+
// and reads modelInputModalities with modelRecordValue, so a `gpt-oss` entry covers
|
|
104
|
+
// `gpt-oss:120b`. A bare lookup reported that model as unclassified on every field.
|
|
105
|
+
// noVisionModels is checked first because `isModelTextOnly` returns true on that
|
|
106
|
+
// match before it ever reads modelInputModalities: a `gpt-oss` noVision entry beats
|
|
107
|
+
// an exact `gpt-oss:120b` entry that lists "image", and the proxy rejects the image.
|
|
108
|
+
const noVision = modelInList(prov.noVisionModels, model);
|
|
109
|
+
const modalities = noVision ? ["text"] : (modelRecordValue(inputModalities, model) ?? null);
|
|
110
|
+
const efforts = modelRecordValue(reasoningEfforts, model) ?? prov.reasoningEfforts ?? null;
|
|
104
111
|
|
|
105
112
|
entries.push({
|
|
106
113
|
provider: provName,
|
|
107
114
|
model,
|
|
108
115
|
isDefault,
|
|
109
|
-
contextWindow: contextWindows
|
|
116
|
+
contextWindow: modelRecordValue(contextWindows, model) ?? globalContext,
|
|
110
117
|
inputModalities: modalities,
|
|
111
118
|
reasoningEfforts: efforts,
|
|
112
119
|
});
|