@bitkyc08/opencodex 2.68.0-preview.20260927 → 2.69.0-preview.20260928
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 +25 -17
- package/gui/dist/assets/App-cDPyCb_V.js +51 -0
- package/gui/dist/assets/{Tray-DFnMiyD0.js → Tray-H1IAB8mN.js} +1 -1
- package/gui/dist/assets/index-BF9QnKO_.css +1 -0
- package/gui/dist/assets/index-CI6YTp8z.js +86 -0
- package/gui/dist/assets/{quota-summary-i84eOU3o.js → quota-summary-CutOlPu2.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/factory-droid.svg +8 -0
- package/package.json +1 -1
- package/src/adapters/anthropic-output-schema.ts +109 -11
- package/src/adapters/coding-agent/protocol.ts +106 -11
- package/src/adapters/coding-agent/turn.ts +23 -18
- package/src/adapters/google-errors.ts +16 -1
- package/src/adapters/google-http.ts +14 -0
- package/src/adapters/openai-chat/passthrough.ts +2 -0
- package/src/adapters/openai-chat/summary-budget.ts +72 -0
- package/src/adapters/openai-chat/tool-call-id-remint.ts +11 -2
- package/src/adapters/openai-chat.ts +6 -9
- package/src/bridge/response-json.ts +3 -1
- package/src/bridge/sse.ts +3 -1
- package/src/claude/desktop-picker.ts +6 -2
- package/src/claude/desktop-system-proxy.ts +155 -0
- package/src/claude/inbound-model-options.ts +10 -2
- package/src/claude/inbound.ts +1 -1
- package/src/claude/intercept/picker-ca-cleanup.ts +31 -0
- package/src/claude/intercept/picker-ca.ts +283 -83
- package/src/claude/intercept/picker-models.ts +4 -1
- package/src/claude/intercept/picker-trust.ts +15 -2
- package/src/claude/intercept/runtime.ts +99 -32
- package/src/cli/account-api.ts +5 -2
- package/src/cli/account-extended.ts +47 -11
- package/src/cli/account-target.ts +12 -7
- package/src/cli/account.ts +47 -3
- package/src/cli/capabilities.ts +33 -7
- package/src/cli/claude-agent-startup-sync.ts +2 -0
- package/src/cli/claude-desktop.ts +37 -13
- package/src/cli/cross-home-owner.ts +138 -0
- package/src/cli/dispatch.ts +1 -1
- package/src/cli/doctor.ts +18 -8
- package/src/cli/ensure-desired-integrations.ts +5 -0
- package/src/cli/export-command.ts +6 -6
- package/src/cli/help.ts +1 -1
- package/src/cli/index.ts +15 -18
- package/src/cli/integrations.ts +8 -1
- package/src/cli/models-runtime-subcommands.ts +1 -0
- package/src/cli/models-runtime.ts +106 -4
- package/src/cli/models.ts +6 -1
- package/src/cli/opencode.ts +2 -83
- package/src/cli/registry.ts +3 -2
- package/src/cli/status.ts +86 -7
- package/src/client/connect.ts +24 -10
- package/src/client/link-ingress.ts +3 -1
- package/src/client/link-join.ts +76 -9
- package/src/client/link-relay-transport.ts +184 -0
- package/src/client/link-relay.ts +16 -4
- package/src/client/machine-listener.ts +2 -0
- package/src/clients/config-export/constants.ts +8 -0
- package/src/clients/config-export/contracts.ts +13 -1
- package/src/clients/config-export/droid.ts +71 -0
- package/src/clients/config-export/kilo.ts +116 -0
- package/src/clients/config-export.ts +33 -2
- package/src/codex/auth-api/routes.ts +1 -1
- package/src/codex/catalog/access-programs.ts +14 -2
- package/src/codex/catalog/provider-models.ts +1 -1
- package/src/codex/catalog-auto-refresh.ts +122 -0
- package/src/codex/config-drift-heal.ts +80 -0
- package/src/codex/desktop-switches.ts +7 -5
- package/src/codex/inject/bounded-config-reader.ts +69 -0
- package/src/codex/inject/config-toml.ts +31 -3
- package/src/codex/inject/routing-target.ts +6 -1
- package/src/codex/inject.ts +4 -0
- package/src/codex/model-entitlements.ts +32 -5
- package/src/codex/native-profile-startup.ts +17 -5
- package/src/codex/project-config-warnings.ts +72 -21
- package/src/codex/quota-auto-refresh-state.ts +8 -2
- package/src/codex/quota-auto-refresh.ts +80 -9
- package/src/codex/sync.ts +10 -0
- package/src/combos/failover.ts +4 -1
- package/src/combos/jev.ts +15 -4
- package/src/combos/types.ts +16 -0
- package/src/config/diagnostics.ts +19 -1
- package/src/config/load-degrade.ts +29 -0
- package/src/config/macos-system-proxy.ts +114 -0
- package/src/config/proxy-env.ts +67 -20
- package/src/config/schema/config-schema.ts +15 -0
- package/src/config/schema/leaf-validators.ts +23 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +220 -144
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/config-io.ts +12 -3
- package/src/integrations/droid-settings.ts +65 -0
- package/src/integrations/kilo-candidates.ts +30 -0
- package/src/integrations/mutation-plan.ts +62 -7
- package/src/integrations/native/ownership-preflight.ts +46 -12
- package/src/integrations/registry.ts +136 -7
- package/src/integrations/state.ts +52 -4
- package/src/integrations/target.ts +3 -0
- package/src/integrations/writer.ts +55 -14
- package/src/lib/account-selection-events.ts +14 -0
- package/src/lib/jsonc.ts +110 -0
- package/src/lib/standalone.ts +9 -1
- package/src/lib/windows-system-proxy.ts +101 -1
- package/src/link/relay-auth.ts +29 -0
- package/src/link/ssh-argv.ts +6 -3
- package/src/link/ssh-runner.ts +5 -5
- package/src/oauth/generic-account-failover.ts +52 -8
- package/src/oauth/index.ts +37 -14
- package/src/oauth/store.ts +96 -11
- package/src/oauth/token-guardian.ts +7 -3
- package/src/oauth/types.ts +2 -0
- package/src/plugins/loader.ts +5 -3
- package/src/providers/fastwire.ts +3 -0
- package/src/providers/key-failover.ts +13 -5
- package/src/providers/kiro-model-catalog.ts +4 -3
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/openai-tiers-destination.ts +9 -0
- package/src/providers/quota/account-cache.ts +17 -3
- package/src/providers/quota/vendor-probes-oauth.ts +2 -0
- package/src/providers/quota.ts +2 -2
- package/src/responses/code-mode-helper-compat.ts +14 -1
- package/src/responses/custom-tool-compat.ts +1 -1
- package/src/server/auth-cors.ts +1 -0
- package/src/server/images.ts +70 -20
- package/src/server/index/link-listener.ts +20 -3
- package/src/server/index/link-relay-sessions.ts +138 -0
- package/src/server/index/optional-listeners.ts +11 -1
- package/src/server/index/serve-options.ts +12 -0
- package/src/server/inference/client-encoder-delivery.ts +1 -0
- package/src/server/local-management-read-client.ts +21 -2
- package/src/server/management/agent-settings-routes.ts +19 -5
- package/src/server/management/combo-routes.ts +5 -3
- package/src/server/management/config-routes.ts +32 -6
- package/src/server/management/model-routes.ts +232 -7
- package/src/server/management/model-rows.ts +199 -1
- package/src/server/management/oauth-account-routes.ts +63 -3
- package/src/server/management/route-registry.ts +2 -0
- package/src/server/port-reclaim.ts +170 -32
- package/src/server/responses/adapter-delivery.ts +6 -4
- package/src/server/responses/adapter-dispatch.ts +72 -2
- package/src/server/responses/core-combo.ts +6 -1
- package/src/server/responses/core-normalize.ts +19 -4
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/memory-models.ts +169 -0
- package/src/server/responses/passthrough-delivery.ts +4 -0
- package/src/server/responses/passthrough-dispatch.ts +10 -0
- package/src/server/responses/request-prepare.ts +86 -4
- package/src/server/responses/request-transport.ts +4 -0
- package/src/server/responses/run-turn-execution.ts +5 -2
- package/src/server/responses/shadow-target-availability.ts +17 -4
- package/src/server/responses/sidecar-execution.ts +18 -2
- package/src/server/responses/skills-snapshot.ts +239 -0
- package/src/server/responses-undeclared-tool-guard.ts +43 -6
- package/src/server/search.ts +1 -1
- package/src/service/guards.ts +4 -5
- package/src/service/state.ts +39 -2
- package/src/service.ts +2 -2
- package/src/types/config.ts +46 -4
- package/src/types/provider.ts +9 -0
- package/src/types/request.ts +6 -0
- package/src/types/tools.ts +38 -8
- package/src/types.ts +3 -0
- package/src/update/async-check.ts +5 -1
- package/src/update/index.ts +30 -12
- package/src/update/job.ts +3 -1
- package/src/update/pnpm-read-policy.d.mts +4 -0
- package/src/update/pnpm-read-policy.mjs +44 -0
- package/src/update/worker-launch.ts +152 -11
- package/src/usage/cost.ts +6 -3
- package/src/usage/log.ts +4 -0
- package/src/web-search/backends.ts +2 -2
- package/src/web-search/devin-executor.ts +1 -1
- package/src/web-search/sidecar-providers.ts +5 -5
- package/gui/dist/assets/App-B5-cAEd2.js +0 -51
- package/gui/dist/assets/index-BDUBS8PW.css +0 -1
- package/gui/dist/assets/index-BcXblvem.js +0 -86
|
@@ -13,7 +13,14 @@ import {
|
|
|
13
13
|
} from "./core-errors";
|
|
14
14
|
import { parseSyntheticRowId } from "../fast-row";
|
|
15
15
|
import { resolveComboId, comboIdFromRawBody, NoAvailableComboTargetsError } from "../../combos";
|
|
16
|
-
import { INTERCEPT_TARGET_UNAVAILABLE_CODE, interceptTargetUnavailableResponse, resolveShadowCallTarget } from "./shadow-target-availability";
|
|
16
|
+
import { INTERCEPT_TARGET_UNAVAILABLE_CODE, interceptTargetUnavailableResponse, resolveChosenTarget, resolveShadowCallTarget } from "./shadow-target-availability";
|
|
17
|
+
import {
|
|
18
|
+
MEMORY_MODEL_TARGET_UNAVAILABLE_CODE,
|
|
19
|
+
configuredMemoryModel,
|
|
20
|
+
detectMemoryModelPhase,
|
|
21
|
+
memoryModelRouteReason,
|
|
22
|
+
memoryModelTargetUnavailableResponse,
|
|
23
|
+
} from "./memory-models";
|
|
17
24
|
import { recallComboForLane } from "./combo-session-recall";
|
|
18
25
|
import {
|
|
19
26
|
sessionLaneIdFromRequest,
|
|
@@ -22,6 +29,7 @@ import {
|
|
|
22
29
|
reasoningReplayConversationIdFromResponsesRequest,
|
|
23
30
|
} from "../request-log-conversation";
|
|
24
31
|
import { resolveContextPrincipal } from "../auth-cors";
|
|
32
|
+
import { resolveSkillsSnapshotScopeKey, snapshotSkillsCatalogInBody } from "./skills-snapshot";
|
|
25
33
|
import {
|
|
26
34
|
isShadowSourceModel,
|
|
27
35
|
shadowSourceModelPrefix,
|
|
@@ -174,6 +182,20 @@ export async function prepareResponsesRequest(
|
|
|
174
182
|
transport: options.inboundTransport,
|
|
175
183
|
});
|
|
176
184
|
}
|
|
185
|
+
// Codex's memory pipeline names a destination per phase. The phase is read from Codex's own turn
|
|
186
|
+
// metadata, never from the model id: Phase 1 shares `gpt-5.6-luna` with the app's title/commit
|
|
187
|
+
// helper calls. Read here, ahead of the shadow intercept below, because the phase decision is the
|
|
188
|
+
// more specific of the two settings and must be the one that survives when both match one request.
|
|
189
|
+
const memoryModelPhase = options.memoryModelPhase
|
|
190
|
+
?? ((config.memoryModels?.extract || config.memoryModels?.consolidation)
|
|
191
|
+
&& !options.comboAttempt && !options.compactionRoutingOverride && inboundWire === "responses"
|
|
192
|
+
? detectMemoryModelPhase(body, req.headers, { transport: options.inboundTransport }) ?? undefined
|
|
193
|
+
: undefined);
|
|
194
|
+
const memoryModelTarget = memoryModelPhase ? configuredMemoryModel(config, memoryModelPhase) : undefined;
|
|
195
|
+
// A combo child is a synthetic replay of the parent's decision: its model is already the target's
|
|
196
|
+
// concrete provider/model, so neither site below may rewrite or re-resolve it. It keeps the phase
|
|
197
|
+
// through `options.memoryModelPhase` instead, which is what applies the phase effort.
|
|
198
|
+
const memoryModelApplies = memoryModelTarget !== undefined && options.comboAttempt !== true;
|
|
177
199
|
options.onRequestBodyParsed?.(body);
|
|
178
200
|
// An effort row naming a table-less combo (`combo/x--high`) must reach the combo dispatcher
|
|
179
201
|
// as its base id, so the selector is normalized here, before comboIdFromRawBody reads model.
|
|
@@ -228,11 +250,22 @@ export async function prepareResponsesRequest(
|
|
|
228
250
|
// hops — which only exist inside that loop — are unreachable (#4129). Rewrite the selector
|
|
229
251
|
// here instead, before comboIdFromRawBody reads `model`, and identify the combo by CONFIG
|
|
230
252
|
// LOOKUP so the check can never observe a one-candidate collapse.
|
|
253
|
+
// A memory target that names a combo has to reach the combo dispatcher as `model`, or its own
|
|
254
|
+
// failover loop is unreachable (#4129) — the same reason the shadow intercept rewrites its combo
|
|
255
|
+
// target here. Every other target is resolved at the late site, where the admission scope exists.
|
|
256
|
+
let memoryModelComboRouted = false;
|
|
257
|
+
if (memoryModelApplies && memoryModelTarget && body && typeof body === "object" && !Array.isArray(body)) {
|
|
258
|
+
const memoryComboId = resolveComboId(config, memoryModelTarget.model);
|
|
259
|
+
if (memoryComboId && Object.hasOwn(config.combos ?? {}, memoryComboId)) {
|
|
260
|
+
memoryModelComboRouted = true;
|
|
261
|
+
(body as Record<string, unknown>).model = memoryModelTarget.model;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
231
264
|
let shadowCallIntercepted = false;
|
|
232
265
|
// A spawned sub-agent turn names its model on purpose; gpt-6-luna is both the helper
|
|
233
266
|
// slug and a default sub-agent model, so neither intercept site may rewrite that turn.
|
|
234
267
|
const threadSpawn = isThreadSpawnRequest(req.headers);
|
|
235
|
-
if (!options.comboAttempt && !options.compactionRoutingOverride && !threadSpawn && body && typeof body === "object" && !Array.isArray(body)) {
|
|
268
|
+
if (!options.comboAttempt && !options.compactionRoutingOverride && !threadSpawn && !memoryModelApplies && body && typeof body === "object" && !Array.isArray(body)) {
|
|
236
269
|
const shadowIntercept = config.shadowCallIntercept;
|
|
237
270
|
const rawShadowModel = (body as { model?: unknown }).model;
|
|
238
271
|
if (shadowIntercept?.enabled && shadowIntercept.model && typeof rawShadowModel === "string"
|
|
@@ -258,6 +291,9 @@ export async function prepareResponsesRequest(
|
|
|
258
291
|
// Concrete combo child selectors no longer match the shadow source model. Carry the
|
|
259
292
|
// interception decision explicitly so provider-specific helper isolation still applies.
|
|
260
293
|
shadowCallIntercepted,
|
|
294
|
+
// Same handoff for a memory phase whose target is a combo: the child keeps the phase's effort
|
|
295
|
+
// override and stays out of the parent conversation.
|
|
296
|
+
memoryModelPhase: memoryModelComboRouted ? memoryModelPhase : undefined,
|
|
261
297
|
// The original request body was accepted above. Combo children are synthetic
|
|
262
298
|
// replays and must not repeat the caller-owned timeout transition.
|
|
263
299
|
onRequestBodyRead: undefined,
|
|
@@ -314,6 +350,15 @@ export async function prepareResponsesRequest(
|
|
|
314
350
|
);
|
|
315
351
|
}
|
|
316
352
|
|
|
353
|
+
const skillsSnapshotScopeKey = resolveSkillsSnapshotScopeKey({
|
|
354
|
+
req,
|
|
355
|
+
config,
|
|
356
|
+
admission: options.admission,
|
|
357
|
+
promptCacheKeyIsSharedCohort: options.promptCacheKeyIsSharedCohort,
|
|
358
|
+
});
|
|
359
|
+
// Substitutes a known snapshot now; a new catalog is only stored once the request is prepared.
|
|
360
|
+
const commitSkillsSnapshot = snapshotSkillsCatalogInBody(body, skillsSnapshotScopeKey, config);
|
|
361
|
+
|
|
317
362
|
let parsed: OcxParsedRequest;
|
|
318
363
|
let toolBridgeMaps: ReturnType<typeof buildToolBridgeMaps>;
|
|
319
364
|
try {
|
|
@@ -381,6 +426,10 @@ export async function prepareResponsesRequest(
|
|
|
381
426
|
}
|
|
382
427
|
if (cursorClientThreadId) parsed._cursorClientThreadId = cursorClientThreadId;
|
|
383
428
|
if (options.shadowCallIntercepted === true) parsed._cursorIsolateConversation = true;
|
|
429
|
+
if (options.memoryModelPhase !== undefined) {
|
|
430
|
+
parsed._memoryModelPhase = options.memoryModelPhase;
|
|
431
|
+
parsed._cursorIsolateConversation = true;
|
|
432
|
+
}
|
|
384
433
|
} catch (err) {
|
|
385
434
|
if (isTranslatorBudgetExceededError(err)) {
|
|
386
435
|
return formatErrorResponse(413, "request_too_large", "request translation buffer exceeded the safe limit", {
|
|
@@ -484,9 +533,28 @@ export async function prepareResponsesRequest(
|
|
|
484
533
|
: parsed._compactionRequest === true
|
|
485
534
|
? routeCompactionModel(config, modelId, evidenceFromBody(parsed._rawBody))
|
|
486
535
|
: routeModel(config, modelId, evidenceFromBody(parsed._rawBody)));
|
|
536
|
+
// The phase's destination. Resolved through the admission-scoped resolver every other route
|
|
537
|
+
// uses, and it fails closed exactly like the shadow target: falling back to the native model
|
|
538
|
+
// would spend the quota the operator routed away from, without their choosing it.
|
|
539
|
+
let memoryRoute: RouteResult | undefined;
|
|
540
|
+
if (memoryModelApplies && memoryModelPhase && memoryModelTarget) {
|
|
541
|
+
const memoryTarget = resolveChosenTarget(memoryModelTarget.model, resolveRoute);
|
|
542
|
+
if ("unavailable" in memoryTarget) {
|
|
543
|
+
logCtx.errorCode = MEMORY_MODEL_TARGET_UNAVAILABLE_CODE;
|
|
544
|
+
return memoryModelTargetUnavailableResponse(memoryModelPhase);
|
|
545
|
+
}
|
|
546
|
+
credentialDomainWasRewritten = true;
|
|
547
|
+
parsed.modelId = memoryModelTarget.model;
|
|
548
|
+
if (parsed._rawBody && typeof parsed._rawBody === "object") {
|
|
549
|
+
(parsed._rawBody as { model?: string }).model = memoryModelTarget.model;
|
|
550
|
+
}
|
|
551
|
+
parsed._memoryModelPhase = memoryModelPhase;
|
|
552
|
+
parsed._cursorIsolateConversation = true;
|
|
553
|
+
memoryRoute = memoryTarget.route;
|
|
554
|
+
}
|
|
487
555
|
const _sci = config.shadowCallIntercept;
|
|
488
556
|
let shadowRoute: RouteResult | undefined;
|
|
489
|
-
if (!options.compactionRoutingOverride && !threadSpawn && _sci?.enabled && _sci.model && isShadowSourceModel(parsed.modelId, _sci.sourceModels)) {
|
|
557
|
+
if (!memoryRoute && !options.memoryModelPhase && !options.compactionRoutingOverride && !threadSpawn && _sci?.enabled && _sci.model && isShadowSourceModel(parsed.modelId, _sci.sourceModels)) {
|
|
490
558
|
const sourcePrefix = shadowSourceModelPrefix(parsed.modelId, _sci.sourceModels)!;
|
|
491
559
|
let sourceIdentity = { providerName: OPENAI_CODEX_PROVIDER_ID, modelId: sourcePrefix };
|
|
492
560
|
try {
|
|
@@ -523,7 +591,20 @@ export async function prepareResponsesRequest(
|
|
|
523
591
|
}
|
|
524
592
|
}
|
|
525
593
|
if (parsed._compactionRequest === true || options.compactionRoutingOverride) parsed._cursorIsolateConversation = true;
|
|
526
|
-
route = shadowRoute ?? resolveRoute(parsed.modelId);
|
|
594
|
+
route = memoryRoute ?? shadowRoute ?? resolveRoute(parsed.modelId);
|
|
595
|
+
// Name the phase in the persisted route decision, so the request log says why this turn went to
|
|
596
|
+
// the memory destination instead of leaving it looking like a plain user selection. Set here, on
|
|
597
|
+
// the resolved route, so a combo child's own route carries it too.
|
|
598
|
+
if (parsed._memoryModelPhase) {
|
|
599
|
+
const reason = memoryModelRouteReason(parsed._memoryModelPhase);
|
|
600
|
+
route.routeReason = reason;
|
|
601
|
+
if (route.routeDecision) {
|
|
602
|
+
route.routeDecision = {
|
|
603
|
+
...route.routeDecision,
|
|
604
|
+
selected: { ...route.routeDecision.selected, reason },
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
}
|
|
527
608
|
if (options.compactionRoutingOverride && !compactionRoutingKeepsProviderIdentity(config, options.compactionRoutingOverride, route)) {
|
|
528
609
|
credentialDomainWasRewritten = true;
|
|
529
610
|
// The destination does not share the conversation's credential domain, so it can neither
|
|
@@ -1263,6 +1344,7 @@ export async function prepareResponsesRequest(
|
|
|
1263
1344
|
? admissionState.authCtx.accountId
|
|
1264
1345
|
: config.activeCodexAccountId ?? null;
|
|
1265
1346
|
|
|
1347
|
+
commitSkillsSnapshot?.();
|
|
1266
1348
|
return {
|
|
1267
1349
|
inboundWire,
|
|
1268
1350
|
translatorBudget,
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
hasAnthropicFailoverQuorum,
|
|
25
25
|
} from "../../oauth/anthropic-routing";
|
|
26
26
|
import {
|
|
27
|
+
OAuthAccountPausedError,
|
|
27
28
|
getValidAccessSnapshotForAccount,
|
|
28
29
|
forceRefreshOAuthAccessSnapshot,
|
|
29
30
|
getValidAccessTokenSnapshot,
|
|
@@ -697,6 +698,9 @@ export async function prepareResponsesTransport(
|
|
|
697
698
|
`${redactSecretString(err.message)}. Remove or reconfigure provider '${safeProviderName}' in the OpenCodex configuration.`,
|
|
698
699
|
);
|
|
699
700
|
}
|
|
701
|
+
if (err instanceof OAuthAccountPausedError) {
|
|
702
|
+
return formatErrorResponse(403, "permission_error", publicOAuthAuthenticationErrorMessage(err));
|
|
703
|
+
}
|
|
700
704
|
return formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(err));
|
|
701
705
|
}
|
|
702
706
|
}
|
|
@@ -551,7 +551,7 @@ export async function executeResponsesRunTurn(
|
|
|
551
551
|
return retryQueue.stream();
|
|
552
552
|
};
|
|
553
553
|
|
|
554
|
-
const { toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, toolSearchToolNames } = toolBridgeMaps;
|
|
554
|
+
const { toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, bareCustomToolNames, toolSearchToolNames } = toolBridgeMaps;
|
|
555
555
|
const enforceDeclaredToolNames = inboundWire !== "chat" && inboundWire !== "anthropic";
|
|
556
556
|
const classifyUndeclaredFirstTool = (
|
|
557
557
|
event: AdapterEvent,
|
|
@@ -559,7 +559,7 @@ export async function executeResponsesRunTurn(
|
|
|
559
559
|
if (!enforceDeclaredToolNames || event.type !== "tool_call_start") return undefined;
|
|
560
560
|
// This tool is declared to the adapter by the private search loop.
|
|
561
561
|
if (wsPlan && event.name === WEB_SEARCH_TOOL_NAME) return undefined;
|
|
562
|
-
const effectiveName = normalizeDeclaredToolName(event.name, declaredToolNames);
|
|
562
|
+
const effectiveName = normalizeDeclaredToolName(event.name, declaredToolNames, undefined, bareCustomToolNames);
|
|
563
563
|
if (declaredToolNames.has(effectiveName)) return undefined;
|
|
564
564
|
return {
|
|
565
565
|
type: "error",
|
|
@@ -574,6 +574,7 @@ export async function executeResponsesRunTurn(
|
|
|
574
574
|
if (preflight.replayUnsafe || preflight.error?.status !== 429
|
|
575
575
|
|| preflight.error.code === SEND_BUDGET_EXHAUSTED_CODE) return;
|
|
576
576
|
const { httpStatus, error } = adapterFailureFromEvent(preflight.error);
|
|
577
|
+
transportState.bindKeyUsageFromBridge(preflight.error.usage);
|
|
577
578
|
cancelResponseCompletion();
|
|
578
579
|
runTurnAbort.abort();
|
|
579
580
|
queue.close();
|
|
@@ -672,6 +673,7 @@ export async function executeResponsesRunTurn(
|
|
|
672
673
|
stallTimeoutSec,
|
|
673
674
|
hideThinkingSummary: parsed.options.hideThinkingSummary,
|
|
674
675
|
declaredToolNames,
|
|
676
|
+
bareCustomToolNames,
|
|
675
677
|
enforceDeclaredToolNames,
|
|
676
678
|
toolParameterSchemas,
|
|
677
679
|
...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
|
|
@@ -814,6 +816,7 @@ export async function executeResponsesRunTurn(
|
|
|
814
816
|
enforceDeclaredToolNames,
|
|
815
817
|
toolParameterSchemas,
|
|
816
818
|
freeformToolNames,
|
|
819
|
+
bareCustomToolNames,
|
|
817
820
|
toolSearchToolNames,
|
|
818
821
|
...(routedCompaction ? { compaction: true } : {}),
|
|
819
822
|
onProviderState: state => { providerState = state; },
|
|
@@ -16,16 +16,21 @@ export const INTERCEPT_TARGET_UNAVAILABLE_CODE = "intercept_target_unavailable";
|
|
|
16
16
|
/** Non-retryable: the target stays unavailable until the operator changes the configuration. */
|
|
17
17
|
export const INTERCEPT_TARGET_UNAVAILABLE_STATUS = 409;
|
|
18
18
|
|
|
19
|
-
export type
|
|
19
|
+
export type ChosenTargetResolution = { route: RouteResult } | { unavailable: string };
|
|
20
|
+
export type ShadowTargetResolution = ChosenTargetResolution;
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
|
-
* Resolve
|
|
23
|
+
* Resolve one operator-chosen target. Admission-scope refusals, exhausted combos and policy
|
|
23
24
|
* evaluations keep their existing responses, so they are rethrown to the caller's handler.
|
|
25
|
+
*
|
|
26
|
+
* Shared by the shadow-call intercept and the memory-model routing: both name a single destination
|
|
27
|
+
* whose unavailability must not be papered over by the router's terminal default-provider
|
|
28
|
+
* fallback. `shadowCallTargetsIntersect` and the memory setting are the two callers.
|
|
24
29
|
*/
|
|
25
|
-
export function
|
|
30
|
+
export function resolveChosenTarget(
|
|
26
31
|
model: string,
|
|
27
32
|
resolve: (model: string) => RouteResult,
|
|
28
|
-
):
|
|
33
|
+
): ChosenTargetResolution {
|
|
29
34
|
let route: RouteResult;
|
|
30
35
|
try {
|
|
31
36
|
route = resolve(model);
|
|
@@ -44,6 +49,14 @@ export function resolveShadowCallTarget(
|
|
|
44
49
|
return { route };
|
|
45
50
|
}
|
|
46
51
|
|
|
52
|
+
/** The shadow-call name for the shared resolver, kept because that is the surface's own vocabulary. */
|
|
53
|
+
export function resolveShadowCallTarget(
|
|
54
|
+
model: string,
|
|
55
|
+
resolve: (model: string) => RouteResult,
|
|
56
|
+
): ShadowTargetResolution {
|
|
57
|
+
return resolveChosenTarget(model, resolve);
|
|
58
|
+
}
|
|
59
|
+
|
|
47
60
|
const warnedTargets = new Set<string>();
|
|
48
61
|
|
|
49
62
|
export function interceptTargetUnavailableResponse(model: string, detail: string): Response {
|
|
@@ -111,6 +111,17 @@ export async function executeResponsesSidecars(
|
|
|
111
111
|
cancelResponseCompletion,
|
|
112
112
|
} = responseEffects;
|
|
113
113
|
|
|
114
|
+
// Resolving the OpenAI search credential may hold the account's sole
|
|
115
|
+
// cooldown-recovery probe lease. A streamed sidecar result keeps it until the
|
|
116
|
+
// stream settles — completion or client cancel — so a later in-stream search
|
|
117
|
+
// outcome can still clear the cooldown; a response with no live body is
|
|
118
|
+
// terminal, so the lease is handed back before returning it. A recorded
|
|
119
|
+
// search outcome already settled the lease, making each release a
|
|
120
|
+
// generation-bound no-op.
|
|
121
|
+
const releaseSearchProbeLease = (): void => {
|
|
122
|
+
openAiSidecar?.releaseProbeLease?.();
|
|
123
|
+
};
|
|
124
|
+
|
|
114
125
|
|
|
115
126
|
// Tool results are PAIRED by call_id. parseRequest writes it into OcxToolResultMessage.toolCallId
|
|
116
127
|
// (parser.ts:738/752) without validating it, because inputItemSchema's permissive catch-all
|
|
@@ -133,6 +144,8 @@ export async function executeResponsesSidecars(
|
|
|
133
144
|
|| (message as { toolCallId: string }).toolCallId.length === 0),
|
|
134
145
|
);
|
|
135
146
|
if (unpaired) {
|
|
147
|
+
// Local validation has no running sidecar body to own this lease.
|
|
148
|
+
releaseSearchProbeLease();
|
|
136
149
|
// Never interpolate the tool output: this message reaches the client and the logs.
|
|
137
150
|
return formatErrorResponse(
|
|
138
151
|
400,
|
|
@@ -330,6 +343,7 @@ export async function executeResponsesSidecars(
|
|
|
330
343
|
// the bridge entirely so enabling the feature doesn't break ordinary non-streaming traffic.
|
|
331
344
|
if (!parsed.stream) {
|
|
332
345
|
if (imgPlan) {
|
|
346
|
+
releaseSearchProbeLease();
|
|
333
347
|
return formatErrorResponse(400, "invalid_request_error", "image bridge requires stream=true");
|
|
334
348
|
}
|
|
335
349
|
// Video-only: skip bridge for non-streaming requests
|
|
@@ -431,11 +445,12 @@ export async function executeResponsesSidecars(
|
|
|
431
445
|
if (imgResponse.body) {
|
|
432
446
|
const imgTurnAc = new AbortController();
|
|
433
447
|
imgTurnAc.signal.addEventListener("abort", cancelResponseCompletion, { once: true });
|
|
434
|
-
return new Response(trackStreamLifetime(imgResponse.body, imgTurnAc,
|
|
448
|
+
return new Response(trackStreamLifetime(imgResponse.body, imgTurnAc, releaseSearchProbeLease, options.turnAdmissionLease), {
|
|
435
449
|
status: imgResponse.status,
|
|
436
450
|
headers: imgResponse.headers,
|
|
437
451
|
});
|
|
438
452
|
}
|
|
453
|
+
releaseSearchProbeLease();
|
|
439
454
|
return imgResponse;
|
|
440
455
|
} // end else (streaming bridge)
|
|
441
456
|
}
|
|
@@ -511,11 +526,12 @@ export async function executeResponsesSidecars(
|
|
|
511
526
|
if (wsResponse.body) {
|
|
512
527
|
const wsTurnAc = new AbortController();
|
|
513
528
|
wsTurnAc.signal.addEventListener("abort", cancelResponseCompletion, { once: true });
|
|
514
|
-
return new Response(trackStreamLifetime(wsResponse.body, wsTurnAc,
|
|
529
|
+
return new Response(trackStreamLifetime(wsResponse.body, wsTurnAc, releaseSearchProbeLease, options.turnAdmissionLease), {
|
|
515
530
|
status: wsResponse.status,
|
|
516
531
|
headers: wsResponse.headers,
|
|
517
532
|
});
|
|
518
533
|
}
|
|
534
|
+
releaseSearchProbeLease();
|
|
519
535
|
return wsResponse;
|
|
520
536
|
}
|
|
521
537
|
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runtime skills catalog session snapshotting (#5569).
|
|
3
|
+
*
|
|
4
|
+
* Preserves the Anthropic/LLM prompt cache prefix across turns by freezing
|
|
5
|
+
* incoming <skills_instructions> for the duration of a trustworthy session.
|
|
6
|
+
* Gated by config `skills.catalog_refresh`: "per_session" (default) or "per_turn".
|
|
7
|
+
*
|
|
8
|
+
* Lifecycle & Bounds:
|
|
9
|
+
* - 4 hours idle TTL (sliding on access)
|
|
10
|
+
* - 1,000 maximum tracked sessions (LRU eviction)
|
|
11
|
+
* - 512 KiB maximum per snapshotted skills block
|
|
12
|
+
* - 8 MiB global retained byte bound across all sessions
|
|
13
|
+
*/
|
|
14
|
+
import type { OcxConfig, SkillsCatalogRefresh } from "../../types/config";
|
|
15
|
+
import { isApiAuthRequired, resolveContextPrincipal, type DataPlaneAdmission } from "../auth-cors";
|
|
16
|
+
import {
|
|
17
|
+
reasoningReplayConversationIdFromResponsesRequest,
|
|
18
|
+
sessionIdHeaderFromRequest,
|
|
19
|
+
} from "../request-log-conversation";
|
|
20
|
+
|
|
21
|
+
const SKILLS_BLOCK_GLOBAL_REGEX = /<skills_instructions>([\s\S]*?)<\/skills_instructions>/g;
|
|
22
|
+
|
|
23
|
+
/** Maximum distinct sessions tracked in the memory LRU. */
|
|
24
|
+
export const MAX_SNAPSHOT_SESSIONS = 1000;
|
|
25
|
+
/** Slide expiry after 4 hours of inactivity. */
|
|
26
|
+
export const SNAPSHOT_TTL_MS = 4 * 60 * 60 * 1000;
|
|
27
|
+
/** Bounded byte ceiling per snapshotted skills block (512 KiB). */
|
|
28
|
+
export const MAX_SKILLS_BLOCK_BYTES = 512 * 1024;
|
|
29
|
+
/** Global retained byte bound across all tracked sessions (8 MiB). */
|
|
30
|
+
export const MAX_TOTAL_RETAINED_BYTES = 8 * 1024 * 1024;
|
|
31
|
+
|
|
32
|
+
interface SnapshotEntry {
|
|
33
|
+
skillsBlock: string; // The full <skills_instructions>...</skills_instructions> block
|
|
34
|
+
byteLength: number;
|
|
35
|
+
lastAccessed: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const snapshotCache = new Map<string, SnapshotEntry>();
|
|
39
|
+
let totalRetainedBytes = 0;
|
|
40
|
+
|
|
41
|
+
function evictOldestEntry(): boolean {
|
|
42
|
+
const oldest = snapshotCache.entries().next().value;
|
|
43
|
+
if (!oldest) return false;
|
|
44
|
+
const [key, entry] = oldest;
|
|
45
|
+
totalRetainedBytes -= entry.byteLength;
|
|
46
|
+
snapshotCache.delete(key);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function resolveSkillsCatalogRefresh(config: OcxConfig | undefined): SkillsCatalogRefresh {
|
|
51
|
+
const configured = config?.skills?.catalog_refresh;
|
|
52
|
+
if (configured === "per_turn") return "per_turn";
|
|
53
|
+
return "per_session";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ResolveSkillsSessionScopeInput {
|
|
57
|
+
req: Request;
|
|
58
|
+
config: OcxConfig;
|
|
59
|
+
admission?: DataPlaneAdmission;
|
|
60
|
+
cursorConversationId?: string;
|
|
61
|
+
promptCacheKeyIsSharedCohort?: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The principal a snapshot is scoped to. Without a named principal, only a server that requires
|
|
66
|
+
* no data-plane auth may share by conversation id (loopback admission, or an internal caller that
|
|
67
|
+
* passed none on such a server): its callers already share one trust domain, which on a no-auth
|
|
68
|
+
* server bound beyond loopback includes remote callers. Any other anonymous request gets no
|
|
69
|
+
* snapshot.
|
|
70
|
+
*/
|
|
71
|
+
function snapshotPrincipal(input: ResolveSkillsSessionScopeInput): string | null | undefined {
|
|
72
|
+
const principal = resolveContextPrincipal(input.req, input.config, input.admission);
|
|
73
|
+
if (principal) return principal;
|
|
74
|
+
if (input.admission) return input.admission.kind === "loopback" ? null : undefined;
|
|
75
|
+
return isApiAuthRequired(input.config) ? undefined : null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Resolves a trustworthy cache key for skills catalog snapshotting.
|
|
80
|
+
* Returns null if no specific, reliable thread/session identity is available,
|
|
81
|
+
* or if the identity comes from a shared cohort fallback.
|
|
82
|
+
*/
|
|
83
|
+
export function resolveSkillsSnapshotScopeKey(input: ResolveSkillsSessionScopeInput): string | null {
|
|
84
|
+
if (input.promptCacheKeyIsSharedCohort === true) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const parentThread = input.req.headers.get("x-codex-parent-thread-id")?.trim() || undefined;
|
|
89
|
+
const ownThreadId = input.req.headers.get("thread-id")?.trim() || undefined;
|
|
90
|
+
const cursorId = input.cursorConversationId?.trim() || undefined;
|
|
91
|
+
|
|
92
|
+
// When a parent thread is present, subagents/children may share a root session-id header.
|
|
93
|
+
// To prevent cross-thread/sibling collapse or parent-level caching, require an explicit
|
|
94
|
+
// own thread-id (or cursor id). If parentThread is present without an own child thread,
|
|
95
|
+
// bypass snapshotting completely.
|
|
96
|
+
if (parentThread) {
|
|
97
|
+
const childId = ownThreadId ?? cursorId;
|
|
98
|
+
if (!childId || childId === parentThread) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
const qualifiedId = `${parentThread}\u0000${childId}`;
|
|
102
|
+
const principal = snapshotPrincipal(input);
|
|
103
|
+
if (principal === undefined) return null;
|
|
104
|
+
return JSON.stringify(["skills_catalog_snapshot_v1", principal, qualifiedId]);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Standalone conversation (no parent thread)
|
|
108
|
+
const standaloneId = reasoningReplayConversationIdFromResponsesRequest({
|
|
109
|
+
threadIdHeader: ownThreadId,
|
|
110
|
+
cursorConversationId: cursorId,
|
|
111
|
+
sessionIdHeader: sessionIdHeaderFromRequest(input.req.headers),
|
|
112
|
+
});
|
|
113
|
+
if (!standaloneId) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
const principal = snapshotPrincipal(input);
|
|
117
|
+
if (principal === undefined) return null;
|
|
118
|
+
return JSON.stringify(["skills_catalog_snapshot_v1", principal, standaloneId]);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** One text slot that may carry a catalog: `instructions` or a developer/system text part. */
|
|
122
|
+
interface CatalogSlot {
|
|
123
|
+
text: string;
|
|
124
|
+
write(next: string): void;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Every developer/system text slot, walked the same way the replacement writes. */
|
|
128
|
+
function catalogSlots(body: Record<string, unknown>): CatalogSlot[] {
|
|
129
|
+
const slots: CatalogSlot[] = [];
|
|
130
|
+
if (typeof body.instructions === "string") {
|
|
131
|
+
slots.push({ text: body.instructions, write: next => { body.instructions = next; } });
|
|
132
|
+
}
|
|
133
|
+
if (!Array.isArray(body.input)) return slots;
|
|
134
|
+
for (const item of body.input) {
|
|
135
|
+
if (!item || typeof item !== "object") continue;
|
|
136
|
+
const it = item as Record<string, unknown>;
|
|
137
|
+
// Restrict message item type: must be undefined or "message", so role-like tool objects are untouched
|
|
138
|
+
if (it.type !== undefined && it.type !== "message") continue;
|
|
139
|
+
// Only developer and system content is inspected/transformed
|
|
140
|
+
if (it.role !== "developer" && it.role !== "system") continue;
|
|
141
|
+
const content = it.content;
|
|
142
|
+
if (typeof content === "string") {
|
|
143
|
+
slots.push({ text: content, write: next => { it.content = next; } });
|
|
144
|
+
} else if (Array.isArray(content)) {
|
|
145
|
+
for (const part of content) {
|
|
146
|
+
if (!part || typeof part !== "object") continue;
|
|
147
|
+
const p = part as Record<string, unknown>;
|
|
148
|
+
// Restrict text parts to known text / input_text
|
|
149
|
+
if (p.type !== "text" && p.type !== "input_text") continue;
|
|
150
|
+
if (typeof p.text === "string") slots.push({ text: p.text, write: next => { p.text = next; } });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return slots;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function liveSnapshot(scopeKey: string, now: number): SnapshotEntry | undefined {
|
|
158
|
+
const existing = snapshotCache.get(scopeKey);
|
|
159
|
+
if (!existing) return undefined;
|
|
160
|
+
if (now - existing.lastAccessed > SNAPSHOT_TTL_MS) {
|
|
161
|
+
totalRetainedBytes -= existing.byteLength;
|
|
162
|
+
snapshotCache.delete(scopeKey);
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
existing.lastAccessed = now;
|
|
166
|
+
// Refresh Map order for true LRU behavior
|
|
167
|
+
snapshotCache.delete(scopeKey);
|
|
168
|
+
snapshotCache.set(scopeKey, existing);
|
|
169
|
+
return existing;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function storeSnapshot(scopeKey: string, skillsBlock: string, now: number): void {
|
|
173
|
+
const blockBytes = Buffer.byteLength(skillsBlock, "utf8");
|
|
174
|
+
if (blockBytes > MAX_SKILLS_BLOCK_BYTES || blockBytes > MAX_TOTAL_RETAINED_BYTES) return;
|
|
175
|
+
// A concurrent request of the same conversation may have stored first; the first catalog wins.
|
|
176
|
+
if (snapshotCache.has(scopeKey)) return;
|
|
177
|
+
// Evict oldest entries until under count ceiling AND under global byte ceiling
|
|
178
|
+
while (
|
|
179
|
+
(snapshotCache.size >= MAX_SNAPSHOT_SESSIONS || totalRetainedBytes + blockBytes > MAX_TOTAL_RETAINED_BYTES)
|
|
180
|
+
&& snapshotCache.size > 0
|
|
181
|
+
) {
|
|
182
|
+
if (!evictOldestEntry()) break;
|
|
183
|
+
}
|
|
184
|
+
if (totalRetainedBytes + blockBytes > MAX_TOTAL_RETAINED_BYTES) return;
|
|
185
|
+
snapshotCache.set(scopeKey, { skillsBlock, byteLength: blockBytes, lastAccessed: now });
|
|
186
|
+
totalRetainedBytes += blockBytes;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Reuses the session's snapshotted <skills_instructions> in developer/system content, preserving
|
|
191
|
+
* the prompt cache prefix across turns. User and assistant messages and tool calls are never
|
|
192
|
+
* modified.
|
|
193
|
+
*
|
|
194
|
+
* Only a body with exactly one catalog block across all of those slots takes part: with two or
|
|
195
|
+
* more there is no way to tell which one the snapshot stands for, so the body passes through
|
|
196
|
+
* untouched rather than rewriting every block to one catalog.
|
|
197
|
+
*
|
|
198
|
+
* A known snapshot is substituted at once, so parsing and the verbatim passthrough body both see
|
|
199
|
+
* it. A new catalog is only stored through the returned commit, which the caller runs once the
|
|
200
|
+
* request has passed parsing and admission, so a rejected first request pins nothing.
|
|
201
|
+
*/
|
|
202
|
+
export function snapshotSkillsCatalogInBody(
|
|
203
|
+
body: unknown,
|
|
204
|
+
scopeKey: string | null,
|
|
205
|
+
config: OcxConfig,
|
|
206
|
+
now: number = Date.now(),
|
|
207
|
+
): (() => void) | undefined {
|
|
208
|
+
if (!scopeKey) return undefined;
|
|
209
|
+
if (resolveSkillsCatalogRefresh(config) === "per_turn") return undefined;
|
|
210
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return undefined;
|
|
211
|
+
|
|
212
|
+
let found: { slot: CatalogSlot; block: string } | undefined;
|
|
213
|
+
let blocks = 0;
|
|
214
|
+
for (const slot of catalogSlots(body as Record<string, unknown>)) {
|
|
215
|
+
if (!slot.text.includes("<skills_instructions>")) continue;
|
|
216
|
+
for (const match of slot.text.matchAll(SKILLS_BLOCK_GLOBAL_REGEX)) {
|
|
217
|
+
blocks++;
|
|
218
|
+
found ??= { slot, block: match[0] };
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (blocks !== 1 || !found) return undefined;
|
|
222
|
+
|
|
223
|
+
const existing = liveSnapshot(scopeKey, now);
|
|
224
|
+
if (existing) {
|
|
225
|
+
const { slot, block } = found;
|
|
226
|
+
const at = slot.text.indexOf(block);
|
|
227
|
+
slot.write(slot.text.slice(0, at) + existing.skillsBlock + slot.text.slice(at + block.length));
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
const incoming = found.block;
|
|
231
|
+
return () => storeSnapshot(scopeKey, incoming, now);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Test helpers */
|
|
235
|
+
export function resetSkillsSnapshotCacheForTests(): void {
|
|
236
|
+
snapshotCache.clear();
|
|
237
|
+
totalRetainedBytes = 0;
|
|
238
|
+
}
|
|
239
|
+
|