@bitkyc08/opencodex 2.91.0 → 2.92.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/bin/ocx.mjs +37 -3
- package/gui/dist/assets/{App-nZ34cR5G.js → App-sUf_8afJ.js} +18 -18
- package/gui/dist/assets/{Tray-BuLy7hL1.js → Tray-BSsxznDm.js} +1 -1
- package/gui/dist/assets/index-BlFE_VB_.js +94 -0
- package/gui/dist/assets/quota-summary-BRlbXgw0.js +1 -0
- package/gui/dist/index.html +1 -1
- package/gui/dist/provider-icons/workbuddy-color.svg +29 -0
- package/package.json +4 -3
- package/src/adapters/google-vertex-endpoint.ts +9 -1
- package/src/adapters/google-vertex-key-project.ts +223 -0
- package/src/adapters/google.ts +13 -3
- package/src/adapters/openai-chat/tool-name-registry.ts +3 -1
- package/src/adapters/openai-chat.ts +2 -2
- package/src/adapters/openai-responses/request-strips.ts +60 -0
- package/src/adapters/registry.ts +5 -0
- package/src/adapters/workbuddy-schema.ts +61 -0
- package/src/adapters/workbuddy.ts +68 -0
- package/src/bridge/response-json.ts +1 -1
- package/src/bridge/sse.ts +1 -1
- package/src/chat/outbound.ts +11 -4
- package/src/cli/capabilities-agents-routing.ts +3 -3
- package/src/cli/codex-cli-update.ts +8 -0
- package/src/cli/combo-result.ts +6 -0
- package/src/cli/combo-stats.ts +3 -0
- package/src/cli/combo.ts +66 -42
- package/src/cli/launcher-context.ts +49 -2
- package/src/cli/link.ts +45 -32
- package/src/cli/provider.ts +2 -1
- package/src/cli/usage-compare.ts +68 -12
- package/src/client/link-ingress.ts +1 -1
- package/src/client/link-readiness.ts +97 -0
- package/src/client/link-relay-transport.ts +79 -11
- package/src/client/link-relay.ts +74 -15
- package/src/client/link-tunnel.ts +45 -18
- package/src/client/machine-listener.ts +20 -2
- package/src/client/runtime.ts +20 -2
- package/src/codex/account-store.ts +17 -3
- package/src/codex/app-server-restart-service.ts +6 -5
- package/src/codex/catalog/effort.ts +9 -2
- package/src/codex/catalog/gather-capture.ts +4 -2
- package/src/codex/catalog/provider-models.ts +14 -8
- package/src/codex/chatgpt-refresh-failure.ts +6 -2
- package/src/codex/cli-install-provenance.ts +418 -0
- package/src/codex/cli-installation-targets.ts +5 -1
- package/src/codex/cli-update-plan.ts +43 -16
- package/src/codex/desktop-app/picker-freshness.ts +10 -8
- package/src/codex/desktop-compatibility/usage-activation.ts +2 -1
- package/src/codex/desktop-compatibility/usage-controller.ts +7 -1
- package/src/codex/pool-refresh-backoff.ts +3 -1
- package/src/codex/user-identity.ts +35 -6
- package/src/codex/windows-current-sid.ts +162 -0
- package/src/combos/jev-quota-activation.ts +5 -1
- package/src/combos/jev-quota-route.ts +1 -1
- package/src/combos/jev-quota.ts +9 -1
- package/src/combos/jev-session-pin-config.ts +51 -0
- package/src/combos/jev.ts +6 -0
- package/src/combos/types.ts +19 -0
- package/src/config/provider-validation.ts +7 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +194 -122
- package/src/lib/decision-quota-sink.ts +10 -0
- package/src/lib/translator-budget.ts +4 -4
- package/src/lib/upstream-retry.ts +4 -1
- package/src/link/diagnostics.ts +193 -0
- package/src/link/relay-auth.ts +6 -0
- package/src/link/status-projection.ts +39 -0
- package/src/link/supervisor.ts +45 -12
- package/src/link/tunnel-history.ts +109 -0
- package/src/link/tunnel-state.ts +80 -17
- package/src/messaging/remote-files.ts +24 -1
- package/src/oauth/generic-account-failover.ts +91 -7
- package/src/oauth/index.ts +15 -0
- package/src/oauth/model-discovery-auth.ts +5 -0
- package/src/oauth/store.ts +4 -1
- package/src/oauth/token-guardian.ts +7 -0
- package/src/oauth/types.ts +9 -0
- package/src/oauth/workbuddy.ts +139 -0
- package/src/providers/antigravity-quota-identity.ts +37 -0
- package/src/providers/base-url-choices.ts +8 -5
- package/src/providers/derive.ts +23 -3
- package/src/providers/model-discovery.ts +2 -0
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/quota/account-cache.ts +3 -2
- package/src/providers/quota/antigravity.ts +25 -14
- package/src/providers/quota/report-cache.ts +13 -4
- package/src/providers/quota/vendor-probes-key.ts +60 -0
- package/src/providers/quota/workbuddy.ts +83 -0
- package/src/providers/quota-decision-antigravity.ts +64 -0
- package/src/providers/quota-decision-publication.ts +11 -0
- package/src/providers/quota-decision-snapshot.ts +21 -4
- package/src/providers/quota-types.ts +2 -0
- package/src/providers/quota.ts +3 -1
- package/src/providers/registry/entries-core.ts +9 -0
- package/src/providers/registry/entries-extended.ts +20 -7
- package/src/providers/registry/model-ids.ts +3 -0
- package/src/providers/registry/model-seeds.ts +27 -28
- package/src/providers/registry/types.ts +19 -1
- package/src/providers/resolved-model-policy.ts +4 -4
- package/src/providers/routed-config.ts +15 -0
- package/src/providers/stale-model-roster-migration.ts +83 -15
- package/src/providers/workbuddy-credit-balance.ts +81 -0
- package/src/providers/workbuddy-models.ts +37 -0
- package/src/providers/workbuddy.ts +60 -0
- package/src/responses/reasoning-replay-cache.ts +18 -14
- package/src/router.ts +2 -0
- package/src/routing/analytics.ts +2 -3
- package/src/routing/fallback-policy.ts +3 -3
- package/src/server/auth-cors.ts +1 -0
- package/src/server/chat-completions.ts +14 -1
- package/src/server/chat-native-reasoning.ts +10 -3
- package/src/server/chat-native.ts +13 -2
- package/src/server/claude-messages.ts +3 -0
- package/src/server/index/link-relay-sessions.ts +9 -5
- package/src/server/index/optional-listeners.ts +4 -0
- package/src/server/local-management-read-client.ts +3 -1
- package/src/server/management/combo-routes.ts +10 -4
- package/src/server/management/link-routes.ts +5 -2
- package/src/server/management/provider-routes.ts +1 -1
- package/src/server/port-reclaim.ts +51 -9
- package/src/server/request-log-conversation.ts +49 -0
- package/src/server/request-log.ts +2 -0
- package/src/server/responses/anthropic-thinking-recovery.ts +1 -0
- package/src/server/responses/compact.ts +10 -2
- package/src/server/responses/core-combo.ts +53 -31
- package/src/server/responses/core-lifetime.ts +2 -2
- package/src/server/responses/core-normalize.ts +6 -3
- package/src/server/responses/core-opaque-recovery.ts +1 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-replay.ts +2 -1
- package/src/server/responses/jev-combo-choice.ts +39 -0
- package/src/server/responses/jev-session-pin.ts +203 -0
- package/src/server/responses/passthrough-delivery.ts +4 -0
- package/src/server/responses/request-prepare.ts +10 -5
- package/src/server/responses/request-transport.ts +25 -6
- package/src/server/responses/run-turn-execution.ts +2 -4
- package/src/server/responses/sidecar-execution.ts +2 -4
- package/src/server/responses-phase-inference.ts +267 -0
- package/src/server/sse-payload-rewrite.ts +14 -1
- package/src/service/definition.ts +20 -8
- package/src/types/config.ts +6 -0
- package/src/types/provider.ts +9 -1
- package/src/types/request.ts +1 -0
- package/src/types/wire.ts +1 -0
- package/src/update/codex-cli-update-launch-policy.d.mts +9 -0
- package/src/update/codex-cli-update-launch-policy.mjs +30 -0
- package/src/usage/jev-stats.ts +54 -14
- package/gui/dist/assets/index-DGXS51sp.js +0 -94
- package/gui/dist/assets/quota-summary-DO1Sx5TM.js +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Server } from "bun";
|
|
2
2
|
import {
|
|
3
3
|
LINK_RELAY_AUTH_PATH, LINK_RELAY_AUTH_TIMEOUT_MS, LINK_RELAY_AUTH_VERSION,
|
|
4
|
-
LINK_RELAY_SESSION_HEADER, linkRelayProof, linkRelayProofMatches,
|
|
4
|
+
LINK_RELAY_PROBE_HEADER, LINK_RELAY_SESSION_HEADER, linkRelayProof, linkRelayProofMatches,
|
|
5
5
|
} from "../../link/relay-auth";
|
|
6
6
|
import { parseLinkPeerReport, type LinkPeerObservations } from "../../link/peer-report";
|
|
7
7
|
|
|
@@ -81,13 +81,17 @@ export function createLinkRelaySessions(options: LinkRelaySessionOptions) {
|
|
|
81
81
|
linkRelayProofMatches(q.get("proof"), linkRelayProof(candidate, "caller", keyId, linkId, nonce)));
|
|
82
82
|
if (!fingerprint) return refuse();
|
|
83
83
|
options.peers?.observe(linkId, LINK_RELAY_AUTH_VERSION, parseLinkPeerReport(req.headers));
|
|
84
|
-
const
|
|
85
|
-
const timer = setTimeout(() => { consume(nonce)?.release(); }, reservationMs);
|
|
86
|
-
sessions.set(nonce, { peer, keyId, linkId, fingerprint, release, timer, expiresAt: Date.now() + reservationMs });
|
|
87
|
-
return new Response(null, { status: 204, headers: {
|
|
84
|
+
const answer = () => new Response(null, { status: 204, headers: {
|
|
88
85
|
"x-opencodex-link-proof": linkRelayProof(fingerprint, "listener", keyId, linkId, nonce)!,
|
|
89
86
|
"cache-control": "no-store",
|
|
90
87
|
} });
|
|
88
|
+
// A readiness probe closes after this answer and sends no data request, so it reserves
|
|
89
|
+
// nothing: no session to consume, no capacity and no drain lease held until expiry.
|
|
90
|
+
if (req.headers.get(LINK_RELAY_PROBE_HEADER) === "1") return answer();
|
|
91
|
+
const release = acquire();
|
|
92
|
+
const timer = setTimeout(() => { consume(nonce)?.release(); }, reservationMs);
|
|
93
|
+
sessions.set(nonce, { peer, keyId, linkId, fingerprint, release, timer, expiresAt: Date.now() + reservationMs });
|
|
94
|
+
return answer();
|
|
91
95
|
}
|
|
92
96
|
const nonce = req.headers.get(LINK_RELAY_SESSION_HEADER);
|
|
93
97
|
let release: () => void;
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
type LinkListenerStatus,
|
|
15
15
|
} from "./link-listener";
|
|
16
16
|
import { createLinkSupervisor, type LinkSupervisor } from "../../link/supervisor";
|
|
17
|
+
import { flushLinkDiagnostics, recordLinkDiagnostic } from "../../link/diagnostics";
|
|
17
18
|
import { linkManagementRouteAllowed } from "../../link/management-routes";
|
|
18
19
|
import { linkChildParityRouteAllowed } from "../../link/child-parity";
|
|
19
20
|
import { saveConfigPreservingClaudeCode } from "../../config/live-reconcile";
|
|
@@ -56,6 +57,9 @@ export function createOptionalListenerSet<T>(linkDeps: LinkListenerDeps = {}): O
|
|
|
56
57
|
let activeConfig: OcxConfig | undefined;
|
|
57
58
|
let stopJevQuotaActivation: (() => void) | undefined;
|
|
58
59
|
const supervisor = createLinkSupervisor({
|
|
60
|
+
// Tunnel state changes and their causes reach <configDir>/link/diagnostics.jsonl (bounded).
|
|
61
|
+
diagnostic: recordLinkDiagnostic,
|
|
62
|
+
flushDiagnostics: flushLinkDiagnostics,
|
|
59
63
|
apiKeys: () => activeConfig?.apiKeys ?? [],
|
|
60
64
|
revokeApiKey: id => {
|
|
61
65
|
if (!activeConfig) return false;
|
|
@@ -51,7 +51,7 @@ export interface LocalManagementReadRequestDeps extends LocalManagementReadDeps
|
|
|
51
51
|
*/
|
|
52
52
|
export async function fetchBoundLocalManagementRead(
|
|
53
53
|
target: LiveProxy,
|
|
54
|
-
path: LocalManagementReadPath
|
|
54
|
+
path: LocalManagementReadPath | `${LocalManagementReadPath}?${string}`,
|
|
55
55
|
deps: LocalManagementReadRequestDeps = {},
|
|
56
56
|
): Promise<LocalManagementReadResult> {
|
|
57
57
|
if (
|
|
@@ -89,6 +89,8 @@ export async function fetchBoundLocalManagementRead(
|
|
|
89
89
|
const timeoutMs = deps.timeoutMs ?? 4_000;
|
|
90
90
|
const url = `http://${probeHostname(target.hostname)}:${target.port}${path}`;
|
|
91
91
|
const init: RequestInit = {
|
|
92
|
+
redirect: "error",
|
|
93
|
+
credentials: "omit",
|
|
92
94
|
headers: {
|
|
93
95
|
[LOCAL_MANAGEMENT_EXPECTED_PID_HEADER]: String(target.pid),
|
|
94
96
|
[LOCAL_MANAGEMENT_NONCE_HEADER]: nonce,
|
|
@@ -208,7 +208,6 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
208
208
|
const {
|
|
209
209
|
clearComboSelectionState,
|
|
210
210
|
clearComboTargetCooldowns,
|
|
211
|
-
comboConfigError,
|
|
212
211
|
comboDisabledModelId,
|
|
213
212
|
comboDisabledModelSelectors,
|
|
214
213
|
comboModelId,
|
|
@@ -225,6 +224,8 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
225
224
|
...requestedCombo,
|
|
226
225
|
...(!Object.hasOwn(requestedCombo, "decisionQuotaSignals") && requestedCombo.strategy === "jev" && previous?.decisionQuotaSignals !== undefined ? { decisionQuotaSignals: previous.decisionQuotaSignals } : {}),
|
|
227
226
|
...(!Object.hasOwn(requestedCombo, "decisionQuotaTiers") && requestedCombo.strategy === "jev" && previous?.decisionQuotaTiers !== undefined ? { decisionQuotaTiers: previous.decisionQuotaTiers } : {}),
|
|
227
|
+
// The dashboard does not edit the session pin (#7525): an omitted field keeps it on a JEV save.
|
|
228
|
+
...(!Object.hasOwn(requestedCombo, "sessionPin") && requestedCombo.strategy === "jev" && previous?.sessionPin != null ? { sessionPin: previous.sessionPin } : {}),
|
|
228
229
|
...(!Object.hasOwn(requestedCombo, "allowMeteredTargets") && previous?.allowMeteredTargets === true
|
|
229
230
|
? { allowMeteredTargets: true }
|
|
230
231
|
: {}),
|
|
@@ -257,7 +258,7 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
257
258
|
...(Array.isArray(requestedCombo.targets) && Array.isArray(previous?.targets)
|
|
258
259
|
? {
|
|
259
260
|
targets: requestedCombo.targets.map(target => {
|
|
260
|
-
// A non-record entry stays as the client sent it so
|
|
261
|
+
// A non-record entry stays as the client sent it so comboConfigIssues reports it.
|
|
261
262
|
// Reading `lastResort` off it here would throw in place of that structured 400.
|
|
262
263
|
if (!isPlainRecord(target)) return target;
|
|
263
264
|
if (["lastResort", "metered"].every(key => Object.hasOwn(target, key))) return target;
|
|
@@ -331,8 +332,13 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
331
332
|
excludeComboId: sourceId,
|
|
332
333
|
normalizeDecisionModel: (model: string) => normalizeDecisionModelSelector(prospectiveConfig, model),
|
|
333
334
|
};
|
|
334
|
-
|
|
335
|
-
|
|
335
|
+
// `field` names the top-level combo field of the first issue, so a client can tell a target or
|
|
336
|
+
// JEV-setting refusal from an alias or rename one without parsing the message (#7579).
|
|
337
|
+
const invalid = comboConfigIssues(id, effectiveCombo, config.providers, validationOptions)[0];
|
|
338
|
+
if (invalid) {
|
|
339
|
+
const field = invalid.path[0];
|
|
340
|
+
return jsonResponse({ error: invalid.message, ...(typeof field === "string" ? { field } : {}) }, 400);
|
|
341
|
+
}
|
|
336
342
|
for (const [otherId, combo] of Object.entries(nextCombos)) {
|
|
337
343
|
if (typeof combo.decisionModel !== "string") continue;
|
|
338
344
|
const issue = otherId === id ? undefined : comboConfigIssues(otherId, combo, config.providers, {
|
|
@@ -6,7 +6,7 @@ import { clearCompensationFailed, compensationPath, readCompensation } from "../
|
|
|
6
6
|
import { loadHostCandidates } from "../../link/ssh-config";
|
|
7
7
|
import { newLinkId, type LinkStore } from "../../link/store";
|
|
8
8
|
import { sshFailureHint, sshRunnerErrorHint } from "../../link/ssh-runner";
|
|
9
|
-
import { attachLinkPeers, projectLinkStatus, type LinkStatusDto } from "../../link/status-projection";
|
|
9
|
+
import { attachLinkPeers, attachLinkTunnelDiagnostics, projectLinkStatus, type LinkStatusDto } from "../../link/status-projection";
|
|
10
10
|
import { homeLinkChildParity } from "../../link/child-parity";
|
|
11
11
|
import { localLinkPeer } from "../../link/compatibility";
|
|
12
12
|
import { packageVersion } from "../../lib/package-version";
|
|
@@ -268,8 +268,9 @@ export async function handleLinkRoutes(ctx: ManagementContext, suppliedState?: L
|
|
|
268
268
|
if (denied) return denied;
|
|
269
269
|
const store = readStoreFor(ctx);
|
|
270
270
|
const listenerStatus = state.listener.status();
|
|
271
|
+
const supervisorStates = state.supervisor.status();
|
|
271
272
|
const dto: LinkStatusDto = projectLinkStatus(
|
|
272
|
-
store,
|
|
273
|
+
store, supervisorStates, listenerStatus, ctx.config, readCompensation(),
|
|
273
274
|
ctx.config.runtimeRole === "client" ? clientLinkTunnelStatus() : null,
|
|
274
275
|
ctx.config.runtimeRole === "client" && ctx.config.client?.link ? clientLinkStatusIdentity(ctx.config.client.link.linkId) : null,
|
|
275
276
|
linkId => homeLinkChildParity.get(linkId),
|
|
@@ -288,6 +289,8 @@ export async function handleLinkRoutes(ctx: ManagementContext, suppliedState?: L
|
|
|
288
289
|
attachLinkPeers(dto, localLinkPeer(packageVersion("0.0.0"), buildPeerFeatures()),
|
|
289
290
|
linkId => state.listener.peerObservation?.(linkId) ?? { observed: null, refused: null });
|
|
290
291
|
}
|
|
292
|
+
// The same opt-in for each Home-initiated tunnel's retry progress and last failure cause.
|
|
293
|
+
if (url.searchParams.get("diagnostics") === "1") attachLinkTunnelDiagnostics(dto, supervisorStates);
|
|
291
294
|
// A dashboard session also learns why it cannot join as a Child. The admin-token answer stays
|
|
292
295
|
// the exact K16 document that `ocx link status` validates key by key.
|
|
293
296
|
const denial = ctx.principal === "gui-session" ? joinDenied(ctx) : null;
|
|
@@ -1726,7 +1726,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
1726
1726
|
return jsonResponse({ ok: false, latencyMs: 0, error: "Antigravity project unavailable — re-run `ocx login google-antigravity`" });
|
|
1727
1727
|
}
|
|
1728
1728
|
const { method, url: modelsUrl, headers } = buildModelsRequest(prov, apiKey, name, {
|
|
1729
|
-
oauthApiBaseUrl: snapshot?.apiBaseUrl,
|
|
1729
|
+
oauthApiBaseUrl: snapshot?.apiBaseUrl, workbuddy: snapshot?.workbuddy,
|
|
1730
1730
|
});
|
|
1731
1731
|
const discovery = resolveProviderModelDiscovery(name, prov);
|
|
1732
1732
|
const started = Date.now();
|
|
@@ -405,22 +405,37 @@ async function runOwnerLookup(file: string, args: string[], timeoutMs: number):
|
|
|
405
405
|
return stdout;
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
+
export type LoopbackOwnerLookupResult = {
|
|
409
|
+
kind: "match" | "mismatch" | "lookup_error" | "deadline";
|
|
410
|
+
elapsedMs: number;
|
|
411
|
+
};
|
|
412
|
+
|
|
408
413
|
export interface LoopbackOwnerLookupIo {
|
|
409
414
|
platform?: NodeJS.Platform;
|
|
415
|
+
nowMs?: () => number;
|
|
416
|
+
scheduleDeadline?: (callback: () => void, timeoutMs: number) => () => void;
|
|
410
417
|
readProc?: (path: string) => Promise<string>;
|
|
411
418
|
listFds?: (path: string) => Promise<string[]>;
|
|
412
419
|
readFdLink?: (path: string) => Promise<string>;
|
|
413
420
|
run?: (file: string, args: string[], timeoutMs: number) => Promise<string>;
|
|
414
421
|
}
|
|
415
422
|
|
|
416
|
-
/**
|
|
417
|
-
|
|
423
|
+
/**
|
|
424
|
+
* Classify fresh IPv4 loopback LISTEN ownership within the bounded lookup deadline.
|
|
425
|
+
* Only match proves the expected PID; mismatch, inspection failure and timeout remain distinct.
|
|
426
|
+
* elapsedMs uses a monotonic clock by default, and no raw OS output is returned.
|
|
427
|
+
*/
|
|
428
|
+
export async function inspectIpv4LoopbackListener(
|
|
418
429
|
port: number,
|
|
419
430
|
expectedPid: number,
|
|
420
431
|
io: LoopbackOwnerLookupIo = {},
|
|
421
|
-
): Promise<
|
|
432
|
+
): Promise<LoopbackOwnerLookupResult> {
|
|
433
|
+
const now = io.nowMs ?? (() => performance.now());
|
|
434
|
+
const startedAt = now();
|
|
435
|
+
const result = (kind: LoopbackOwnerLookupResult["kind"]): LoopbackOwnerLookupResult =>
|
|
436
|
+
({ kind, elapsedMs: Math.max(0, now() - startedAt) });
|
|
422
437
|
if (!Number.isInteger(port) || port < 1 || port > 65535
|
|
423
|
-
|| !Number.isSafeInteger(expectedPid) || expectedPid < 1) return
|
|
438
|
+
|| !Number.isSafeInteger(expectedPid) || expectedPid < 1) return result("mismatch");
|
|
424
439
|
const platform = io.platform ?? process.platform;
|
|
425
440
|
const lookup = async (): Promise<boolean> => {
|
|
426
441
|
if (platform === "linux") {
|
|
@@ -458,13 +473,40 @@ export async function ownsIpv4LoopbackListener(
|
|
|
458
473
|
}
|
|
459
474
|
return false;
|
|
460
475
|
};
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
timer = setTimeout(() => resolve(false), OWNER_LOOKUP_TIMEOUT_MS);
|
|
476
|
+
const scheduleDeadline = io.scheduleDeadline ?? ((callback, timeoutMs) => {
|
|
477
|
+
const timer = setTimeout(callback, timeoutMs);
|
|
464
478
|
timer.unref?.();
|
|
479
|
+
return () => clearTimeout(timer);
|
|
465
480
|
});
|
|
466
|
-
|
|
467
|
-
|
|
481
|
+
let cancelDeadline: (() => void) | undefined;
|
|
482
|
+
const deadline = new Promise<LoopbackOwnerLookupResult["kind"]>(resolve => {
|
|
483
|
+
cancelDeadline = scheduleDeadline(() => resolve("deadline"), OWNER_LOOKUP_TIMEOUT_MS);
|
|
484
|
+
});
|
|
485
|
+
const inspected = lookup().then(
|
|
486
|
+
matches => matches ? "match" as const : "mismatch" as const,
|
|
487
|
+
error => {
|
|
488
|
+
const failure = error as {
|
|
489
|
+
killed?: boolean; signal?: string | null; code?: unknown; stdout?: unknown; stderr?: unknown;
|
|
490
|
+
} | null;
|
|
491
|
+
// lsof exits 1 with no output when no socket matches its selectors.
|
|
492
|
+
if (platform === "darwin" && failure?.code === 1 && !failure.killed
|
|
493
|
+
&& failure.signal == null && failure.stdout === "" && failure.stderr === "") return "mismatch" as const;
|
|
494
|
+
// execFile's timeout kills with SIGTERM; other spawn/exit/buffer errors are lookup failures.
|
|
495
|
+
return failure?.killed === true && failure.signal === "SIGTERM" && failure.code === null
|
|
496
|
+
? "deadline" as const : "lookup_error" as const;
|
|
497
|
+
},
|
|
498
|
+
);
|
|
499
|
+
try { return result(await Promise.race([inspected, deadline])); }
|
|
500
|
+
finally { cancelDeadline?.(); }
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** Boolean compatibility API: only a proven match admits the listener. */
|
|
504
|
+
export async function ownsIpv4LoopbackListener(
|
|
505
|
+
port: number,
|
|
506
|
+
expectedPid: number,
|
|
507
|
+
io: LoopbackOwnerLookupIo = {},
|
|
508
|
+
): Promise<boolean> {
|
|
509
|
+
return (await inspectIpv4LoopbackListener(port, expectedPid, io)).kind === "match";
|
|
468
510
|
}
|
|
469
511
|
|
|
470
512
|
/**
|
|
@@ -172,6 +172,55 @@ export function conversationIdFromResponsesRequest(input: {
|
|
|
172
172
|
);
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Hermes Agent session affinity header for log correlation (#330).
|
|
177
|
+
* Kept separate from runtime/session identities so it cannot affect lanes or routing.
|
|
178
|
+
*/
|
|
179
|
+
export function conversationIdFromHermesHeader(headers: Headers): string | undefined {
|
|
180
|
+
return normalizeLogConversationId(headers.get("x-hermes-conversation-id"));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Validates and normalizes native header candidates in priority order for logging:
|
|
185
|
+
* parent-thread -> session_id -> session-id -> thread-id.
|
|
186
|
+
* Each candidate spelling is evaluated and normalized independently so an empty or
|
|
187
|
+
* whitespace-only candidate (e.g. empty session_id) cannot suppress a subsequent valid candidate.
|
|
188
|
+
*/
|
|
189
|
+
export function nativeLogConversationIdFromHeaders(headers: Headers): string | undefined {
|
|
190
|
+
return (
|
|
191
|
+
normalizeLogConversationId(headers.get("x-codex-parent-thread-id")) ??
|
|
192
|
+
normalizeLogConversationId(headers.get("session_id")) ??
|
|
193
|
+
normalizeLogConversationId(headers.get("session-id")) ??
|
|
194
|
+
normalizeLogConversationId(headers.get("thread-id"))
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** The request-log context's conversation slot; structural so callers need not import request-log. */
|
|
199
|
+
export interface LogConversationSlot {
|
|
200
|
+
conversationId?: string;
|
|
201
|
+
conversationIdIsHermesFallback?: boolean;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Record a native log identity. Fills an empty slot or replaces the log-only Hermes fallback,
|
|
206
|
+
* so a native id found late (Responses preparation, a Cursor conversation created mid-turn)
|
|
207
|
+
* still wins. A caller-owned id already in the slot (routed Claude metadata) is kept.
|
|
208
|
+
*/
|
|
209
|
+
export function assignNativeLogConversationId(slot: LogConversationSlot, nativeId: string | undefined): void {
|
|
210
|
+
if (!nativeId || (slot.conversationId && !slot.conversationIdIsHermesFallback)) return;
|
|
211
|
+
slot.conversationId = nativeId;
|
|
212
|
+
delete slot.conversationIdIsHermesFallback;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Fill an empty slot with the log-only Hermes digest, marked so any native id replaces it. */
|
|
216
|
+
export function fillHermesLogConversationFallback(slot: LogConversationSlot, headers: Headers): void {
|
|
217
|
+
if (slot.conversationId) return;
|
|
218
|
+
const hermes = conversationIdFromHermesHeader(headers);
|
|
219
|
+
if (!hermes) return;
|
|
220
|
+
slot.conversationId = hermes;
|
|
221
|
+
slot.conversationIdIsHermesFallback = true;
|
|
222
|
+
}
|
|
223
|
+
|
|
175
224
|
/**
|
|
176
225
|
* Claude Code metadata.user_id only — never the system-hash Desktop fallback.
|
|
177
226
|
* Hashes the raw user_id once (same opacity goal as inbound prompt_cache_key).
|
|
@@ -150,6 +150,8 @@ export interface RequestLogContext extends UpstreamErrorDiagnostics {
|
|
|
150
150
|
lastOutputAt?: number;
|
|
151
151
|
/** Best-effort chat/session correlation for Logs grouping (#330). Opaque; omit when unknown. */
|
|
152
152
|
conversationId?: string;
|
|
153
|
+
/** In-memory only: conversationId holds the log-only Hermes digest, which a native id replaces. */
|
|
154
|
+
conversationIdIsHermesFallback?: boolean;
|
|
153
155
|
surface?: "claude" | "claude-desktop" | "grok";
|
|
154
156
|
/** The matched configured key's id. Set ONLY for admissionKind "configured" —
|
|
155
157
|
* never a sentinel, so a hand-edited entry whose id happens to be "loopback"
|
|
@@ -130,6 +130,7 @@ export function createAnthropicThinkingRecovery(
|
|
|
130
130
|
|| response.status !== "completed" || !Array.isArray(response.output) || response.output.length === 0
|
|
131
131
|
|| !rejectedScope
|
|
132
132
|
|| rejectedScope.clientThreadId !== parsed._reasoningReplayScope?.clientThreadId
|
|
133
|
+
|| rejectedScope.clientPrincipalId !== parsed._reasoningReplayScope?.clientPrincipalId
|
|
133
134
|
|| JSON.stringify(rejectedScope.current) !== JSON.stringify(parsed._reasoningReplayScope?.current)) return;
|
|
134
135
|
remembered = true;
|
|
135
136
|
rememberReasoningReplayOpaqueBlobRejection(rejectedScope, rejectedPayloads);
|
|
@@ -129,6 +129,7 @@ import {
|
|
|
129
129
|
ForwardAdmissionCredentialError,
|
|
130
130
|
contextPrincipalIdOf,
|
|
131
131
|
hasForwardableCodexBearer,
|
|
132
|
+
resolveContextPrincipal,
|
|
132
133
|
validateForwardAdmissionCredential,
|
|
133
134
|
} from "../auth-cors";
|
|
134
135
|
import type { DataPlaneAdmission } from "../auth-cors";
|
|
@@ -1002,7 +1003,12 @@ export async function handleResponsesCompact(
|
|
|
1002
1003
|
cursorConversationId: gatewayReplayParsed._cursorConversationId,
|
|
1003
1004
|
sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
|
|
1004
1005
|
});
|
|
1005
|
-
if (clientThreadId)
|
|
1006
|
+
if (clientThreadId) {
|
|
1007
|
+
gatewayReplayParsed._reasoningReplayScope = {
|
|
1008
|
+
clientThreadId,
|
|
1009
|
+
clientPrincipalId: resolveContextPrincipal(req, config, admission),
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1006
1012
|
}
|
|
1007
1013
|
let compactUrl = gatewayTransport ? `${gatewayTransport.url.replace(/\/+$/, "")}/compact` : `${base}/responses/compact`;
|
|
1008
1014
|
const compactTargetKey = `${route.providerName}|${route.modelId}|compact`;
|
|
@@ -1723,7 +1729,9 @@ export async function handleResponsesCompact(
|
|
|
1723
1729
|
// The routed compaction turn is a handoff inside the same logical request, so it draws the
|
|
1724
1730
|
// REMAINDER. Minting here is what let a native attempt spend three sends and the routed
|
|
1725
1731
|
// fallback spend four more.
|
|
1726
|
-
|
|
1732
|
+
// internalReq keeps only FORWARD_HEADERS, so the caller principal is resolved from req.
|
|
1733
|
+
const replayPrincipal = { clientPrincipalId: resolveContextPrincipal(req, config, admission) };
|
|
1734
|
+
const response = await handleResponses(internalReq, config, logCtx, { abortSignal: req.signal, turnAdmissionLease, sendBudget, compactionRecoveryKind: "compaction-v1", compactionRoutingOverride: options.compactionRoutingOverride, replayPrincipal, ...(admission ? { admission } : {}) });
|
|
1727
1735
|
if (req.signal.aborted) {
|
|
1728
1736
|
void response.body?.cancel(req.signal.reason).catch(() => undefined);
|
|
1729
1737
|
return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
|
|
@@ -68,7 +68,7 @@ import {
|
|
|
68
68
|
import { codexAccountLogLabel, oauthAccountLogLabel } from "../../codex/account-label";
|
|
69
69
|
import { isGenericFailoverProvider, rotateGenericOAuthAccountOn429 } from "../../oauth/generic-account-failover";
|
|
70
70
|
import { codexQuotaScopeForModel, getCodexQuotaHealthSnapshot } from "../../codex/routing";
|
|
71
|
-
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
71
|
+
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
|
|
72
72
|
import type { AgentTaskRecoveryFailureReason } from "./agent-task-recovery";
|
|
73
73
|
import {
|
|
74
74
|
agentTaskRecoveryConfig,
|
|
@@ -76,6 +76,7 @@ import {
|
|
|
76
76
|
recoverEncryptedAgentTaskWithResult,
|
|
77
77
|
} from "./agent-task-recovery";
|
|
78
78
|
import { isThreadSpawnRequest, supportedLadderFor } from "../effort-policy";
|
|
79
|
+
import { jevChoiceEfforts } from "./jev-combo-choice";
|
|
79
80
|
import { applyDroidResponsesReasoningDefault } from "../droid-reasoning-default";
|
|
80
81
|
import { isPlainObject } from "../../lib/plain-data";
|
|
81
82
|
import {
|
|
@@ -102,6 +103,7 @@ import type { ResponsesTerminalStatus } from "../../bridge";
|
|
|
102
103
|
import { beginRequestAttempt, sealRequestAttemptIdentity, finishRequestAttempt } from "../request-log";
|
|
103
104
|
import { rememberComboForLane } from "./combo-session-recall";
|
|
104
105
|
import { COMBO_TOOL_LOOP_PIN_REASON, toolLoopPinnedPick } from "./combo-tool-loop-pin";
|
|
106
|
+
import { JEV_SESSION_PIN_ROUTE_REASON, jevSessionPinScope } from "./jev-session-pin";
|
|
105
107
|
import { runTurnAdapterSseResponses } from "./core-lifetime";
|
|
106
108
|
import { normalizePersistedJevDecision } from "../../usage/jev-stats";
|
|
107
109
|
import {
|
|
@@ -118,6 +120,7 @@ import { createComboProtocolLanes, dispatchNativeComboChild } from "./core-combo
|
|
|
118
120
|
import { createJevModelInvoker } from "./jev-model-invoke";
|
|
119
121
|
import { comboRequestedEffortLabel } from "./combo-requested-effort";
|
|
120
122
|
import { clientWireOf } from "../inference/client-wire";
|
|
123
|
+
import { resolveContextPrincipal } from "../auth-cors";
|
|
121
124
|
|
|
122
125
|
/**
|
|
123
126
|
* Sends one combo target may run on its own before the ladder moves on. A target is a whole
|
|
@@ -316,30 +319,6 @@ interface JevComboChoice {
|
|
|
316
319
|
candidate: JevCandidate;
|
|
317
320
|
}
|
|
318
321
|
|
|
319
|
-
/** A target's JEV effort choices, or undefined when an explicit allowlist has none still supported. */
|
|
320
|
-
function jevChoiceEfforts(
|
|
321
|
-
config: OcxConfig,
|
|
322
|
-
target: NonNullable<ReturnType<typeof getCombo>>["targets"][number],
|
|
323
|
-
): OcxComboDefaultEffort[] | undefined {
|
|
324
|
-
let ladder: string[] | undefined;
|
|
325
|
-
try {
|
|
326
|
-
const route = routeConcreteModel(config, targetKey(target));
|
|
327
|
-
ladder = supportedLadderFor({ provider: route.provider, modelId: route.modelId });
|
|
328
|
-
} catch {
|
|
329
|
-
// Preserve the existing routing-failure surface. Unknown capability becomes the explicit
|
|
330
|
-
// no-effort choice rather than broadening JEV's effort allowlist.
|
|
331
|
-
ladder = undefined;
|
|
332
|
-
}
|
|
333
|
-
const supportedEfforts = (ladder ?? []).filter(isCodexReasoningEffort) as OcxComboDefaultEffort[];
|
|
334
|
-
const configuredEfforts = target.reasoningEfforts;
|
|
335
|
-
const reasoningEfforts = configuredEfforts === undefined
|
|
336
|
-
? supportedEfforts
|
|
337
|
-
: configuredEfforts.filter(effort => supportedEfforts.includes(effort));
|
|
338
|
-
// An explicit allowlist is restrictive. If catalog capabilities drift until no configured
|
|
339
|
-
// effort remains supported, omit the target instead of silently broadening JEV's choices.
|
|
340
|
-
return configuredEfforts !== undefined && reasoningEfforts.length === 0 ? undefined : reasoningEfforts;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
322
|
/** Enumerate the current ordinary Combo eligibility set without retaining attempted picks. */
|
|
344
323
|
function eligibleJevComboChoices(
|
|
345
324
|
config: OcxConfig,
|
|
@@ -358,15 +337,15 @@ function eligibleJevComboChoices(
|
|
|
358
337
|
excluded.add(key);
|
|
359
338
|
// The TypeSafe row owns a decision credential, not an inference transport.
|
|
360
339
|
if (pick.target.provider === JEV_PROVIDER_ID) continue;
|
|
361
|
-
const
|
|
362
|
-
if (!
|
|
340
|
+
const identity = jevChoiceEfforts(config, pick.target, combo.decisionQuotaSignals === true);
|
|
341
|
+
if (!identity) continue;
|
|
363
342
|
choices.push({
|
|
364
343
|
pick: { ...pick, attempted: [key] },
|
|
365
344
|
candidate: {
|
|
366
345
|
key,
|
|
367
346
|
provider: pick.target.provider,
|
|
368
347
|
model: pick.target.model,
|
|
369
|
-
|
|
348
|
+
...identity,
|
|
370
349
|
modelProfile: pick.target.modelProfile,
|
|
371
350
|
},
|
|
372
351
|
});
|
|
@@ -405,6 +384,10 @@ export async function executeComboResponses(
|
|
|
405
384
|
if (!combo) {
|
|
406
385
|
return formatErrorResponse(404, "invalid_request_error", `Unknown combo: ${comboId}`);
|
|
407
386
|
}
|
|
387
|
+
// Resolve against the original request once; every bridge candidate receives the same trusted
|
|
388
|
+
// result after its provisional Authorization is removed, including an explicitly keyless result.
|
|
389
|
+
const replayPrincipal = options.replayPrincipal
|
|
390
|
+
?? { clientPrincipalId: resolveContextPrincipal(req, config, options.admission) };
|
|
408
391
|
// PF-07: present only for a Chat combo with `nativeChatCombos` on; otherwise every child
|
|
409
392
|
// takes the bridge below exactly as before.
|
|
410
393
|
const protocolLanes = createComboProtocolLanes({
|
|
@@ -481,6 +464,7 @@ export async function executeComboResponses(
|
|
|
481
464
|
const unavailable = mandatoryResponsesReasoningReplayUnavailable({
|
|
482
465
|
body,
|
|
483
466
|
clientThreadId: reasoningReplayConversationId,
|
|
467
|
+
clientPrincipalId: replayPrincipal.clientPrincipalId,
|
|
484
468
|
providerName: route.providerName,
|
|
485
469
|
provider: route.provider,
|
|
486
470
|
adapterName: route.provider.adapter,
|
|
@@ -623,6 +607,10 @@ export async function executeComboResponses(
|
|
|
623
607
|
eligible: targetEligible,
|
|
624
608
|
now: initialNow,
|
|
625
609
|
});
|
|
610
|
+
// Opt-in JEV session pin (#7525); override routes neither read nor write it, like the lane recall.
|
|
611
|
+
const jevPin = options.compactionRoutingOverride || options.mainHardLockFallbackOverride
|
|
612
|
+
? undefined
|
|
613
|
+
: jevSessionPinScope({ combo, comboId, admission: options.admission, lane: sessionLaneIdFromRequest(req.headers) });
|
|
626
614
|
|
|
627
615
|
if (unreadableEncryptedAgentTask && !pick) {
|
|
628
616
|
pick = await pickWithWait({ now: initialNow });
|
|
@@ -684,7 +672,7 @@ export async function executeComboResponses(
|
|
|
684
672
|
const first = choices[0];
|
|
685
673
|
if (!first) {
|
|
686
674
|
return comboUnavailable(comboId, undefined, comboExclusionReasons(config, comboId, target =>
|
|
687
|
-
targetEligible(target) && target.provider !== JEV_PROVIDER_ID && jevChoiceEfforts(config, target) !== undefined));
|
|
675
|
+
targetEligible(target) && target.provider !== JEV_PROVIDER_ID && jevChoiceEfforts(config, target, false) !== undefined));
|
|
688
676
|
}
|
|
689
677
|
const resolvedFailOpenEffort = resolveEffortAtOrBelow(
|
|
690
678
|
"medium",
|
|
@@ -697,8 +685,13 @@ export async function executeComboResponses(
|
|
|
697
685
|
: null,
|
|
698
686
|
};
|
|
699
687
|
const decisionStartedAt = Date.now();
|
|
688
|
+
const pinReuse = jevPin?.reuse(
|
|
689
|
+
choices.map(choice => choice.candidate),
|
|
690
|
+
combo.decisionQuotaSignals === true ? { config, tiers: combo.decisionQuotaTiers } : undefined,
|
|
691
|
+
);
|
|
700
692
|
let decision: JevDecision;
|
|
701
|
-
|
|
693
|
+
if (pinReuse?.decision) decision = pinReuse.decision;
|
|
694
|
+
else try {
|
|
702
695
|
decision = await resolveJevComboDecision({
|
|
703
696
|
body,
|
|
704
697
|
candidates: choices.map(choice => choice.candidate),
|
|
@@ -732,6 +725,7 @@ export async function executeComboResponses(
|
|
|
732
725
|
latencyMs: Math.max(0, Date.now() - decisionStartedAt),
|
|
733
726
|
};
|
|
734
727
|
}
|
|
728
|
+
if (pinReuse?.released) decision = { ...decision, pinReason: pinReuse.released };
|
|
735
729
|
jevDecision = decision;
|
|
736
730
|
const selected = choices.find(choice => choice.candidate.key === decision.targetKey) ?? first;
|
|
737
731
|
pick = { ...selected.pick, attempted: [targetKey(selected.pick.target)] };
|
|
@@ -752,6 +746,8 @@ export async function executeComboResponses(
|
|
|
752
746
|
: {}),
|
|
753
747
|
...(decision.usage ? { usage: decision.usage } : {}),
|
|
754
748
|
...(decision.quota ? { quota: decision.quota } : {}),
|
|
749
|
+
...(decision.pinned ? { pinned: true } : {}),
|
|
750
|
+
...(decision.pinReason ? { pinReason: decision.pinReason } : {}),
|
|
755
751
|
});
|
|
756
752
|
console.debug("[combo] JEV decision", {
|
|
757
753
|
backend: decision.backend,
|
|
@@ -759,6 +755,7 @@ export async function executeComboResponses(
|
|
|
759
755
|
effort: decision.effort,
|
|
760
756
|
gate: decision.gate,
|
|
761
757
|
latencyMs: decision.latencyMs,
|
|
758
|
+
...(decision.pinReason ? { pinReason: decision.pinReason } : {}),
|
|
762
759
|
...(decision.confidence !== undefined ? { confidence: decision.confidence } : {}),
|
|
763
760
|
...(decision.chosenProbability !== undefined
|
|
764
761
|
? { chosenProbability: decision.chosenProbability }
|
|
@@ -771,7 +768,7 @@ export async function executeComboResponses(
|
|
|
771
768
|
const decision = comboRouteDecisionTrace(config, comboId, pick, requestedModel);
|
|
772
769
|
const selectedReason = options.memoryModelPhase
|
|
773
770
|
? memoryModelRouteReason(options.memoryModelPhase)
|
|
774
|
-
: toolLoopPin ? COMBO_TOOL_LOOP_PIN_REASON : undefined;
|
|
771
|
+
: toolLoopPin ? COMBO_TOOL_LOOP_PIN_REASON : jevDecision?.pinned ? JEV_SESSION_PIN_ROUTE_REASON : undefined;
|
|
775
772
|
logCtx.routeDecision = selectedReason
|
|
776
773
|
? { ...decision, selected: { ...decision.selected, reason: selectedReason } }
|
|
777
774
|
: decision;
|
|
@@ -858,6 +855,22 @@ export async function executeComboResponses(
|
|
|
858
855
|
// The physical destination after provider aliases and blockedModelRedirects: hops are
|
|
859
856
|
// classified by where the request really goes, not by the selector the combo names.
|
|
860
857
|
const targetRoute = routeConcreteModel(config, `${pick.target.provider}/${pick.target.model}`);
|
|
858
|
+
// The hard-lock rewrite may have previewed a different random pick. Enforce this actual
|
|
859
|
+
// first destination against the locked source, after aliases and redirects resolve.
|
|
860
|
+
if (firstComboTarget && options.mainHardLockFallbackOverride) {
|
|
861
|
+
const hop = guardFallbackHop(config, {
|
|
862
|
+
site: "combo",
|
|
863
|
+
route: `combo/${comboId}`,
|
|
864
|
+
aliases: [requestedModel],
|
|
865
|
+
from: { provider: OPENAI_CODEX_PROVIDER_ID, model: options.mainHardLockFallbackOverride.sourceModel },
|
|
866
|
+
to: { provider: targetRoute.providerName, model: targetRoute.modelId },
|
|
867
|
+
trigger: "hard_lock",
|
|
868
|
+
admission: options.admission,
|
|
869
|
+
});
|
|
870
|
+
if (!hop.ok) return hop.response;
|
|
871
|
+
const declaredCommit = pendingHopCommit;
|
|
872
|
+
pendingHopCommit = declaredCommit ? () => { declaredCommit(); hop.commit(); } : hop.commit;
|
|
873
|
+
}
|
|
861
874
|
// OCX-11: every later dispatch after a failure is a hop when the target changed. One check
|
|
862
875
|
// here covers the ordinary failover pick and the recovery picks alike.
|
|
863
876
|
if (!firstComboTarget && lastFailure && lastFailedTarget) {
|
|
@@ -1014,6 +1027,7 @@ export async function executeComboResponses(
|
|
|
1014
1027
|
&& provider && provider.disabled !== true
|
|
1015
1028
|
&& currentCombo?.targets.some(target => targetKey(target) === targetKey(completedTarget))) {
|
|
1016
1029
|
rememberComboForLane(sessionLaneIdFromRequest(req.headers), comboId, completedTarget, model, writerGeneration);
|
|
1030
|
+
jevPin?.complete(targetKey(completedTarget), initialJevDecision, writerGeneration);
|
|
1017
1031
|
}
|
|
1018
1032
|
options.onResponseComplete?.(model);
|
|
1019
1033
|
},
|
|
@@ -1026,6 +1040,8 @@ export async function executeComboResponses(
|
|
|
1026
1040
|
if (childLog.terminalIncompleteReason !== undefined) logCtx.terminalIncompleteReason = childLog.terminalIncompleteReason;
|
|
1027
1041
|
if (childLog.terminalErrorCode !== undefined) logCtx.terminalErrorCode = childLog.terminalErrorCode;
|
|
1028
1042
|
if (childLog.upstreamError !== undefined) logCtx.upstreamError = childLog.upstreamError;
|
|
1043
|
+
// A committed stream that ends failed is a target failure too; incomplete is an answer.
|
|
1044
|
+
if (status === "failed") jevPin?.forget();
|
|
1029
1045
|
options.onNativePassthroughTerminal?.(status);
|
|
1030
1046
|
},
|
|
1031
1047
|
});
|
|
@@ -1065,6 +1081,7 @@ export async function executeComboResponses(
|
|
|
1065
1081
|
},
|
|
1066
1082
|
}) : await requestDispatchers.handleResponses(childRequest, config, childLog, {
|
|
1067
1083
|
...options,
|
|
1084
|
+
replayPrincipal,
|
|
1068
1085
|
// A bridge child is a concrete route; the native source belongs to this loop only.
|
|
1069
1086
|
...(options.protocolSource ? { protocolSource: undefined } : {}),
|
|
1070
1087
|
// After the spread: the child must run on THIS target's ladder, not on the holder the
|
|
@@ -1099,6 +1116,7 @@ export async function executeComboResponses(
|
|
|
1099
1116
|
retainCancelledAttempt();
|
|
1100
1117
|
return clientCancelledResponse();
|
|
1101
1118
|
}
|
|
1119
|
+
jevPin?.forget();
|
|
1102
1120
|
finishRequestAttempt(attempt, 502, Date.now() - started, childLog.usage);
|
|
1103
1121
|
throw error;
|
|
1104
1122
|
}
|
|
@@ -1123,6 +1141,7 @@ export async function executeComboResponses(
|
|
|
1123
1141
|
retainCancelledAttempt();
|
|
1124
1142
|
return clientCancelledResponse();
|
|
1125
1143
|
}
|
|
1144
|
+
jevPin?.forget();
|
|
1126
1145
|
finishRequestAttempt(attempt, 502, Date.now() - started, childLog.usage);
|
|
1127
1146
|
throw error;
|
|
1128
1147
|
}
|
|
@@ -1179,6 +1198,7 @@ export async function executeComboResponses(
|
|
|
1179
1198
|
retainCancelledAttempt();
|
|
1180
1199
|
return clientCancelledResponse();
|
|
1181
1200
|
}
|
|
1201
|
+
jevPin?.forget();
|
|
1182
1202
|
finishRequestAttempt(attempt, 502, Date.now() - started, childLog.usage);
|
|
1183
1203
|
throw error;
|
|
1184
1204
|
}
|
|
@@ -1210,6 +1230,8 @@ export async function executeComboResponses(
|
|
|
1210
1230
|
}
|
|
1211
1231
|
attemptRetained = true;
|
|
1212
1232
|
lastFailure = failure.response;
|
|
1233
|
+
// A real target failure ends the session pin; the next turn is decided afresh (#7525).
|
|
1234
|
+
jevPin?.forget();
|
|
1213
1235
|
if (!quotaFailure && (lastFailure.status === 429 || lastFailure.status === 402)) {
|
|
1214
1236
|
quotaFailure = lastFailure;
|
|
1215
1237
|
quotaFailedChildLog = childLog;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { TranslatorBudget } from "../../lib/translator-budget";
|
|
2
2
|
import { carryResponseToolOwnership } from "./response-tool-ownership";
|
|
3
3
|
import { finalizeTranslatorBudgetResponse } from "../../lib/translator-budget";
|
|
4
|
-
import { isNonReplayableResponse, markResponseNonReplayable } from "../../lib/upstream-retry";
|
|
4
|
+
import { carryReplayRefusal, isNonReplayableResponse, markResponseNonReplayable } from "../../lib/upstream-retry";
|
|
5
5
|
import {
|
|
6
6
|
isNativePassthroughSseResponse,
|
|
7
7
|
markNativePassthroughSseResponse,
|
|
@@ -76,7 +76,7 @@ export function finalizeAccountLease(response: Response, release: () => void, on
|
|
|
76
76
|
if (isEagerRelaySseResponse(response)) markEagerRelaySseResponse(wrapped);
|
|
77
77
|
if (isPreinspectedJsonResponse(response)) markPreinspectedJsonResponse(wrapped);
|
|
78
78
|
if (runTurnAdapterSseResponses.has(response)) runTurnAdapterSseResponses.add(wrapped);
|
|
79
|
-
return carryResponseToolOwnership(response, wrapped);
|
|
79
|
+
return carryResponseToolOwnership(response, carryReplayRefusal(response, wrapped));
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
|
|
@@ -358,14 +358,17 @@ export async function applyFinalRouteRequestNormalization(args: {
|
|
|
358
358
|
|
|
359
359
|
{
|
|
360
360
|
const { nativeEffortClamp, shouldApplyNativeEffortClamp } = await import("../../codex/catalog");
|
|
361
|
+
const raw = parsed._rawBody as { reasoning?: { effort?: string } } | undefined;
|
|
362
|
+
// The parser folds `ultra` to `max` only in options.reasoning; the passthrough adapter
|
|
363
|
+
// serializes the raw body, so a raw `ultra` must reach the clamp itself (#7590).
|
|
364
|
+
const rawUltra = raw?.reasoning && typeof raw.reasoning === "object" && raw.reasoning.effort === "ultra";
|
|
361
365
|
const clamped = shouldApplyNativeEffortClamp(route.providerName, route.provider, finalSelectedModelId)
|
|
362
|
-
? nativeEffortClamp(route.modelId, parsed.options.reasoning)
|
|
366
|
+
? nativeEffortClamp(route.modelId, rawUltra ? "ultra" : parsed.options.reasoning)
|
|
363
367
|
: null;
|
|
364
368
|
if (clamped) {
|
|
365
369
|
parsed.options.reasoning = clamped;
|
|
366
|
-
const raw = parsed._rawBody as { reasoning?: { effort?: string } } | undefined;
|
|
367
370
|
if (raw?.reasoning && typeof raw.reasoning === "object") raw.reasoning.effort = clamped;
|
|
368
|
-
logCtx.requestedEffort = `${logCtx.requestedEffort ?? "max"}->${clamped}`;
|
|
371
|
+
logCtx.requestedEffort = `${logCtx.requestedEffort ?? (rawUltra ? "ultra" : "max")}->${clamped}`;
|
|
369
372
|
}
|
|
370
373
|
}
|
|
371
374
|
// Chat ingress cannot strip effort against a provisional policy pick. Apply the
|
|
@@ -547,6 +547,7 @@ export async function attemptOpaqueBlobRecovery<Failure = Response>(
|
|
|
547
547
|
const rejectedScope = args.parsed._reasoningReplayScope
|
|
548
548
|
? {
|
|
549
549
|
clientThreadId: args.parsed._reasoningReplayScope.clientThreadId,
|
|
550
|
+
clientPrincipalId: args.parsed._reasoningReplayScope.clientPrincipalId,
|
|
550
551
|
...(args.parsed._reasoningReplayScope.current
|
|
551
552
|
? { current: { ...args.parsed._reasoningReplayScope.current } }
|
|
552
553
|
: {}),
|