@bitkyc08/opencodex 2.7.40 → 2.7.41
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/README.md +63 -12
- package/assets/claude-code-models.gif +0 -0
- package/gui/dist/assets/index-B2J4t3te.css +1 -0
- package/gui/dist/assets/index-BmvM6wRb.js +65 -0
- package/gui/dist/index.html +2 -2
- package/package.json +3 -2
- package/src/adapters/google.ts +48 -9
- package/src/adapters/kiro-events.ts +15 -3
- package/src/adapters/kiro.ts +292 -28
- package/src/adapters/openai-chat.ts +63 -15
- package/src/adapters/openai-responses.ts +18 -0
- package/src/bridge.ts +76 -21
- package/src/chat/outbound.ts +66 -34
- package/src/claude/auth-detect.ts +229 -0
- package/src/claude/auth-mode-migration.ts +32 -0
- package/src/claude/auth-mode.ts +62 -0
- package/src/claude/desktop-3p-guard.ts +35 -0
- package/src/claude/desktop-3p.ts +121 -21
- package/src/claude/desktop-health.ts +26 -0
- package/src/claude/desktop-profile.ts +263 -0
- package/src/claude/inbound-debug.ts +4 -0
- package/src/claude/model-info.ts +9 -3
- package/src/cli/account-extended.ts +34 -0
- package/src/cli/account.ts +3 -1
- package/src/cli/claude-desktop.ts +152 -0
- package/src/cli/claude.ts +37 -3
- package/src/cli/doctor.ts +187 -6
- package/src/cli/help.ts +13 -1
- package/src/cli/index.ts +151 -57
- package/src/cli/status-oauth.ts +68 -0
- package/src/cli/status.ts +4 -0
- package/src/codex/account-lifecycle.ts +31 -0
- package/src/codex/auth-api.ts +133 -26
- package/src/codex/auth-collision.ts +55 -11
- package/src/codex/auth-context.ts +50 -6
- package/src/codex/catalog/provider-fetch.ts +6 -2
- package/src/codex/home.ts +61 -1
- package/src/codex/inject.ts +28 -66
- package/src/codex/injected-marker.ts +72 -0
- package/src/codex/journal.ts +39 -3
- package/src/codex/main-account-cache.ts +25 -0
- package/src/codex/model-cache.ts +20 -1
- package/src/codex/paths.ts +5 -0
- package/src/codex/routing.ts +138 -11
- package/src/codex/subagent-model-fallback.ts +455 -0
- package/src/codex/sync.ts +17 -0
- package/src/combos/failover.ts +10 -2
- package/src/combos/index.ts +1 -0
- package/src/combos/types.ts +9 -0
- package/src/config.ts +117 -0
- package/src/grok/inject.ts +339 -0
- package/src/grok/status.ts +88 -0
- package/src/grok/sync.ts +66 -0
- package/src/lib/destination-policy.ts +13 -0
- package/src/lib/errors.ts +59 -0
- package/src/lib/privacy.ts +9 -0
- package/src/lib/process-control.ts +57 -4
- package/src/oauth/health.ts +375 -0
- package/src/oauth/index.ts +69 -22
- package/src/oauth/kiro.ts +5 -3
- package/src/oauth/log.ts +48 -0
- package/src/oauth/store.ts +55 -6
- package/src/providers/alibaba-region-backup.ts +75 -0
- package/src/providers/alibaba-region-migration.ts +143 -0
- package/src/providers/alibaba-region-startup.ts +36 -0
- package/src/providers/api-keys.ts +5 -5
- package/src/providers/derive.ts +22 -1
- package/src/providers/free-directory.ts +181 -0
- package/src/providers/key-failover.ts +2 -2
- package/src/providers/kiro-models.ts +3 -2
- package/src/providers/openai-tiers.ts +1 -1
- package/src/providers/provider-id-rewrite.ts +150 -0
- package/src/providers/registry.ts +49 -2
- package/src/responses/parser.ts +49 -20
- package/src/responses/state.ts +156 -2
- package/src/router.ts +73 -5
- package/src/server/chat-completions.ts +78 -22
- package/src/server/claude-messages.ts +8 -0
- package/src/server/images.ts +2 -1
- package/src/server/index.ts +50 -30
- package/src/server/live.ts +2 -1
- package/src/server/management/agent-settings-routes.ts +337 -18
- package/src/server/management/api-access.ts +141 -0
- package/src/server/management/combo-routes.ts +3 -3
- package/src/server/management/config-routes.ts +4 -4
- package/src/server/management/logs-usage-routes.ts +64 -4
- package/src/server/management/model-routes.ts +114 -8
- package/src/server/management/oauth-account-routes.ts +31 -5
- package/src/server/management/provider-routes.ts +42 -11
- package/src/server/management/shared.ts +81 -2
- package/src/server/management/system-routes.ts +6 -1
- package/src/server/management-api.ts +24 -7
- package/src/server/port-reclaim.ts +261 -0
- package/src/server/request-log.ts +11 -5
- package/src/server/responses/collaboration.ts +11 -6
- package/src/server/responses/compact.ts +2 -1
- package/src/server/responses/core.ts +358 -155
- package/src/server/responses/passthrough-error.ts +53 -0
- package/src/server/search.ts +2 -1
- package/src/server/system-env.ts +23 -6
- package/src/server/windows-tcp-drop.ts +174 -0
- package/src/service.ts +43 -3
- package/src/types.ts +74 -6
- package/src/update/index.ts +30 -19
- package/src/update/job.ts +30 -15
- package/src/usage/log.ts +158 -2
- package/src/usage/summary.ts +8 -4
- package/src/web-search/loop.ts +4 -1
- package/gui/dist/assets/index-CMip1DzF.css +0 -1
- package/gui/dist/assets/index-cydcmbzC.js +0 -52
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
multiAgentGuidanceEnabled,
|
|
11
11
|
providerBaseUrlConfigError,
|
|
12
12
|
providerHeadersConfigError,
|
|
13
|
-
|
|
13
|
+
saveConfigPreservingClaudeCode,
|
|
14
14
|
} from "../config";
|
|
15
15
|
import {
|
|
16
16
|
clearLoginState,
|
|
@@ -48,7 +48,8 @@ import {
|
|
|
48
48
|
setDebugSettings,
|
|
49
49
|
type DebugFlag,
|
|
50
50
|
} from "../lib/debug-settings";
|
|
51
|
-
import type { OcxClaudeCodeConfig, OcxConfig, OcxCustomModel, OcxProviderConfig } from "../types";
|
|
51
|
+
import type { OcxClaudeCodeConfig, OcxClaudeDesktopProfile, OcxConfig, OcxCustomModel, OcxProviderConfig } from "../types";
|
|
52
|
+
import type { DesktopProfileModel } from "../claude/desktop-profile";
|
|
52
53
|
import { drainAndShutdown } from "./lifecycle";
|
|
53
54
|
import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "./request-log";
|
|
54
55
|
import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../usage/cost";
|
|
@@ -135,16 +136,32 @@ export async function handleManagementAPI(req: Request, url: URL, config: OcxCon
|
|
|
135
136
|
|
|
136
137
|
if (url.pathname === "/api/stop" && req.method === "POST") {
|
|
137
138
|
const { restoreNativeCodex } = await import("../codex/inject");
|
|
138
|
-
const { stopServiceIfInstalled } = await import("../service");
|
|
139
|
-
|
|
139
|
+
const { stopServiceIfInstalled, isServiceOwnershipError } = await import("../service");
|
|
140
|
+
try {
|
|
141
|
+
stopServiceIfInstalled();
|
|
142
|
+
} catch (err) {
|
|
143
|
+
if (isServiceOwnershipError(err)) {
|
|
144
|
+
// The installed service belongs to another CODEX_HOME/OPENCODEX_HOME: it would respawn
|
|
145
|
+
// this proxy immediately, and its shared config is not ours to tear down. Refuse the
|
|
146
|
+
// stop instead of half-performing it. 409, not 500 — the request is well-formed.
|
|
147
|
+
return jsonResponse({ success: false, message: err.message }, 409, req, config);
|
|
148
|
+
}
|
|
149
|
+
throw err;
|
|
150
|
+
}
|
|
140
151
|
const restore = restoreNativeCodex();
|
|
152
|
+
// Both managed configs come down together on an explicit teardown. The daemon's own
|
|
153
|
+
// syncCleanup skips this when OCX_SERVICE is set (so a crash/respawn keeps the fence),
|
|
154
|
+
// which is exactly why an intentional stop has to do it here.
|
|
155
|
+
const { stripGrokConfig } = await import("../grok/inject");
|
|
156
|
+
const grok = stripGrokConfig();
|
|
141
157
|
setTimeout(async () => {
|
|
142
158
|
await drainAndShutdown(undefined, config.shutdownTimeoutMs ?? 5000);
|
|
143
159
|
process.exit(0);
|
|
144
160
|
}, 200);
|
|
161
|
+
const grokNote = grok.ok ? "" : ` Grok config cleanup failed: ${grok.message}`;
|
|
145
162
|
return jsonResponse(restore.success
|
|
146
|
-
? { success: true, message:
|
|
147
|
-
: { success: false, message: `Proxy stopping, but native Codex restore failed: ${restore.message}. Run \`ocx restore
|
|
163
|
+
? { success: true, message: `Proxy stopping, native Codex restored.${grokNote}` }
|
|
164
|
+
: { success: false, message: `Proxy stopping, but native Codex restore failed: ${restore.message}. Run \`ocx restore\`.${grokNote}` });
|
|
148
165
|
}
|
|
149
166
|
|
|
150
167
|
if (url.pathname.startsWith("/api/codex-auth/")) {
|
|
@@ -156,4 +173,4 @@ export async function handleManagementAPI(req: Request, url: URL, config: OcxCon
|
|
|
156
173
|
}
|
|
157
174
|
|
|
158
175
|
|
|
159
|
-
export { fetchAllModels } from "./management/shared";
|
|
176
|
+
export { buildClaudeDesktopState, fetchAllModels } from "./management/shared";
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reclaim a listen port after stop/update so restart can stay on the configured
|
|
3
|
+
* port instead of hopping to an ephemeral one (Windows CLOSE_WAIT / leftover ocx).
|
|
4
|
+
*
|
|
5
|
+
* Killing is never the default: a process may be killed only when the caller
|
|
6
|
+
* supplies a non-empty explicit PID allowlist for a process it just stopped.
|
|
7
|
+
*/
|
|
8
|
+
import { execFileSync } from "node:child_process";
|
|
9
|
+
import { verifyPidIdentity } from "../config";
|
|
10
|
+
import { isProcessAlive, killProxy } from "../lib/process-control";
|
|
11
|
+
import { isPortAvailable, type WaitForPortOptions } from "./ports";
|
|
12
|
+
import { dropWindowsTcpRowsForLocalPort } from "./windows-tcp-drop";
|
|
13
|
+
|
|
14
|
+
export type ListenPidScan =
|
|
15
|
+
| { ok: true; pids: number[] }
|
|
16
|
+
| { ok: false; error?: string };
|
|
17
|
+
|
|
18
|
+
export type ReclaimListenPortOptions = WaitForPortOptions & {
|
|
19
|
+
/**
|
|
20
|
+
* When true AND `onlyKillPids` is a non-empty allowlist, those PIDs may be
|
|
21
|
+
* killed after revalidation. Default false — never kill without an allowlist.
|
|
22
|
+
*/
|
|
23
|
+
killOcxHolders?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Explicit PIDs the caller just stopped / hard-killed. An omitted or empty
|
|
26
|
+
* list means no process may be killed.
|
|
27
|
+
*/
|
|
28
|
+
onlyKillPids?: number[];
|
|
29
|
+
/**
|
|
30
|
+
* On Windows, force-delete IPv4 TCP rows for this local port via SetTcpEntry.
|
|
31
|
+
* Default true on win32. Never kills foreign processes, never runs while a
|
|
32
|
+
* live foreign / protected ocx listener owns the port, and never runs when
|
|
33
|
+
* the listener scan failed.
|
|
34
|
+
*/
|
|
35
|
+
dropTcpRows?: boolean;
|
|
36
|
+
/** How often to scan for listen PIDs / attempt TCB drop (ms). Default 500. */
|
|
37
|
+
scanIntervalMs?: number;
|
|
38
|
+
listListenPidsFn?: (port: number) => ListenPidScan | number[];
|
|
39
|
+
isAliveFn?: (pid: number) => boolean;
|
|
40
|
+
verifyOcxFn?: (pid: number) => number | null;
|
|
41
|
+
killFn?: (pid: number) => void;
|
|
42
|
+
dropTcpFn?: (port: number) => number | { dropped: number; skippedIpv6: number };
|
|
43
|
+
isAvailableFn?: (port: number, hostname?: string) => Promise<boolean>;
|
|
44
|
+
sleepMs?: (ms: number) => Promise<void>;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Parse `netstat -ano` (Windows) / `netstat -anlp` listen lines for a port.
|
|
49
|
+
* Exported for unit tests.
|
|
50
|
+
*/
|
|
51
|
+
export function parseListenPidsFromNetstat(output: string, port: number): number[] {
|
|
52
|
+
const pids = new Set<number>();
|
|
53
|
+
const portSuffix = `:${port}`;
|
|
54
|
+
for (const rawLine of output.split(/\r?\n/)) {
|
|
55
|
+
const line = rawLine.trim();
|
|
56
|
+
if (!/^TCP\b/i.test(line) && !/^tcp\b/i.test(line)) continue;
|
|
57
|
+
const parts = line.split(/\s+/);
|
|
58
|
+
// Prefer the first address token that ends with :port (local), not a later foreign one.
|
|
59
|
+
const localIdx = parts.findIndex(part => part.endsWith(portSuffix) || part.endsWith(`]:${port}`));
|
|
60
|
+
if (localIdx < 0) continue;
|
|
61
|
+
const foreign = parts[localIdx + 1] ?? "";
|
|
62
|
+
// Locale-safe listen detection: English LISTEN*, or unbound foreign wildcard
|
|
63
|
+
// (German ABHÖREN still shows 0.0.0.0:0 / *:*).
|
|
64
|
+
const listenWord = /\bLISTEN/i.test(line);
|
|
65
|
+
const wildcardForeign = /^(0\.0\.0\.0|::|\*|\[::\]):0$/.test(foreign) || foreign === "*:*";
|
|
66
|
+
if (!listenWord && !wildcardForeign) continue;
|
|
67
|
+
const last = parts[parts.length - 1] ?? "";
|
|
68
|
+
const winPid = /^\d+$/.test(last) ? Number(last) : NaN;
|
|
69
|
+
const unixPid = /^(\d+)(?:\/\S*)?$/.exec(last);
|
|
70
|
+
const pid = Number.isSafeInteger(winPid) && winPid > 0
|
|
71
|
+
? winPid
|
|
72
|
+
: unixPid
|
|
73
|
+
? Number(unixPid[1])
|
|
74
|
+
: NaN;
|
|
75
|
+
if (Number.isSafeInteger(pid) && pid > 0) pids.add(pid);
|
|
76
|
+
}
|
|
77
|
+
return [...pids];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function normalizeListenPidScan(result: ListenPidScan | number[]): ListenPidScan {
|
|
81
|
+
if (Array.isArray(result)) return { ok: true, pids: result };
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Prefer English netstat states; fall back to the UI-locale table. */
|
|
86
|
+
function readWindowsNetstatAno(): string {
|
|
87
|
+
const netstat = `${process.env.SystemRoot ?? "C:\\Windows"}\\System32\\netstat.exe`;
|
|
88
|
+
const cmd = `${process.env.SystemRoot ?? "C:\\Windows"}\\System32\\cmd.exe`;
|
|
89
|
+
try {
|
|
90
|
+
// chcp 437 forces English LISTENING/ESTABLISHED labels on localized Windows.
|
|
91
|
+
return execFileSync(cmd, ["/d", "/c", `chcp 437>nul & "${netstat}" -ano -p tcp`], {
|
|
92
|
+
encoding: "utf-8",
|
|
93
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
94
|
+
timeout: 5000,
|
|
95
|
+
windowsHide: true,
|
|
96
|
+
});
|
|
97
|
+
} catch {
|
|
98
|
+
return execFileSync(netstat, ["-ano", "-p", "tcp"], {
|
|
99
|
+
encoding: "utf-8",
|
|
100
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
101
|
+
timeout: 4000,
|
|
102
|
+
windowsHide: true,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Scan for PIDs currently LISTENing on `port`.
|
|
109
|
+
* Distinguishes probe failure (`ok: false`) from a successful empty result.
|
|
110
|
+
*/
|
|
111
|
+
export function scanListenPids(port: number): ListenPidScan {
|
|
112
|
+
if (!Number.isFinite(port) || port <= 0 || port > 65535) {
|
|
113
|
+
return { ok: false, error: "invalid port" };
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
if (process.platform === "win32") {
|
|
117
|
+
return { ok: true, pids: parseListenPidsFromNetstat(readWindowsNetstatAno(), port) };
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const output = execFileSync("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], {
|
|
121
|
+
encoding: "utf-8",
|
|
122
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
123
|
+
timeout: 3000,
|
|
124
|
+
});
|
|
125
|
+
return {
|
|
126
|
+
ok: true,
|
|
127
|
+
pids: output
|
|
128
|
+
.split(/\r?\n/)
|
|
129
|
+
.map(line => Number(line.trim()))
|
|
130
|
+
.filter(pid => Number.isSafeInteger(pid) && pid > 0),
|
|
131
|
+
};
|
|
132
|
+
} catch (lsofErr) {
|
|
133
|
+
try {
|
|
134
|
+
const output = execFileSync("netstat", ["-anlp"], {
|
|
135
|
+
encoding: "utf-8",
|
|
136
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
137
|
+
timeout: 3000,
|
|
138
|
+
});
|
|
139
|
+
return { ok: true, pids: parseListenPidsFromNetstat(output, Math.trunc(port)) };
|
|
140
|
+
} catch (netstatErr) {
|
|
141
|
+
return {
|
|
142
|
+
ok: false,
|
|
143
|
+
error: `lsof/netstat unavailable: ${String(lsofErr)} / ${String(netstatErr)}`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
} catch (error) {
|
|
148
|
+
return { ok: false, error: String(error) };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Best-effort PIDs currently LISTENing on `port`. Empty on probe failure. */
|
|
153
|
+
export function listListenPids(port: number): number[] {
|
|
154
|
+
const scan = scanListenPids(port);
|
|
155
|
+
return scan.ok ? scan.pids : [];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Wait until `port` can bind.
|
|
160
|
+
* Never kills a process unless `killOcxHolders === true` and `onlyKillPids` is a
|
|
161
|
+
* non-empty allowlist of PIDs the caller itself just stopped — then revalidates
|
|
162
|
+
* immediately before each kill.
|
|
163
|
+
* Never kills foreign processes. Never drops TCP rows while a live foreign or
|
|
164
|
+
* protected ocx listener owns the port, or when the listener scan failed.
|
|
165
|
+
*/
|
|
166
|
+
export async function reclaimListenPort(
|
|
167
|
+
port: number,
|
|
168
|
+
hostname = "127.0.0.1",
|
|
169
|
+
opts: ReclaimListenPortOptions = {},
|
|
170
|
+
): Promise<boolean> {
|
|
171
|
+
const timeoutMs = opts.timeoutMs ?? 30_000;
|
|
172
|
+
const intervalMs = opts.intervalMs ?? 100;
|
|
173
|
+
const scanIntervalMs = opts.scanIntervalMs ?? 500;
|
|
174
|
+
const allowedKillPids = new Set(
|
|
175
|
+
(opts.onlyKillPids ?? []).filter(pid => Number.isSafeInteger(pid) && pid > 0),
|
|
176
|
+
);
|
|
177
|
+
const mayKill = opts.killOcxHolders === true && allowedKillPids.size > 0;
|
|
178
|
+
const dropTcpRows = opts.dropTcpRows ?? process.platform === "win32";
|
|
179
|
+
const listFn = opts.listListenPidsFn ?? scanListenPids;
|
|
180
|
+
const isAliveFn = opts.isAliveFn ?? isProcessAlive;
|
|
181
|
+
const verifyOcxFn = opts.verifyOcxFn ?? verifyPidIdentity;
|
|
182
|
+
const killFn = opts.killFn ?? killProxy;
|
|
183
|
+
const dropTcpFn = opts.dropTcpFn ?? dropWindowsTcpRowsForLocalPort;
|
|
184
|
+
const isAvailableFn = opts.isAvailableFn ?? isPortAvailable;
|
|
185
|
+
const sleep = opts.sleepMs ?? ((ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms)));
|
|
186
|
+
|
|
187
|
+
const deadline = Date.now() + timeoutMs;
|
|
188
|
+
let lastScan = 0;
|
|
189
|
+
const killed = new Set<number>();
|
|
190
|
+
|
|
191
|
+
for (;;) {
|
|
192
|
+
if (await isAvailableFn(port, hostname)) return true;
|
|
193
|
+
if (Date.now() >= deadline) return false;
|
|
194
|
+
|
|
195
|
+
if (Date.now() - lastScan >= scanIntervalMs) {
|
|
196
|
+
lastScan = Date.now();
|
|
197
|
+
|
|
198
|
+
const scan = normalizeListenPidScan(listFn(port));
|
|
199
|
+
if (!scan.ok) {
|
|
200
|
+
// Failed probe ≠ empty listeners: do not kill and do not reset TCP rows.
|
|
201
|
+
await sleep(intervalMs);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let foreignLive = false;
|
|
206
|
+
let protectedOcxListener = false;
|
|
207
|
+
|
|
208
|
+
for (const pid of scan.pids) {
|
|
209
|
+
if (pid === process.pid) continue;
|
|
210
|
+
if (!isAliveFn(pid)) continue; // Windows may still list a dead owner briefly
|
|
211
|
+
const isOcx = verifyOcxFn(pid) === pid;
|
|
212
|
+
if (!isOcx) {
|
|
213
|
+
foreignLive = true;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (!mayKill || !allowedKillPids.has(pid)) {
|
|
217
|
+
// Healthy / intentional ocx proxy — never steal its port.
|
|
218
|
+
protectedOcxListener = true;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (!killed.has(pid)) {
|
|
222
|
+
// Revalidate immediately before termination.
|
|
223
|
+
if (isAliveFn(pid) && verifyOcxFn(pid) === pid && allowedKillPids.has(pid)) {
|
|
224
|
+
try {
|
|
225
|
+
killFn(pid);
|
|
226
|
+
killed.add(pid);
|
|
227
|
+
} catch {
|
|
228
|
+
// Kill failed: keep waiting and never reset this listener's TCP rows.
|
|
229
|
+
protectedOcxListener = true;
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
// Revalidation failed while the allowlisted listener is still listed live.
|
|
233
|
+
protectedOcxListener = true;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// Only reset TCP rows after confirmed process death.
|
|
237
|
+
if (isAliveFn(pid)) protectedOcxListener = true;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (foreignLive || protectedOcxListener) {
|
|
241
|
+
// Foreign app or an unprotected live ocx listener owns the port: never
|
|
242
|
+
// SetTcpEntry-reset their sockets, and fail reclaim once the deadline hits.
|
|
243
|
+
await sleep(intervalMs);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// After hard-kill, browsers often keep ESTABLISHED/CLOSE_WAIT to the dead listener.
|
|
248
|
+
// Reset those IPv4 TCBs (and ghost LISTEN rows) so the configured port can bind again —
|
|
249
|
+
// without killing the browser process. Only safe when no live foreign/protected listener remains.
|
|
250
|
+
if (dropTcpRows) {
|
|
251
|
+
try {
|
|
252
|
+
dropTcpFn(port);
|
|
253
|
+
} catch {
|
|
254
|
+
/* access denied / unsupported — keep waiting */
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
await sleep(intervalMs);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -11,6 +11,7 @@ import type { OcxUsage } from "../types";
|
|
|
11
11
|
import { redactSecretString } from "../lib/redact";
|
|
12
12
|
import {
|
|
13
13
|
appendUsageEntry,
|
|
14
|
+
isKnownUsageSurface,
|
|
14
15
|
readRecentUsageEntries,
|
|
15
16
|
usageForFinalLog,
|
|
16
17
|
usageStatusForFinalLog,
|
|
@@ -33,7 +34,7 @@ export interface RequestLogContext {
|
|
|
33
34
|
provider: string;
|
|
34
35
|
/** TTFT: ms from request start to the first non-empty model output delta (WP4, devlog 040). */
|
|
35
36
|
firstOutputMs?: number;
|
|
36
|
-
surface?: "claude";
|
|
37
|
+
surface?: "claude" | "claude-desktop" | "grok";
|
|
37
38
|
requestedModel?: string;
|
|
38
39
|
/** Internal structural combo identity; omitted from RequestLogEntry/JSONL. */
|
|
39
40
|
comboId?: string;
|
|
@@ -58,6 +59,11 @@ export interface RequestLogContext {
|
|
|
58
59
|
/** Route adapter type ("cursor"/"kiro"/"anthropic"/…): drives estimated-usage detection
|
|
59
60
|
* independent of the user-chosen provider NAME (devlog 130 B2). */
|
|
60
61
|
providerAdapter?: string;
|
|
62
|
+
/** Set when the bridge reported raw adapter usage via onUsage: the bridged wire now always
|
|
63
|
+
* carries synthetic zero-default token-detail objects (strict-client normalization, see
|
|
64
|
+
* responsesUsage in src/bridge.ts), so SSE/JSON re-parsing must not overwrite the raw
|
|
65
|
+
* provenance — a synthetic cached_tokens:0 is NOT a measured cache read. */
|
|
66
|
+
usageFromBridge?: boolean;
|
|
61
67
|
/** Secret-redacted upstream error reason (e.g. the granular Cursor "rate limit exceeded…"
|
|
62
68
|
* message) extracted from a `response.failed` SSE payload or non-streaming error body, so the
|
|
63
69
|
* request log / GUI shows the actual upstream failure rather than only the HTTP-mapped code. */
|
|
@@ -76,7 +82,7 @@ export interface RequestLogEntry {
|
|
|
76
82
|
provider: string;
|
|
77
83
|
/** TTFT: ms from request start to the first non-empty model output delta; unset for non-streaming/tool-only. */
|
|
78
84
|
firstOutputMs?: number;
|
|
79
|
-
surface?: "claude";
|
|
85
|
+
surface?: "claude" | "claude-desktop" | "grok";
|
|
80
86
|
requestedModel?: string;
|
|
81
87
|
requestedEffort?: string;
|
|
82
88
|
requestedServiceTier?: string;
|
|
@@ -139,7 +145,7 @@ export function requestLogEntryFromPersistedUsage(entry: PersistedUsageEntry): R
|
|
|
139
145
|
model: entry.model,
|
|
140
146
|
provider: entry.provider,
|
|
141
147
|
...(entry.firstOutputMs !== undefined ? { firstOutputMs: entry.firstOutputMs } : {}),
|
|
142
|
-
...(entry.surface
|
|
148
|
+
...(isKnownUsageSurface(entry.surface) ? { surface: entry.surface } : {}),
|
|
143
149
|
...(entry.requestedModel ? { requestedModel: entry.requestedModel } : {}),
|
|
144
150
|
...(entry.requestedEffort ? { requestedEffort: entry.requestedEffort } : {}),
|
|
145
151
|
...(entry.requestedServiceTier ? { requestedServiceTier: entry.requestedServiceTier } : {}),
|
|
@@ -215,7 +221,7 @@ export function addRequestLog(entry: RequestLogEntry) {
|
|
|
215
221
|
timestamp: entry.timestamp,
|
|
216
222
|
provider: entry.provider,
|
|
217
223
|
model: entry.model,
|
|
218
|
-
...(entry.surface
|
|
224
|
+
...(isKnownUsageSurface(entry.surface) ? { surface: entry.surface } : {}),
|
|
219
225
|
...(entry.resolvedModel ? { resolvedModel: entry.resolvedModel } : {}),
|
|
220
226
|
...(entry.requestedModel ? { requestedModel: entry.requestedModel } : {}),
|
|
221
227
|
...(entry.requestedEffort ? { requestedEffort: entry.requestedEffort } : {}),
|
|
@@ -338,7 +344,7 @@ export function applyResponseLogMetadata(logCtx: RequestLogContext, payload: unk
|
|
|
338
344
|
const serviceTier = (source as { service_tier?: unknown }).service_tier;
|
|
339
345
|
if (typeof serviceTier === "string" && serviceTier.trim()) logCtx.responseServiceTier = serviceTier;
|
|
340
346
|
const usage = usageFromResponsesPayload((source as { usage?: unknown }).usage);
|
|
341
|
-
if (usage) {
|
|
347
|
+
if (usage && !logCtx.usageFromBridge) {
|
|
342
348
|
logCtx.usage = usage;
|
|
343
349
|
if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
|
|
344
350
|
}
|
|
@@ -61,6 +61,7 @@ import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } f
|
|
|
61
61
|
import { listOpenAiForwardSidecarCandidates, resolveFirstUsableOpenAiSidecar, type ResolvedOpenAiForwardSidecar } from "../../providers/openai-sidecar";
|
|
62
62
|
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
63
63
|
import { slugsEquivalent } from "../../providers/slug-codec";
|
|
64
|
+
import { subagentFallbackGuidanceText } from "../../codex/subagent-model-fallback";
|
|
64
65
|
import { applyOpenAiVirtualModel, resolveOpenAiCompactModel } from "../../providers/openai-virtual-models";
|
|
65
66
|
import { isUsageDebugEnabled } from "../../usage/debug";
|
|
66
67
|
import { readJsonRequestBody, DecompressedBodyTooLargeError, UnsupportedContentEncodingError } from "../request-decompress";
|
|
@@ -160,6 +161,7 @@ export interface MultiAgentGuidanceOptions {
|
|
|
160
161
|
injectionModel?: string;
|
|
161
162
|
injectionEffort?: string;
|
|
162
163
|
subagentModels?: string[];
|
|
164
|
+
subagentModelFallback?: string[];
|
|
163
165
|
injectionPrompt?: string;
|
|
164
166
|
}
|
|
165
167
|
|
|
@@ -194,6 +196,7 @@ export async function multiAgentGuidanceText(
|
|
|
194
196
|
injectionModel,
|
|
195
197
|
injectionEffort,
|
|
196
198
|
subagentModels,
|
|
199
|
+
subagentModelFallback,
|
|
197
200
|
injectionPrompt,
|
|
198
201
|
} = options;
|
|
199
202
|
const surface = collabSurface(parsed);
|
|
@@ -222,11 +225,12 @@ export async function multiAgentGuidanceText(
|
|
|
222
225
|
.map(item => `${item.configured}:${item.reason}`)
|
|
223
226
|
.join(", ")}`);
|
|
224
227
|
}
|
|
225
|
-
|
|
228
|
+
const fallbackGuidance = subagentFallbackGuidanceText({ subagentModelFallback } as OcxConfig);
|
|
229
|
+
if (!injectionModel && roster === "" && fallbackGuidance === "") return null;
|
|
226
230
|
if (injectionPrompt) {
|
|
227
|
-
return `<multi_agent_mode>${applyInjectionPlaceholders(injectionPrompt, injectionModel, injectionEffort, roster)}</multi_agent_mode>`;
|
|
231
|
+
return `<multi_agent_mode>${applyInjectionPlaceholders(injectionPrompt, injectionModel, injectionEffort, roster, fallbackGuidance)}</multi_agent_mode>`;
|
|
228
232
|
}
|
|
229
|
-
if (!preferred && roster === "") return null;
|
|
233
|
+
if (!preferred && roster === "" && fallbackGuidance === "") return null;
|
|
230
234
|
let text = "When the active spawn_agent tool supports optional \"model\" or \"reasoning_effort\" overrides, "
|
|
231
235
|
+ "use only models listed for this collaboration surface. "
|
|
232
236
|
+ "When setting either override, set fork_turns to \"none\" "
|
|
@@ -237,6 +241,7 @@ export async function multiAgentGuidanceText(
|
|
|
237
241
|
+ (injectionEffort ? `, reasoning_effort "${injectionEffort}"` : "")
|
|
238
242
|
+ " — use it unless the user names another.";
|
|
239
243
|
}
|
|
244
|
+
text += fallbackGuidance;
|
|
240
245
|
text += roster;
|
|
241
246
|
if (text.length > V2_GUIDANCE_CHAR_BUDGET) {
|
|
242
247
|
// Roster is the only unbounded part — drop it before breaking the budget.
|
|
@@ -256,11 +261,12 @@ export async function multiAgentGuidanceText(
|
|
|
256
261
|
|
|
257
262
|
export const V2_GUIDANCE_CHAR_BUDGET = 700;
|
|
258
263
|
|
|
259
|
-
export function applyInjectionPlaceholders(prompt: string, model?: string, effort?: string, roster?: string): string {
|
|
264
|
+
export function applyInjectionPlaceholders(prompt: string, model?: string, effort?: string, roster?: string, fallback?: string): string {
|
|
260
265
|
return prompt
|
|
261
266
|
.replaceAll("{{model}}", model ?? "")
|
|
262
267
|
.replaceAll("{{effort}}", effort ?? "")
|
|
263
|
-
.replaceAll("{{roster}}", roster ?? "")
|
|
268
|
+
.replaceAll("{{roster}}", roster ?? "")
|
|
269
|
+
.replaceAll("{{fallback}}", fallback ?? "");
|
|
264
270
|
}
|
|
265
271
|
|
|
266
272
|
|
|
@@ -314,4 +320,3 @@ export function injectDeveloperMessage(parsed: OcxParsedRequest, text: string):
|
|
|
314
320
|
}
|
|
315
321
|
}
|
|
316
322
|
}
|
|
317
|
-
|
|
@@ -42,6 +42,7 @@ import { createAdapterEventQueue, preflightAdapterEvents } from "../../adapters/
|
|
|
42
42
|
import {
|
|
43
43
|
applyCodexAuthContextToProvider,
|
|
44
44
|
CodexAccountCooldownError,
|
|
45
|
+
cooldownErrorResponse,
|
|
45
46
|
CodexAuthContextError,
|
|
46
47
|
CodexDirectAuthenticationError,
|
|
47
48
|
CodexPoolAuthenticationError,
|
|
@@ -231,7 +232,7 @@ export async function handleResponsesCompact(
|
|
|
231
232
|
}
|
|
232
233
|
} catch (err) {
|
|
233
234
|
if (err instanceof CodexAccountCooldownError) {
|
|
234
|
-
return
|
|
235
|
+
return cooldownErrorResponse(err);
|
|
235
236
|
}
|
|
236
237
|
if (err instanceof CodexThreadAffinityExpiredError) {
|
|
237
238
|
return formatErrorResponse(409, "invalid_request_error", "Codex thread account affinity expired; start a new session");
|