@bitkyc08/opencodex 2.48.0-preview.20260908 → 2.49.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/README.md +8 -5
- package/SPONSORS.md +1 -1
- package/assets/sponsors/orcarouter.png +0 -0
- package/assets/sponsors/packycode.png +0 -0
- package/gui/dist/assets/index-BoBRSehJ.css +1 -0
- package/gui/dist/assets/index-BtyONQrZ.js +115 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/packycode.svg +19 -0
- package/gui/dist/provider-icons/qoder.svg +5 -0
- package/package.json +5 -3
- package/src/adapters/anthropic.ts +31 -16
- package/src/adapters/codebuddy/adapter.ts +85 -0
- package/src/adapters/codebuddy/profiles.ts +52 -0
- package/src/adapters/coding-agent/profile.ts +100 -0
- package/src/adapters/coding-agent/protocol.ts +463 -0
- package/src/adapters/coding-agent/turn.ts +353 -0
- package/src/adapters/google.ts +15 -11
- package/src/adapters/mimo-free.ts +3 -0
- package/src/adapters/openai-chat.ts +2 -2
- package/src/adapters/openai-responses.ts +18 -11
- package/src/adapters/qoder/adapter.ts +70 -0
- package/src/adapters/qoder/live-models.ts +89 -0
- package/src/adapters/qoder/profiles.ts +36 -0
- package/src/adapters/registry.ts +12 -0
- package/src/adapters/responses-tool-schema.ts +113 -8
- package/src/cli/dispatch.ts +2 -0
- package/src/cli/doctor.ts +40 -0
- package/src/cli/effort.ts +24 -8
- package/src/cli/help.ts +2 -0
- package/src/cli/index.ts +24 -1
- package/src/cli/provider-runtime.ts +2 -1
- package/src/cli/registry.ts +2 -2
- package/src/cli/system-command.ts +10 -3
- package/src/cli/usage-report.ts +9 -5
- package/src/codex/account-lifecycle.ts +35 -2
- package/src/codex/auth-api.ts +6 -3
- package/src/codex/auth-collision.ts +12 -2
- package/src/codex/auth-context.ts +75 -7
- package/src/codex/catalog/provider-fetch.ts +73 -9
- package/src/codex/inject.ts +127 -30
- package/src/codex/internal/catalog-writer.ts +3 -0
- package/src/codex/journal.ts +61 -12
- package/src/codex/model-cache.ts +11 -4
- package/src/codex/native-profile-startup.ts +72 -5
- package/src/codex/native-profile-store.ts +2 -2
- package/src/codex/ocx-compaction-history.ts +226 -0
- package/src/codex/project-config-warnings.ts +3 -1
- package/src/codex/quota.ts +18 -8
- package/src/codex/reserve-availability.ts +21 -5
- package/src/codex/runtime.ts +45 -1
- package/src/codex/sync.ts +5 -0
- package/src/config.ts +1 -0
- package/src/generated/compatibility-version.json +126 -74
- package/src/images/loop.ts +1 -0
- package/src/images/xai-video-client.ts +2 -0
- package/src/integrations/registry.ts +1 -0
- package/src/lib/process-control.ts +52 -8
- package/src/lib/upstream-retry.ts +1 -0
- package/src/oauth/chatgpt.ts +83 -0
- package/src/oauth/xai.ts +151 -8
- package/src/providers/api-key-selection-capture.ts +10 -0
- package/src/providers/api-key-selection.ts +2 -7
- package/src/providers/caller-authorization.ts +36 -0
- package/src/providers/codebuddy-models.ts +184 -0
- package/src/providers/derive.ts +5 -0
- package/src/providers/free-directory.ts +26 -2
- package/src/providers/openai-sidecar.ts +35 -11
- package/src/providers/qoder-models.ts +25 -0
- package/src/providers/registry.ts +139 -0
- package/src/responses/compaction.ts +4 -0
- package/src/responses/task-input.ts +21 -1
- package/src/router.ts +1 -1
- package/src/server/chat-completions.ts +26 -9
- package/src/server/claude-messages.ts +12 -2
- package/src/server/images.ts +1 -0
- package/src/server/management/account-selection-stream.ts +13 -4
- package/src/server/management/config-routes.ts +24 -5
- package/src/server/management/logs-usage-routes.ts +1 -0
- package/src/server/management/native-integration-routes.ts +2 -1
- package/src/server/management/provider-routes.ts +24 -0
- package/src/server/management-api.ts +14 -0
- package/src/server/port-reclaim.ts +11 -26
- package/src/server/request-log.ts +6 -0
- package/src/server/responses/collaboration.ts +4 -9
- package/src/server/responses/core.ts +141 -19
- package/src/server/responses/fetch-helpers.ts +18 -7
- package/src/server/responses/policy-fallback.ts +12 -0
- package/src/service.ts +36 -2
- package/src/storage/cleanup.ts +77 -45
- package/src/types/config.ts +7 -1
- package/src/usage/log.ts +24 -0
- package/src/vision/anthropic-describe.ts +1 -0
- package/src/web-search/anthropic-executor.ts +1 -0
- package/src/web-search/loop.ts +1 -0
- package/src/web-search/progress-stream.ts +4 -0
- package/gui/dist/assets/index-B6EFoatP.js +0 -115
- package/gui/dist/assets/index-D5SiRo8X.css +0 -1
|
@@ -38,6 +38,9 @@ import {
|
|
|
38
38
|
} from "./request-log";
|
|
39
39
|
import { responseWithDeferredRequestLog } from "./relay";
|
|
40
40
|
import { handleResponses } from "./responses";
|
|
41
|
+
import { providerConsumesCallerAuthorization } from "../providers/caller-authorization";
|
|
42
|
+
import { captureExplicitOpenAiCallerAuth } from "../providers/openai-sidecar";
|
|
43
|
+
import { captureCallerDirectAuth } from "../providers/caller-authorization";
|
|
41
44
|
import type { AdmissionLease } from "../lib/admission";
|
|
42
45
|
import type { DataPlaneAdmission } from "./auth-cors";
|
|
43
46
|
import { tryClaimNativeMainProfileForTurn } from "../codex/native-main-admission";
|
|
@@ -133,7 +136,8 @@ async function handleChatCompletionsWithBudget(
|
|
|
133
136
|
// it registers (extra_headers, sent verbatim by upstream Grok). Dashboard usage
|
|
134
137
|
// bucketing only — never an auth or billing signal.
|
|
135
138
|
if (req.headers.get("x-opencodex-grok") === "1") logCtx.surface = "grok";
|
|
136
|
-
let
|
|
139
|
+
let callerAuthorizationRoute = false;
|
|
140
|
+
let routeMayChangeCredentialDomain = false;
|
|
137
141
|
let settledRoute: ReturnType<typeof routeModel> | null = null;
|
|
138
142
|
let chatNativeRoute: ReturnType<typeof routeModel> | null = null;
|
|
139
143
|
try {
|
|
@@ -150,9 +154,9 @@ async function handleChatCompletionsWithBudget(
|
|
|
150
154
|
logCtx.provider = route.providerName;
|
|
151
155
|
logCtx.routeDecision = route.routeDecision;
|
|
152
156
|
settledRoute = route;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
157
|
+
routeMayChangeCredentialDomain = route.combo !== undefined || route.routeKind === "policy";
|
|
158
|
+
callerAuthorizationRoute = !routeMayChangeCredentialDomain
|
|
159
|
+
&& providerConsumesCallerAuthorization(route.provider);
|
|
156
160
|
if (route.provider.adapter === "cursor" || route.provider.adapter === "kiro") {
|
|
157
161
|
const parts: string[] = [];
|
|
158
162
|
if (chatBody.messages !== undefined) parts.push(JSON.stringify(chatBody.messages));
|
|
@@ -240,17 +244,23 @@ async function handleChatCompletionsWithBudget(
|
|
|
240
244
|
&& isCodexReserveHelperUnsupported(config, settledRoute.modelId, logIds?.admission, visionDescribeTerminal)) {
|
|
241
245
|
return chatCompletionsErrorResponse(400, CODEX_RESERVE_HELPER_UNSUPPORTED_MESSAGE, "invalid_request_error");
|
|
242
246
|
}
|
|
247
|
+
const nativeCallerAuth = captureExplicitOpenAiCallerAuth(req.headers, config);
|
|
248
|
+
// Caller-owned only: stored-main enrichment below is sidecar authority, never Direct authority.
|
|
249
|
+
const callerDirectAuth = captureCallerDirectAuth(req.headers, config);
|
|
250
|
+
let openAiSidecarAuth = nativeCallerAuth;
|
|
243
251
|
const headers = new Headers({ "content-type": "application/json" });
|
|
244
252
|
// Internal bridge metadata; the Go resolver scopes and hashes it before upstream use.
|
|
245
253
|
const openCodeSession = req.headers.get("x-opencode-session");
|
|
246
254
|
if (openCodeSession) headers.set("x-opencode-session", openCodeSession);
|
|
247
255
|
for (const name of FORWARD_HEADERS) {
|
|
248
|
-
if (name === "authorization"
|
|
256
|
+
if (routeMayChangeCredentialDomain && (name === "authorization" || name === "chatgpt-account-id")) continue;
|
|
257
|
+
if (name === "authorization" && !callerAuthorizationRoute) continue;
|
|
249
258
|
const value = req.headers.get(name);
|
|
250
259
|
if (value) headers.set(name, value);
|
|
251
260
|
}
|
|
252
|
-
//
|
|
253
|
-
|
|
261
|
+
// A noncanonical caller-auth route can use stored main auth only through a sidecar snapshot.
|
|
262
|
+
// Later shadow/thread rewrites strip primary credentials at the actual Responses boundary.
|
|
263
|
+
if (!callerAuthorizationRoute || (settledRoute && !isCanonicalOpenAiForwardProvider(settledRoute.provider))) {
|
|
254
264
|
// This enrichment is optional for routed/non-main providers. If native main
|
|
255
265
|
// is fenced, omit it and let auth-context reject only a final physical-main
|
|
256
266
|
// selection while healthy pool/provider routes continue.
|
|
@@ -259,8 +269,12 @@ async function handleChatCompletionsWithBudget(
|
|
|
259
269
|
const { getMainAccountToken } = await import("../codex/main-account");
|
|
260
270
|
const token = getMainAccountToken();
|
|
261
271
|
if (token) {
|
|
262
|
-
|
|
263
|
-
|
|
272
|
+
const mainHeaders = new Headers({ authorization: `Bearer ${token.accessToken}`, "chatgpt-account-id": token.chatgptAccountId });
|
|
273
|
+
openAiSidecarAuth ??= captureExplicitOpenAiCallerAuth(mainHeaders, config);
|
|
274
|
+
if (!callerAuthorizationRoute && !routeMayChangeCredentialDomain) {
|
|
275
|
+
headers.set("authorization", `Bearer ${token.accessToken}`);
|
|
276
|
+
headers.set("chatgpt-account-id", token.chatgptAccountId);
|
|
277
|
+
}
|
|
264
278
|
}
|
|
265
279
|
} catch {
|
|
266
280
|
/* optional */
|
|
@@ -299,6 +313,9 @@ async function handleChatCompletionsWithBudget(
|
|
|
299
313
|
addFinalRequestLog(logIds.requestId, logIds.start, logCtx, status, meta);
|
|
300
314
|
};
|
|
301
315
|
const upstream = await handleResponses(internalReq, config, logCtx, {
|
|
316
|
+
openAiSidecarAuth,
|
|
317
|
+
nativeCallerAuth,
|
|
318
|
+
callerDirectAuth,
|
|
302
319
|
...(logIds?.turnAdmissionLease ? { turnAdmissionLease: logIds.turnAdmissionLease } : {}),
|
|
303
320
|
// #1686: the Chat surface translates its body and replays here, so the admission fact has
|
|
304
321
|
// to ride along or a bearer-admitted Chat caller would still be refused by Direct.
|
|
@@ -603,7 +603,7 @@ export async function fetchWithHeaderDeadline(
|
|
|
603
603
|
): Promise<HeaderDeadlineFetchResult> {
|
|
604
604
|
const deadline = makeDeadline(timeoutMs, parent);
|
|
605
605
|
try {
|
|
606
|
-
const upstream = await fetchImpl(input, { ...init, signal: deadline.signal, timeout: 0 });
|
|
606
|
+
const upstream = await fetchImpl(input, { ...init, redirect: "manual", signal: deadline.signal, timeout: 0 });
|
|
607
607
|
return { kind: "response", upstream };
|
|
608
608
|
} catch (error) {
|
|
609
609
|
if (deadline.didExpire()) return { kind: "timeout" };
|
|
@@ -837,6 +837,7 @@ async function handleClaudeMessagesWithBudget(
|
|
|
837
837
|
}
|
|
838
838
|
|
|
839
839
|
const headers = new Headers({ "content-type": "application/json" });
|
|
840
|
+
let trustedClaudeMainAuth: { authorization: string; chatgptAccountId?: string } | undefined;
|
|
840
841
|
for (const name of FORWARD_HEADERS) {
|
|
841
842
|
// The caller's bearer is the proxy admission token (ocx claude placeholder), never a
|
|
842
843
|
// ChatGPT credential — forwarding it upstream turns into {"detail":"Unauthorized"}.
|
|
@@ -852,8 +853,13 @@ async function handleClaudeMessagesWithBudget(
|
|
|
852
853
|
const { getMainAccountToken } = await import("../codex/main-account");
|
|
853
854
|
const token = getMainAccountToken();
|
|
854
855
|
if (token) {
|
|
855
|
-
|
|
856
|
+
const authorization = `Bearer ${token.accessToken}`;
|
|
857
|
+
headers.set("authorization", authorization);
|
|
856
858
|
headers.set("chatgpt-account-id", token.chatgptAccountId);
|
|
859
|
+
trustedClaudeMainAuth = {
|
|
860
|
+
authorization,
|
|
861
|
+
...(token.chatgptAccountId ? { chatgptAccountId: token.chatgptAccountId } : {}),
|
|
862
|
+
};
|
|
857
863
|
}
|
|
858
864
|
}
|
|
859
865
|
if (opencodeGoRoute) {
|
|
@@ -923,6 +929,10 @@ async function handleClaudeMessagesWithBudget(
|
|
|
923
929
|
// would fire, disagreeing with the pre-flight decision above.
|
|
924
930
|
inboundWire: "anthropic",
|
|
925
931
|
stripClaudeMainAuthForNoncanonicalForward: true,
|
|
932
|
+
...(trustedClaudeMainAuth ? { trustedClaudeMainAuth } : {}),
|
|
933
|
+
// Claude's internal stored-main enrichment is not an original caller credential.
|
|
934
|
+
nativeCallerAuth: null,
|
|
935
|
+
callerDirectAuth: null,
|
|
926
936
|
translatorBudget,
|
|
927
937
|
...(logIds ? { onFirstOutput: () => recordFirstOutput(logCtx, logIds.start) } : {}),
|
|
928
938
|
onNativePassthroughTerminal: status => finalizeNativeLog(httpStatusForRequestLogTerminal(status, logCtx), { terminalStatus: status, closeReason: "terminal" }),
|
package/src/server/images.ts
CHANGED
|
@@ -288,6 +288,7 @@ async function tryCcaImageGeneration(
|
|
|
288
288
|
try {
|
|
289
289
|
upstream = await fetch(`${baseUrl}/v1internal:generateContent`, {
|
|
290
290
|
method: "POST",
|
|
291
|
+
redirect: "manual",
|
|
291
292
|
headers: {
|
|
292
293
|
"Content-Type": "application/json",
|
|
293
294
|
"Authorization": `Bearer ${token}`,
|
|
@@ -3,6 +3,7 @@ import { registerOptionalShutdownHook } from "../../lib/optional-shutdown-hooks"
|
|
|
3
3
|
|
|
4
4
|
const MAX_SELECTION_STREAMS = 64;
|
|
5
5
|
const HEARTBEAT_MS = 15_000;
|
|
6
|
+
const STREAM_QUEUE_HIGH_WATER_MARK = 16;
|
|
6
7
|
const encoder = new TextEncoder();
|
|
7
8
|
const connections = new Set<() => void>();
|
|
8
9
|
|
|
@@ -36,9 +37,17 @@ export function accountSelectionStream(request: Request, validate: () => boolean
|
|
|
36
37
|
const send = (frame: string) => {
|
|
37
38
|
if (closed) return;
|
|
38
39
|
if (!authorized()) {
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
// A revoked consumer must not drain frames queued before revocation: error() is
|
|
41
|
+
// what discards a non-empty queue. When nothing is queued — the common expired-session
|
|
42
|
+
// path, including the heartbeat — close() alone terminates quietly, so an expired
|
|
43
|
+
// dashboard session does not dump an expected DOMException into the server console.
|
|
44
|
+
// desiredSize equals the high water mark exactly when the queue is empty.
|
|
45
|
+
if (controller.desiredSize !== null && controller.desiredSize < STREAM_QUEUE_HIGH_WATER_MARK) {
|
|
46
|
+
try { controller.error(new DOMException("Management session is no longer authorized", "NotAllowedError")); }
|
|
47
|
+
finally { close(); }
|
|
48
|
+
} else {
|
|
49
|
+
close();
|
|
50
|
+
}
|
|
42
51
|
return;
|
|
43
52
|
}
|
|
44
53
|
// Reconnection sends a ready event, so a slow reader can reconcile without an
|
|
@@ -61,7 +70,7 @@ export function accountSelectionStream(request: Request, validate: () => boolean
|
|
|
61
70
|
heartbeat.unref?.();
|
|
62
71
|
},
|
|
63
72
|
cancel() { cleanup(); },
|
|
64
|
-
}, { highWaterMark:
|
|
73
|
+
}, { highWaterMark: STREAM_QUEUE_HIGH_WATER_MARK });
|
|
65
74
|
return new Response(body, { headers: {
|
|
66
75
|
"Content-Type": "text/event-stream; charset=utf-8",
|
|
67
76
|
"Cache-Control": "no-cache, no-transform",
|
|
@@ -329,6 +329,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
329
329
|
oauthOpenBrowser: config.oauthOpenBrowser !== false,
|
|
330
330
|
// Absent means off (today's Design B injection), so the GUI/CLI render a plain switch.
|
|
331
331
|
codexDesktopAuthless: config.codexDesktopAuthless === true,
|
|
332
|
+
// Absent keeps Design B remote compaction; true selects the dedicated provider identity.
|
|
333
|
+
codexClientCompaction: config.codexClientCompaction === true,
|
|
332
334
|
startupHealth: await readStartupHealth(config),
|
|
333
335
|
codexRuntime: {
|
|
334
336
|
path: displayCodexRuntimePath(resolved.runtime.command),
|
|
@@ -419,6 +421,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
419
421
|
ultraFastTier?: unknown;
|
|
420
422
|
codexMainAccountHardLock?: unknown;
|
|
421
423
|
codexDesktopAuthless?: unknown;
|
|
424
|
+
codexClientCompaction?: unknown;
|
|
422
425
|
};
|
|
423
426
|
if (body.codexAutoStart === undefined
|
|
424
427
|
&& body.streamMode === undefined
|
|
@@ -429,8 +432,9 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
429
432
|
&& body.showCodexSparkQuota === undefined
|
|
430
433
|
&& body.ultraFastTier === undefined
|
|
431
434
|
&& body.codexMainAccountHardLock === undefined
|
|
432
|
-
&& body.codexDesktopAuthless === undefined
|
|
433
|
-
|
|
435
|
+
&& body.codexDesktopAuthless === undefined
|
|
436
|
+
&& body.codexClientCompaction === undefined) {
|
|
437
|
+
return jsonResponse({ error: "provide codexAutoStart, streamMode, appOwnedMemoryBudgetMb, codexAccountPickerEnabled, codexQuotaAutoRefresh, oauthOpenBrowser, showCodexSparkQuota, ultraFastTier, codexMainAccountHardLock, codexDesktopAuthless, or codexClientCompaction" }, 400);
|
|
434
438
|
}
|
|
435
439
|
if (body.codexAutoStart !== undefined && typeof body.codexAutoStart !== "boolean") {
|
|
436
440
|
return jsonResponse({ error: "codexAutoStart boolean is required" }, 400);
|
|
@@ -457,6 +461,9 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
457
461
|
if (body.codexDesktopAuthless !== undefined && typeof body.codexDesktopAuthless !== "boolean") {
|
|
458
462
|
return jsonResponse({ error: "codexDesktopAuthless boolean is required" }, 400);
|
|
459
463
|
}
|
|
464
|
+
if (body.codexClientCompaction !== undefined && typeof body.codexClientCompaction !== "boolean") {
|
|
465
|
+
return jsonResponse({ error: "codexClientCompaction boolean is required" }, 400);
|
|
466
|
+
}
|
|
460
467
|
let quotaAutoRefreshChange: { id: string; window: "fiveHour" | "weekly"; enabled: boolean } | undefined;
|
|
461
468
|
if (body.codexQuotaAutoRefresh !== undefined) {
|
|
462
469
|
if (!isPlainRecord(body.codexQuotaAutoRefresh)) {
|
|
@@ -509,10 +516,13 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
509
516
|
hasCodexMainAccountHardLock: Object.hasOwn(config, "codexMainAccountHardLock"),
|
|
510
517
|
codexDesktopAuthless: config.codexDesktopAuthless,
|
|
511
518
|
hasCodexDesktopAuthless: Object.hasOwn(config, "codexDesktopAuthless"),
|
|
519
|
+
codexClientCompaction: config.codexClientCompaction,
|
|
520
|
+
hasCodexClientCompaction: Object.hasOwn(config, "codexClientCompaction"),
|
|
512
521
|
};
|
|
513
522
|
const pickerWasEnabled = codexAccountPickerEnabled(config);
|
|
514
523
|
let pickerIsEnabled = pickerWasEnabled;
|
|
515
524
|
const authlessWasEnabled = config.codexDesktopAuthless === true;
|
|
525
|
+
const clientCompactionWasEnabled = config.codexClientCompaction === true;
|
|
516
526
|
try {
|
|
517
527
|
if (typeof body.codexAutoStart === "boolean") {
|
|
518
528
|
config.codexAutoStart = body.codexAutoStart;
|
|
@@ -547,6 +557,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
547
557
|
else if (body.codexMainAccountHardLock === false) deleteConfigTopLevelKey(config, "codexMainAccountHardLock");
|
|
548
558
|
if (body.codexDesktopAuthless === true) config.codexDesktopAuthless = true;
|
|
549
559
|
else if (body.codexDesktopAuthless === false) deleteConfigTopLevelKey(config, "codexDesktopAuthless");
|
|
560
|
+
if (body.codexClientCompaction === true) config.codexClientCompaction = true;
|
|
561
|
+
else if (body.codexClientCompaction === false) deleteConfigTopLevelKey(config, "codexClientCompaction");
|
|
550
562
|
if (quotaAutoRefreshChange) {
|
|
551
563
|
const { id, window, enabled } = quotaAutoRefreshChange;
|
|
552
564
|
const setting = { ...(config.codexQuotaAutoRefresh?.[id] ?? {}) };
|
|
@@ -592,16 +604,22 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
592
604
|
if (previousSettings.hasCodexDesktopAuthless) {
|
|
593
605
|
config.codexDesktopAuthless = previousSettings.codexDesktopAuthless;
|
|
594
606
|
} else deleteConfigTopLevelKey(config, "codexDesktopAuthless");
|
|
607
|
+
if (previousSettings.hasCodexClientCompaction) {
|
|
608
|
+
config.codexClientCompaction = previousSettings.codexClientCompaction;
|
|
609
|
+
} else deleteConfigTopLevelKey(config, "codexClientCompaction");
|
|
595
610
|
throw error;
|
|
596
611
|
}
|
|
597
612
|
if (typeof body.appOwnedMemoryBudgetMb === "number") {
|
|
598
613
|
configureAppOwnedMemoryBudget(resolveAppOwnedMemoryBudgetBytes(body.appOwnedMemoryBudgetMb));
|
|
599
614
|
enforceAppOwnedMemoryBudget();
|
|
600
615
|
}
|
|
601
|
-
//
|
|
602
|
-
// waiting for the next start; the injector re-reads config and rewrites the form.
|
|
616
|
+
// Both Desktop compatibility switches change the injected config.toml shape, so converge now
|
|
617
|
+
// rather than waiting for the next start; the injector re-reads config and rewrites the form.
|
|
603
618
|
const authlessIsEnabled = config.codexDesktopAuthless === true;
|
|
604
|
-
const
|
|
619
|
+
const clientCompactionIsEnabled = config.codexClientCompaction === true;
|
|
620
|
+
const catalogRefresh = pickerWasEnabled !== pickerIsEnabled
|
|
621
|
+
|| authlessWasEnabled !== authlessIsEnabled
|
|
622
|
+
|| clientCompactionWasEnabled !== clientCompactionIsEnabled
|
|
605
623
|
? await convergeCodexCatalog()
|
|
606
624
|
: undefined;
|
|
607
625
|
const catalogRefreshPending = catalogRefresh
|
|
@@ -620,6 +638,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
620
638
|
catalogRefreshPending,
|
|
621
639
|
showCodexSparkQuota: config.showCodexSparkQuota === true,
|
|
622
640
|
codexDesktopAuthless: authlessIsEnabled,
|
|
641
|
+
codexClientCompaction: clientCompactionIsEnabled,
|
|
623
642
|
codexMainAccountHardLock: config.codexMainAccountHardLock === true,
|
|
624
643
|
mainAccountHardLock: getMainAccountHardLockStatus(config),
|
|
625
644
|
startupHealth: await readStartupHealth(config),
|
|
@@ -425,6 +425,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
425
425
|
bytes: result.bytes,
|
|
426
426
|
...(result.trashDir ? { trashDir: result.trashDir } : {}),
|
|
427
427
|
removedPaths: result.removedPaths,
|
|
428
|
+
...(result.skippedReferencedPaths?.length ? { skippedReferencedPaths: result.skippedReferencedPaths } : {}),
|
|
428
429
|
});
|
|
429
430
|
} catch {
|
|
430
431
|
return jsonResponse({
|
|
@@ -366,13 +366,14 @@ async function handleCodexToggle(ctx: ManagementContext): Promise<Response> {
|
|
|
366
366
|
}
|
|
367
367
|
}
|
|
368
368
|
const { restoreNativeCodexAsync } = await import("../../codex/inject");
|
|
369
|
+
const { OCX_NATIVE_REPLAY_RECOVERY_NOTE } = await import("../../responses/compaction");
|
|
369
370
|
const restored = await restoreNativeCodexAsync({ revalidateDesiredState: true });
|
|
370
371
|
return jsonResponse({
|
|
371
372
|
ok: true, clientId: "codex", changed: durable && persisted.status === "committed",
|
|
372
373
|
state: restored.success ? "absent" : "unsafe",
|
|
373
374
|
desiredEnabled: enabled,
|
|
374
375
|
message: restored.success
|
|
375
|
-
?
|
|
376
|
+
? `Codex restored to its native path; the proxy is still serving other clients. ${OCX_NATIVE_REPLAY_RECOVERY_NOTE}`
|
|
376
377
|
: `Codex intent saved, but restoring the native path did not complete: ${restored.message}`,
|
|
377
378
|
...(restored.success
|
|
378
379
|
? (durable ? {} : { reason: "not_durable" })
|
|
@@ -40,6 +40,8 @@ import { providerDestinationResolvedError } from "../../lib/destination-policy";
|
|
|
40
40
|
import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
|
|
41
41
|
import { ProviderOutboundPolicyError, providerOutboundGet, providerOutboundPost, providerRedirectError } from "../../lib/provider-outbound";
|
|
42
42
|
import { fetchCursorUsableModels } from "../../adapters/cursor/live-models";
|
|
43
|
+
import { fetchQoderModels } from "../../adapters/qoder/live-models";
|
|
44
|
+
import { resolveQoderProfile } from "../../adapters/qoder/profiles";
|
|
43
45
|
import { parseAntigravityAvailableModels } from "../../providers/antigravity-models";
|
|
44
46
|
import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/key-providers";
|
|
45
47
|
import { deriveProviderPresets, providerConfigSeed } from "../../providers/derive";
|
|
@@ -1350,6 +1352,28 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
1350
1352
|
message: `Connected. ${live.models.length} models.`,
|
|
1351
1353
|
});
|
|
1352
1354
|
}
|
|
1355
|
+
if (prov.adapter === "qoder") {
|
|
1356
|
+
const started = Date.now();
|
|
1357
|
+
const profile = resolveQoderProfile(prov.baseUrl);
|
|
1358
|
+
if (!profile) {
|
|
1359
|
+
return jsonResponse({ ok: false, latencyMs: 0, error: "qoder discovery refused a non-canonical destination" });
|
|
1360
|
+
}
|
|
1361
|
+
const live = await fetchQoderModels(profile, apiKey ?? "");
|
|
1362
|
+
const latencyMs = Date.now() - started;
|
|
1363
|
+
if (!live.ok) {
|
|
1364
|
+
return jsonResponse({
|
|
1365
|
+
ok: false,
|
|
1366
|
+
latencyMs,
|
|
1367
|
+
error: `qoder discovery ${live.error}${live.detail ? `: ${live.detail}` : ""}`,
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
return jsonResponse({
|
|
1371
|
+
ok: true,
|
|
1372
|
+
latencyMs,
|
|
1373
|
+
models: live.models.length,
|
|
1374
|
+
message: `Connected. ${live.models.length} models.`,
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1353
1377
|
const project = prov.project ?? snapshot?.projectId;
|
|
1354
1378
|
if (antigravity && !project) {
|
|
1355
1379
|
return jsonResponse({ ok: false, latencyMs: 0, error: "Antigravity project unavailable — re-run `ocx login google-antigravity`" });
|
|
@@ -300,6 +300,20 @@ export async function handleManagementAPI(
|
|
|
300
300
|
message: "This proxy is managed by a Task Scheduler wrapper that can respawn it, so the stop must be run by `ocx stop`, which verifies the respawn window. Nothing was changed.",
|
|
301
301
|
}, 409, req, config);
|
|
302
302
|
}
|
|
303
|
+
if (respawnRisk === "self-unload") {
|
|
304
|
+
// This proxy IS the launchd/systemd job, so stopping the manager below would
|
|
305
|
+
// terminate the handler before the shared teardown at the end of this route restores
|
|
306
|
+
// the native Codex keys — the dashboard Stop button left `openai_base_url`,
|
|
307
|
+
// `experimental_realtime_ws_base_url` and `model_catalog_json` pointed at a dead
|
|
308
|
+
// proxy (#4023). Refuse before touching anything, like the Windows branch above.
|
|
309
|
+
// `ocx stop` is safe because it runs outside this process and owns the teardown
|
|
310
|
+
// through its receipt, which is why the receipt-backed caller never reaches here.
|
|
311
|
+
return jsonResponse({
|
|
312
|
+
success: false,
|
|
313
|
+
code: "self_unload_service",
|
|
314
|
+
message: "This proxy is running as the installed service, so stopping the manager from inside it would end this process before native Codex is restored. Run `ocx stop`, which stops the service from outside and completes the restore. Nothing was changed.",
|
|
315
|
+
}, 409, req, config);
|
|
316
|
+
}
|
|
303
317
|
if (respawnRisk === "unknown") {
|
|
304
318
|
// Do NOT send them to `ocx stop`: it maps the same unanswerable probe to a stop
|
|
305
319
|
// failure, so that advice would be a loop. The scheduler query itself is what needs
|
|
@@ -2,11 +2,9 @@
|
|
|
2
2
|
* Reclaim a listen port after stop/update so restart can stay on the configured
|
|
3
3
|
* port instead of hopping to an ephemeral one (Windows CLOSE_WAIT / leftover ocx).
|
|
4
4
|
*
|
|
5
|
-
* Killing is never the default.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* or enables `killAllOcxOnPort` for revalidated ocx listeners. Unknown foreign
|
|
9
|
-
* (non-ocx, non-allowlisted) processes are never killed.
|
|
5
|
+
* Killing is never the default. It requires `killOcxHolders`, an allowed PID or
|
|
6
|
+
* `killAllOcxOnPort`, and successful ocx verification. A historical PID allowlist
|
|
7
|
+
* never overrides a rejected verifier result; rejected live holders stay protected.
|
|
10
8
|
*/
|
|
11
9
|
import { execFileSync } from "node:child_process";
|
|
12
10
|
import { verifyPidIdentity } from "../config/process-state";
|
|
@@ -29,6 +27,9 @@ export type ReclaimListenPortOptions = WaitForPortOptions & {
|
|
|
29
27
|
/**
|
|
30
28
|
* Explicit PIDs the caller just stopped / hard-killed. An omitted or empty
|
|
31
29
|
* list means no process may be killed — unless {@link killAllOcxOnPort} is set.
|
|
30
|
+
* The allowlist only narrows kill candidates: every candidate, allowlisted or
|
|
31
|
+
* not, still requires verifier acceptance (`verifyOcxFn(pid) === pid`) on each
|
|
32
|
+
* scan, and a rejected live holder is never killed or TCP-row dropped.
|
|
32
33
|
*/
|
|
33
34
|
onlyKillPids?: number[];
|
|
34
35
|
/**
|
|
@@ -36,8 +37,7 @@ export type ReclaimListenPortOptions = WaitForPortOptions & {
|
|
|
36
37
|
* killed (re-checked each scan). Used by post-update restart so a Windows
|
|
37
38
|
* service wrapper that respawns a *new* bun PID mid-reclaim cannot stay
|
|
38
39
|
* protected just because it was absent from the pre-wait allowlist snapshot.
|
|
39
|
-
*
|
|
40
|
-
* and revalidated ocx listeners.
|
|
40
|
+
* Every candidate still requires ocx verifier acceptance before termination.
|
|
41
41
|
*/
|
|
42
42
|
killAllOcxOnPort?: boolean;
|
|
43
43
|
/**
|
|
@@ -174,8 +174,8 @@ export function listListenPids(port: number): number[] {
|
|
|
174
174
|
* Never kills a process unless `killOcxHolders === true` and either
|
|
175
175
|
* `onlyKillPids` is a non-empty allowlist or `killAllOcxOnPort` is set — then
|
|
176
176
|
* revalidates immediately before each kill.
|
|
177
|
-
* Never
|
|
178
|
-
* protected ocx listener owns the port, or when the
|
|
177
|
+
* Never overrides a rejected ocx verifier result. Never drops TCP rows while a
|
|
178
|
+
* rejected live or protected ocx listener owns the port, or when the scan failed.
|
|
179
179
|
*/
|
|
180
180
|
export async function reclaimListenPort(
|
|
181
181
|
port: number,
|
|
@@ -231,24 +231,9 @@ export async function reclaimListenPort(
|
|
|
231
231
|
}
|
|
232
232
|
const isOcx = verifyOcxFn(pid) === pid;
|
|
233
233
|
const allowlisted = allowedKillPids.has(pid);
|
|
234
|
-
// Pre-update PIDs can fail verify while still LISTENing (dead owner still
|
|
235
|
-
// listed, or cmdline probe raced). Allowlisted teardown PIDs may be killed;
|
|
236
|
-
// unknown foreign claimants must remain fail-closed.
|
|
237
234
|
if (!isOcx) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
try {
|
|
241
|
-
killFn(pid);
|
|
242
|
-
killed.add(pid);
|
|
243
|
-
} catch {
|
|
244
|
-
// Kill failed: never SetTcpEntry while the process may still own the port.
|
|
245
|
-
protectedOcxListener = true;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
if (!isAliveFn(pid)) killed.delete(pid);
|
|
249
|
-
else protectedOcxListener = true;
|
|
250
|
-
continue;
|
|
251
|
-
}
|
|
235
|
+
// A saved PID narrows eligible candidates; it cannot override verifier rejection.
|
|
236
|
+
// Dead ghost owners have already been skipped by the liveness check above.
|
|
252
237
|
foreignLive = true;
|
|
253
238
|
continue;
|
|
254
239
|
}
|
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
appendUsageEntry,
|
|
23
23
|
isKnownAdmissionKind,
|
|
24
24
|
isKnownInboundProtocol,
|
|
25
|
+
isKnownTerminalSource,
|
|
26
|
+
isKnownTransportPhase,
|
|
25
27
|
isKnownUsageSurface,
|
|
26
28
|
isCodexUsageAccountLogLabel,
|
|
27
29
|
isValidReasoningWireValue,
|
|
@@ -320,6 +322,8 @@ export function requestLogEntryFromPersistedUsage(entry: PersistedUsageEntry): R
|
|
|
320
322
|
...(entry.usage ? { usage: entry.usage } : {}),
|
|
321
323
|
...(entry.totalTokens !== undefined ? { totalTokens: entry.totalTokens } : {}),
|
|
322
324
|
...(entry.attempts !== undefined ? { attempts: entry.attempts } : {}),
|
|
325
|
+
...(isKnownTransportPhase(entry.transportPhase) ? { transportPhase: entry.transportPhase } : {}),
|
|
326
|
+
...(isKnownTerminalSource(entry.terminalSource) ? { terminalSource: entry.terminalSource } : {}),
|
|
323
327
|
...(routeDecision ? { routeDecision } : {}),
|
|
324
328
|
...(claudeCompatibility ? { claudeCompatibility } : {}),
|
|
325
329
|
};
|
|
@@ -441,6 +445,8 @@ export function addRequestLog(entry: RequestLogEntry) {
|
|
|
441
445
|
...(entry.usage ? { usage: entry.usage } : {}),
|
|
442
446
|
...(entry.totalTokens !== undefined ? { totalTokens: entry.totalTokens } : {}),
|
|
443
447
|
...(entry.attempts !== undefined ? { attempts: entry.attempts } : {}),
|
|
448
|
+
...(isKnownTransportPhase(entry.transportPhase) ? { transportPhase: entry.transportPhase } : {}),
|
|
449
|
+
...(isKnownTerminalSource(entry.terminalSource) ? { terminalSource: entry.terminalSource } : {}),
|
|
444
450
|
...failureDiagnostics,
|
|
445
451
|
...(entry.routeDecision ? { routeDecision: entry.routeDecision } : {}),
|
|
446
452
|
...(entry.claudeCompatibility ? { claudeCompatibility: entry.claudeCompatibility } : {}),
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from "../../config";
|
|
9
9
|
import { parseRequest } from "../../responses/parser";
|
|
10
10
|
import { externalTaskInputContent } from "../../responses/task-input";
|
|
11
|
+
import { MULTI_AGENT_MODE_HINT_RECOMMENDATION } from "../../codex/multi-agent-mode-policy";
|
|
11
12
|
import { buildCompactV1Output, COMPACT_PROMPT, decodeCompactionSummary, extractCompactUserMessages } from "../../responses/compaction";
|
|
12
13
|
import { FORWARD_HEADERS, sanitizeReasoningInputContent } from "../../adapters/openai-responses";
|
|
13
14
|
import { expandPreviousResponseInput, previousResponseProviderState, rememberResponseState } from "../../responses/state";
|
|
@@ -233,13 +234,7 @@ export function buildToolBridgeMaps(parsed: OcxParsedRequest, budget?: Translato
|
|
|
233
234
|
|
|
234
235
|
|
|
235
236
|
|
|
236
|
-
export const PROACTIVE_MULTI_AGENT_MODE_TEXT =
|
|
237
|
-
"Proactive multi-agent delegation is active.",
|
|
238
|
-
"Any earlier instruction requiring an explicit user request before spawning sub-agents no longer applies.",
|
|
239
|
-
"Delegate independent sub-tasks to sub-agents whenever parallel work would materially improve speed or quality — do not serialize work that can run concurrently.",
|
|
240
|
-
"Each sub-agent runs in its own context and can use all available tools; prefer spawning specialists over doing everything yourself.",
|
|
241
|
-
"This mode remains active until a later multi-agent mode developer message changes it.",
|
|
242
|
-
].join(" ");
|
|
237
|
+
export const PROACTIVE_MULTI_AGENT_MODE_TEXT = MULTI_AGENT_MODE_HINT_RECOMMENDATION.text;
|
|
243
238
|
|
|
244
239
|
const OPENCODEX_SUBAGENT_GUIDANCE_OPEN_TAG = "<opencodex_subagent_guidance>";
|
|
245
240
|
const OPENCODEX_SUBAGENT_GUIDANCE_CLOSE_TAG = "</opencodex_subagent_guidance>";
|
|
@@ -491,8 +486,8 @@ export async function multiAgentGuidanceText(
|
|
|
491
486
|
}
|
|
492
487
|
|
|
493
488
|
const effort = parsed.options.reasoning;
|
|
494
|
-
// v1
|
|
495
|
-
//
|
|
489
|
+
// v1 changes only the delegation trigger at the top tier; other rules still apply.
|
|
490
|
+
// Ultra arrives as max on the wire. No designation/roster payload here.
|
|
496
491
|
if (effort !== "max" && effort !== "ultra") return null;
|
|
497
492
|
return `<multi_agent_mode>${PROACTIVE_MULTI_AGENT_MODE_TEXT}</multi_agent_mode>`;
|
|
498
493
|
}
|