@bitkyc08/opencodex 2.55.0 → 2.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/{index-VuoiWj9J.js → index-D4zuyIxQ.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +2 -1
- package/src/adapters/base.ts +21 -0
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-schema.ts +477 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +33 -1445
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +611 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +34 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +624 -0
- package/src/bridge/sse.ts +1444 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +71 -9
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +554 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +422 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +95 -28
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +304 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +706 -0
- package/src/codex/catalog/routed-gather.ts +858 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +540 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +81 -1436
- package/src/codex/lineage.ts +458 -0
- package/src/codex/pool-refresh-backoff.ts +152 -0
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cooldown-math.ts +275 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +703 -0
- package/src/codex/routing/thread-affinity.ts +538 -0
- package/src/codex/routing.ts +353 -2234
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +539 -39
- package/src/lib/request-execution-budget.ts +69 -20
- package/src/lib/spend-reservation-ledger.ts +940 -0
- package/src/lib/upstream-retry.ts +55 -11
- package/src/lib/workflow-budget.ts +553 -30
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1221 -0
- package/src/providers/registry/entries-extended.ts +1204 -0
- package/src/providers/registry/model-seeds.ts +908 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +449 -0
- package/src/routing/probe-lease.ts +511 -0
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +565 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +335 -0
- package/src/server/index.ts +40 -2547
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +245 -1
- package/src/server/responses/account-change-state.ts +233 -0
- package/src/server/responses/adapter-continuation.ts +514 -0
- package/src/server/responses/adapter-delivery.ts +214 -0
- package/src/server/responses/adapter-dispatch.ts +971 -0
- package/src/server/responses/compact.ts +59 -4
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +527 -0
- package/src/server/responses/core-codex-account.ts +859 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +707 -0
- package/src/server/responses/core-errors.ts +152 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +225 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/passthrough-delivery.ts +856 -0
- package/src/server/responses/passthrough-dispatch.ts +1476 -0
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +970 -0
- package/src/server/responses/request-send-budget.ts +164 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-transport.ts +744 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +448 -0
- package/src/server/responses/sidecar-execution.ts +469 -0
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/workflow-refusal.ts +84 -0
- package/src/types/config.ts +30 -0
- package/src/usage/log.ts +146 -0
- package/src/usage/summary.ts +171 -21
|
@@ -21,24 +21,33 @@ import type { AdapterTierMetadata } from "../providers/fastwire";
|
|
|
21
21
|
import { redactSecretString, sanitizeLogMetadataString } from "../lib/redact";
|
|
22
22
|
import {
|
|
23
23
|
appendUsageEntry,
|
|
24
|
+
classifyCacheTelemetryProvenance,
|
|
24
25
|
isKnownAdmissionKind,
|
|
26
|
+
isKnownAffinityMove,
|
|
27
|
+
isKnownAffinityReason,
|
|
28
|
+
isKnownCacheTelemetryProvenance,
|
|
25
29
|
isKnownInboundProtocol,
|
|
26
30
|
isKnownTerminalSource,
|
|
27
31
|
isKnownTransportPhase,
|
|
28
32
|
isKnownUsageSurface,
|
|
29
33
|
isCodexUsageAccountLogLabel,
|
|
34
|
+
isLogicalRequestId,
|
|
30
35
|
isValidReasoningWireValue,
|
|
31
36
|
normalizeClaudeCompatibilityUsageLog,
|
|
37
|
+
normalizeRequestSpend,
|
|
32
38
|
readRecentUsageEntries,
|
|
33
39
|
usageForFinalLog,
|
|
34
40
|
usageStatusForFinalLog,
|
|
35
41
|
usageTotalTokens,
|
|
36
42
|
type AttemptRecoveryKind,
|
|
43
|
+
type CacheTelemetryProvenance,
|
|
44
|
+
type PersistedRequestSpend,
|
|
37
45
|
type PersistedUsageAttempt,
|
|
38
46
|
type PersistedUsageEntry,
|
|
39
47
|
type PersistedClaudeCompatibilityLog,
|
|
40
48
|
type UsageStatus,
|
|
41
49
|
} from "../usage/log";
|
|
50
|
+
import type { RequestExecutionBudget } from "../lib/request-execution-budget";
|
|
42
51
|
import {
|
|
43
52
|
appendUsageDebug,
|
|
44
53
|
isUsageDebugEnabled,
|
|
@@ -57,6 +66,29 @@ import { modelRecordValue } from "../reasoning-effort";
|
|
|
57
66
|
export interface RequestLogContext {
|
|
58
67
|
model: string;
|
|
59
68
|
provider: string;
|
|
69
|
+
/**
|
|
70
|
+
* Identity of the ONE logical request this context serves (#4546). Set from the execution
|
|
71
|
+
* budget minted at ingress; a retry leg, a repair refetch and a combo child share it.
|
|
72
|
+
*/
|
|
73
|
+
logicalRequestId?: string;
|
|
74
|
+
/**
|
|
75
|
+
* Internal live reference to this request's execution budget; omitted from RequestLogEntry and
|
|
76
|
+
* JSONL. Read at final-log time so the row reports the budget's FINAL state rather than a
|
|
77
|
+
* snapshot taken before the recovery legs that the row is meant to explain.
|
|
78
|
+
*/
|
|
79
|
+
executionBudget?: RequestExecutionBudget;
|
|
80
|
+
/**
|
|
81
|
+
* True once usage counts were taken from a response wire rather than reported raw by the
|
|
82
|
+
* adapter. It decides cache provenance: the normalizer writes zero-default token-detail
|
|
83
|
+
* objects, so an all-zero cache detail from a parsed wire is not a measured cache miss.
|
|
84
|
+
*/
|
|
85
|
+
usageWireParsed?: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Every affinity reason recorded for this request, in order. `affinityReason` keeps the final
|
|
88
|
+
* one for the existing row shape; a request that moved twice has two causes and losing the
|
|
89
|
+
* first one loses the more expensive half of the story.
|
|
90
|
+
*/
|
|
91
|
+
affinityMoveReasons?: CodexAffinityReason[];
|
|
60
92
|
/** TTFT: ms from request start to the first non-empty model output delta (WP4, devlog 040). */
|
|
61
93
|
firstOutputMs?: number;
|
|
62
94
|
/** Best-effort chat/session correlation for Logs grouping (#330). Opaque; omit when unknown. */
|
|
@@ -143,6 +175,11 @@ export interface RequestLogContext {
|
|
|
143
175
|
affinity?: CodexAffinityMove;
|
|
144
176
|
/** Why the binding was kept, moved, or released (#4546). */
|
|
145
177
|
affinityReason?: CodexAffinityReason;
|
|
178
|
+
/**
|
|
179
|
+
* Set when this request dropped account-bound continuation because the serving
|
|
180
|
+
* Codex pool account was not the issuer. Never an account identifier.
|
|
181
|
+
*/
|
|
182
|
+
conversationStateScrub?: "account-change";
|
|
146
183
|
transportPhase?: "pre_headers" | "mid_stream" | "terminal_sse";
|
|
147
184
|
terminalSource?: "upstream" | "synthetic";
|
|
148
185
|
/** Bounded route-decision trace (RI-01); never contains secrets. */
|
|
@@ -153,6 +190,8 @@ export interface RequestLogContext {
|
|
|
153
190
|
|
|
154
191
|
export interface RequestLogEntry {
|
|
155
192
|
requestId: string;
|
|
193
|
+
/** The logical request this row belongs to (#4546); absent on rows written without a budget. */
|
|
194
|
+
logicalRequestId?: string;
|
|
156
195
|
timestamp: number;
|
|
157
196
|
model: string;
|
|
158
197
|
provider: string;
|
|
@@ -206,13 +245,30 @@ export interface RequestLogEntry {
|
|
|
206
245
|
usage?: OcxUsage;
|
|
207
246
|
totalTokens?: number;
|
|
208
247
|
attempts?: PersistedUsageAttempt[];
|
|
248
|
+
/**
|
|
249
|
+
* Upstream spend for the whole logical request: sends aggregated across attempts and combo
|
|
250
|
+
* children, split into settled and unresolved, with the budget state and move reasons that
|
|
251
|
+
* explain them. Per-attempt `sendCount` stays the accounting source; this is the total.
|
|
252
|
+
*/
|
|
253
|
+
spend?: PersistedRequestSpend;
|
|
254
|
+
/** Whether this row's cache detail was observed, synthesized for the wire, or absent. */
|
|
255
|
+
cacheProvenance?: CacheTelemetryProvenance;
|
|
209
256
|
/** Codex pool affinity decision for this request (diagnostics for #186). */
|
|
210
257
|
affinity?: CodexAffinityMove;
|
|
211
258
|
/** Why that decision was made (#4546): a move is the expensive event, so it names its cause. */
|
|
212
259
|
affinityReason?: CodexAffinityReason;
|
|
260
|
+
/**
|
|
261
|
+
* Set when this request dropped account-bound continuation after a Codex pool
|
|
262
|
+
* account change. Never an account identifier.
|
|
263
|
+
*/
|
|
264
|
+
conversationStateScrub?: "account-change";
|
|
213
265
|
/** Where the upstream terminal/failure was observed. */
|
|
214
266
|
transportPhase?: "pre_headers" | "mid_stream" | "terminal_sse";
|
|
215
|
-
/**
|
|
267
|
+
/**
|
|
268
|
+
* Whether the HTTP status and message originated upstream or were synthesized by this
|
|
269
|
+
* proxy. Covers SSE tails and pre-stream JSON refusals. Management surfaces this so a
|
|
270
|
+
* local refusal cannot be presented as an upstream reason.
|
|
271
|
+
*/
|
|
216
272
|
terminalSource?: "upstream" | "synthetic";
|
|
217
273
|
/** Bounded route-decision trace (RI-01); never contains secrets. */
|
|
218
274
|
routeDecision?: RouteDecisionTraceV1;
|
|
@@ -287,8 +343,10 @@ export function requestLogEntryFromPersistedUsage(entry: PersistedUsageEntry): R
|
|
|
287
343
|
const closeReason = asCloseReason(entry.closeReason);
|
|
288
344
|
const routeDecision = normalizeRouteDecisionTraceForLog(entry.routeDecision);
|
|
289
345
|
const claudeCompatibility = normalizeClaudeCompatibilityUsageLog(entry.claudeCompatibility);
|
|
346
|
+
const spend = normalizeRequestSpend(entry.spend);
|
|
290
347
|
return {
|
|
291
348
|
requestId: entry.requestId,
|
|
349
|
+
...(isLogicalRequestId(entry.logicalRequestId) ? { logicalRequestId: entry.logicalRequestId } : {}),
|
|
292
350
|
timestamp: entry.timestamp,
|
|
293
351
|
model: entry.model,
|
|
294
352
|
provider: entry.provider,
|
|
@@ -328,10 +386,33 @@ export function requestLogEntryFromPersistedUsage(entry: PersistedUsageEntry): R
|
|
|
328
386
|
...(entry.usage ? { usage: entry.usage } : {}),
|
|
329
387
|
...(entry.totalTokens !== undefined ? { totalTokens: entry.totalTokens } : {}),
|
|
330
388
|
...(entry.attempts !== undefined ? { attempts: entry.attempts } : {}),
|
|
389
|
+
...(spend ? { spend } : {}),
|
|
390
|
+
...(isKnownCacheTelemetryProvenance(entry.cacheProvenance)
|
|
391
|
+
? { cacheProvenance: entry.cacheProvenance }
|
|
392
|
+
: {}),
|
|
393
|
+
...persistedAffinityFields(entry),
|
|
331
394
|
...(isKnownTransportPhase(entry.transportPhase) ? { transportPhase: entry.transportPhase } : {}),
|
|
332
395
|
...(isKnownTerminalSource(entry.terminalSource) ? { terminalSource: entry.terminalSource } : {}),
|
|
333
396
|
...(routeDecision ? { routeDecision } : {}),
|
|
334
397
|
...(claudeCompatibility ? { claudeCompatibility } : {}),
|
|
398
|
+
...(entry.conversationStateScrub === "account-change"
|
|
399
|
+
? { conversationStateScrub: "account-change" }
|
|
400
|
+
: {}),
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Affinity survived only in memory before this: `addFinalRequestLog` set it on the row and the
|
|
406
|
+
* field-by-field disk projection never named it, so the move that discarded a warm prefix was
|
|
407
|
+
* gone at the next restart — the same whitelist trap #4592 hit one layer up.
|
|
408
|
+
*/
|
|
409
|
+
function persistedAffinityFields(
|
|
410
|
+
entry: Pick<RequestLogEntry, "affinity" | "affinityReason">,
|
|
411
|
+
): Pick<PersistedUsageEntry, "affinity" | "affinityReason"> {
|
|
412
|
+
if (!isKnownAffinityMove(entry.affinity)) return {};
|
|
413
|
+
return {
|
|
414
|
+
affinity: entry.affinity,
|
|
415
|
+
...(isKnownAffinityReason(entry.affinityReason) ? { affinityReason: entry.affinityReason } : {}),
|
|
335
416
|
};
|
|
336
417
|
}
|
|
337
418
|
|
|
@@ -410,6 +491,7 @@ export function addRequestLog(entry: RequestLogEntry) {
|
|
|
410
491
|
: {};
|
|
411
492
|
appendUsageEntry({
|
|
412
493
|
requestId: entry.requestId,
|
|
494
|
+
...(isLogicalRequestId(entry.logicalRequestId) ? { logicalRequestId: entry.logicalRequestId } : {}),
|
|
413
495
|
timestamp: entry.timestamp,
|
|
414
496
|
provider: entry.provider,
|
|
415
497
|
model: entry.model,
|
|
@@ -451,11 +533,19 @@ export function addRequestLog(entry: RequestLogEntry) {
|
|
|
451
533
|
...(entry.usage ? { usage: entry.usage } : {}),
|
|
452
534
|
...(entry.totalTokens !== undefined ? { totalTokens: entry.totalTokens } : {}),
|
|
453
535
|
...(entry.attempts !== undefined ? { attempts: entry.attempts } : {}),
|
|
536
|
+
...(entry.spend ? { spend: entry.spend } : {}),
|
|
537
|
+
...(isKnownCacheTelemetryProvenance(entry.cacheProvenance)
|
|
538
|
+
? { cacheProvenance: entry.cacheProvenance }
|
|
539
|
+
: {}),
|
|
540
|
+
...persistedAffinityFields(entry),
|
|
454
541
|
...(isKnownTransportPhase(entry.transportPhase) ? { transportPhase: entry.transportPhase } : {}),
|
|
455
542
|
...(isKnownTerminalSource(entry.terminalSource) ? { terminalSource: entry.terminalSource } : {}),
|
|
456
543
|
...failureDiagnostics,
|
|
457
544
|
...(entry.routeDecision ? { routeDecision: entry.routeDecision } : {}),
|
|
458
545
|
...(entry.claudeCompatibility ? { claudeCompatibility: entry.claudeCompatibility } : {}),
|
|
546
|
+
...(entry.conversationStateScrub === "account-change"
|
|
547
|
+
? { conversationStateScrub: "account-change" }
|
|
548
|
+
: {}),
|
|
459
549
|
});
|
|
460
550
|
} catch {
|
|
461
551
|
/* request logging must never fail a user request */
|
|
@@ -684,6 +774,10 @@ export function applyResponseLogMetadata(logCtx: RequestLogContext, payload: unk
|
|
|
684
774
|
if (usage && !logCtx.usageFromBridge) {
|
|
685
775
|
logCtx.usage = usage;
|
|
686
776
|
if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
|
|
777
|
+
// Counts taken off a wire, not reported raw. The zero-default token-detail objects strict
|
|
778
|
+
// clients require are indistinguishable here from a measured zero, so the cache detail these
|
|
779
|
+
// counts carry is recorded as synthesized rather than as an observed miss.
|
|
780
|
+
logCtx.usageWireParsed = true;
|
|
687
781
|
}
|
|
688
782
|
}
|
|
689
783
|
|
|
@@ -741,6 +835,15 @@ export function usageFromResponsesPayload(usage: unknown): OcxUsage | undefined
|
|
|
741
835
|
return undefined;
|
|
742
836
|
}
|
|
743
837
|
|
|
838
|
+
/**
|
|
839
|
+
* Mark a refusal this proxy synthesized locally. Sets origin to `synthetic` and a
|
|
840
|
+
* distinct local reason so the request log cannot be read as an upstream overload.
|
|
841
|
+
*/
|
|
842
|
+
export function markLocalRequestLogRefusal(logCtx: RequestLogContext, reason: string): void {
|
|
843
|
+
logCtx.localTerminalReason = reason;
|
|
844
|
+
logCtx.terminalSource = "synthetic";
|
|
845
|
+
}
|
|
846
|
+
|
|
744
847
|
export function inspectResponseLogJson(logCtx: RequestLogContext, text: string): void {
|
|
745
848
|
try {
|
|
746
849
|
applyResponseLogMetadata(logCtx, JSON.parse(text));
|
|
@@ -977,6 +1080,133 @@ export function httpStatusForRequestLogTerminal(
|
|
|
977
1080
|
return httpStatusForTerminalStatus(status);
|
|
978
1081
|
}
|
|
979
1082
|
|
|
1083
|
+
/**
|
|
1084
|
+
* Aggregate one logical request's upstream spend from the rows that recorded it.
|
|
1085
|
+
*
|
|
1086
|
+
* Attempts are the accounting source and combo children are attempts of the same context, so a
|
|
1087
|
+
* sum over `logCtx.attempts` is the send count for one user turn — the number the amplification
|
|
1088
|
+
* in #4546 is measured in. A terminal status is what makes a send explainable, so the split is
|
|
1089
|
+
* drawn there rather than at success: a 502 is settled spend, an attempt abandoned in flight is
|
|
1090
|
+
* not. The budget's own counter is folded in as `reserved` because a leg that re-sent without
|
|
1091
|
+
* opening an attempt row is charged and unobserved, and that difference belongs in
|
|
1092
|
+
* `unresolved` rather than quietly inflating `settled`.
|
|
1093
|
+
*/
|
|
1094
|
+
export function requestSpendRecord(
|
|
1095
|
+
logCtx: Pick<RequestLogContext, "executionBudget" | "affinityMoveReasons" | "affinityReason">,
|
|
1096
|
+
attempts: readonly PersistedUsageAttempt[] | undefined,
|
|
1097
|
+
): PersistedRequestSpend | undefined {
|
|
1098
|
+
const rows = attempts ?? [];
|
|
1099
|
+
const budget = logCtx.executionBudget;
|
|
1100
|
+
const reasons = [...new Set(
|
|
1101
|
+
(logCtx.affinityMoveReasons ?? (logCtx.affinityReason ? [logCtx.affinityReason] : []))
|
|
1102
|
+
.filter(isKnownAffinityReason),
|
|
1103
|
+
)];
|
|
1104
|
+
if (rows.length === 0 && !budget && reasons.length === 0) return undefined;
|
|
1105
|
+
const sends = rows.reduce((total, attempt) => total + attempt.sendCount, 0);
|
|
1106
|
+
const settled = rows.reduce(
|
|
1107
|
+
(total, attempt) => attempt.status >= 100 ? total + attempt.sendCount : total,
|
|
1108
|
+
0,
|
|
1109
|
+
);
|
|
1110
|
+
const charged = Math.max(sends, budget?.used ?? 0);
|
|
1111
|
+
return {
|
|
1112
|
+
sends,
|
|
1113
|
+
settled,
|
|
1114
|
+
unresolved: Math.max(0, charged - settled),
|
|
1115
|
+
...(budget ? { reserved: budget.used, policyVersion: budget.policyVersion } : {}),
|
|
1116
|
+
...(reasons.length > 0 ? { moveReasons: reasons } : {}),
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/**
|
|
1121
|
+
* Record an affinity decision so both the row's final answer and the sequence survive. A request
|
|
1122
|
+
* that moved for `quota_refusal` and then again for `transient` paid for two discarded prefixes,
|
|
1123
|
+
* and the single-valued field can only report the second.
|
|
1124
|
+
*/
|
|
1125
|
+
export function noteAffinityMove(
|
|
1126
|
+
logCtx: RequestLogContext,
|
|
1127
|
+
move: CodexAffinityMove,
|
|
1128
|
+
reason: CodexAffinityReason,
|
|
1129
|
+
): void {
|
|
1130
|
+
logCtx.affinity = move;
|
|
1131
|
+
logCtx.affinityReason = reason;
|
|
1132
|
+
(logCtx.affinityMoveReasons ??= []).push(reason);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/**
|
|
1136
|
+
* The affinity scope a released binding belonged to: one thread, one model lane.
|
|
1137
|
+
*
|
|
1138
|
+
* Both halves are part of the key. A thread holds a separate binding per model lane, so a
|
|
1139
|
+
* quota refusal on one lane and a transient streak on another are two releases; keyed by thread
|
|
1140
|
+
* alone the second overwrites the first and one of the two rows reports a cause that never
|
|
1141
|
+
* happened on it.
|
|
1142
|
+
*/
|
|
1143
|
+
export interface AffinityModelLane {
|
|
1144
|
+
model: string;
|
|
1145
|
+
/** Thread/conversation that owns the binding; omitted when the caller has no thread identity. */
|
|
1146
|
+
conversationId?: string;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/**
|
|
1150
|
+
* Release reasons waiting for the request that can report them (#4546, #4598).
|
|
1151
|
+
*
|
|
1152
|
+
* Bounded like the routing-side map it mirrors: this is a diagnostic, and an unbounded map keyed
|
|
1153
|
+
* by conversation is a leak.
|
|
1154
|
+
*/
|
|
1155
|
+
const pendingNoAccountReasons = new Map<string, CodexAffinityReason>();
|
|
1156
|
+
const MAX_PENDING_NO_ACCOUNT_REASONS = 1024;
|
|
1157
|
+
|
|
1158
|
+
function affinityLaneKey(lane: AffinityModelLane): string {
|
|
1159
|
+
return `${lane.conversationId ?? ""}\u0000${lane.model}`;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
export function noteNoAccountAffinityReason(lane: AffinityModelLane, reason: CodexAffinityReason): void {
|
|
1163
|
+
if (!isKnownAffinityReason(reason)) return;
|
|
1164
|
+
const key = affinityLaneKey(lane);
|
|
1165
|
+
if (!pendingNoAccountReasons.has(key) && pendingNoAccountReasons.size >= MAX_PENDING_NO_ACCOUNT_REASONS) {
|
|
1166
|
+
const oldest = pendingNoAccountReasons.keys().next();
|
|
1167
|
+
if (!oldest.done) pendingNoAccountReasons.delete(oldest.value);
|
|
1168
|
+
}
|
|
1169
|
+
pendingNoAccountReasons.set(key, reason);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
/** Read and forget one lane's reason. Other lanes on the same thread keep theirs. */
|
|
1173
|
+
export function takeNoAccountAffinityReason(lane: AffinityModelLane): CodexAffinityReason | undefined {
|
|
1174
|
+
const key = affinityLaneKey(lane);
|
|
1175
|
+
const reason = pendingNoAccountReasons.get(key);
|
|
1176
|
+
if (reason !== undefined) pendingNoAccountReasons.delete(key);
|
|
1177
|
+
return reason;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
/** Test-only process-state reset for isolated harnesses. */
|
|
1181
|
+
export function clearNoAccountAffinityReasonsForTests(): void {
|
|
1182
|
+
pendingNoAccountReasons.clear();
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
/**
|
|
1186
|
+
* Report a selection that produced no account, on the request that failed because of it.
|
|
1187
|
+
*
|
|
1188
|
+
* A no-account resolve reaches no auth context, so until now its cause was handed to whichever
|
|
1189
|
+
* later resolve happened to succeed — and a pool that stays exhausted never produces one, leaving
|
|
1190
|
+
* the failure permanently unexplained. Attaching the reason to THIS request's own record is what
|
|
1191
|
+
* makes the failure self-describing: the row is written, persisted and hydrated like any other,
|
|
1192
|
+
* and it survives a restart.
|
|
1193
|
+
*
|
|
1194
|
+
* Deliberately not a separate synthetic row. `/api/usage` counts one row as one request, so an
|
|
1195
|
+
* extra event row would report a request that never existed and skew the very cost totals this
|
|
1196
|
+
* work exists to make trustworthy.
|
|
1197
|
+
*/
|
|
1198
|
+
export function recordNoAccountAffinityFailure(
|
|
1199
|
+
logCtx: RequestLogContext,
|
|
1200
|
+
lane: AffinityModelLane,
|
|
1201
|
+
reason?: CodexAffinityReason,
|
|
1202
|
+
): CodexAffinityReason | undefined {
|
|
1203
|
+
const resolved = isKnownAffinityReason(reason) ? reason : takeNoAccountAffinityReason(lane);
|
|
1204
|
+
if (resolved === undefined) return undefined;
|
|
1205
|
+
noteAffinityMove(logCtx, "cleared", resolved);
|
|
1206
|
+
logCtx.errorCode ??= "codex_no_account";
|
|
1207
|
+
return resolved;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
980
1210
|
export function addFinalRequestLog(
|
|
981
1211
|
requestId: string,
|
|
982
1212
|
start: number,
|
|
@@ -1032,6 +1262,11 @@ export function addFinalRequestLog(
|
|
|
1032
1262
|
const loggedUsage = aggregate?.usage ?? existing.usage;
|
|
1033
1263
|
const usageStatus = aggregate?.status ?? existing.status;
|
|
1034
1264
|
const totalTokens = aggregate?.totalTokens ?? existing.totalTokens;
|
|
1265
|
+
const spend = requestSpendRecord(logCtx, attempts);
|
|
1266
|
+
const cacheProvenance = classifyCacheTelemetryProvenance(loggedUsage, {
|
|
1267
|
+
wireParsed: logCtx.usageWireParsed === true,
|
|
1268
|
+
});
|
|
1269
|
+
const logicalRequestId = logCtx.logicalRequestId ?? logCtx.executionBudget?.logicalRequestId;
|
|
1035
1270
|
// Sanitize at the logging layer, not only at the one call site that populates this today.
|
|
1036
1271
|
// The value originates in an upstream-supplied model id, so an unsanitized newline would
|
|
1037
1272
|
// let a single field forge a record boundary in any line-oriented log viewer. Doing it here
|
|
@@ -1041,6 +1276,7 @@ export function addFinalRequestLog(
|
|
|
1041
1276
|
const claudeCompatibility = normalizeClaudeCompatibilityUsageLog(logCtx.claudeCompatibility);
|
|
1042
1277
|
addLog({
|
|
1043
1278
|
requestId,
|
|
1279
|
+
...(isLogicalRequestId(logicalRequestId) ? { logicalRequestId } : {}),
|
|
1044
1280
|
timestamp: start,
|
|
1045
1281
|
model: isCombo ? logCtx.requestedModel! : logCtx.model,
|
|
1046
1282
|
provider: isCombo ? "combo" : logCtx.provider,
|
|
@@ -1084,8 +1320,16 @@ export function addFinalRequestLog(
|
|
|
1084
1320
|
...(loggedUsage ? { usage: loggedUsage } : {}),
|
|
1085
1321
|
...(totalTokens !== undefined ? { totalTokens } : {}),
|
|
1086
1322
|
...(attempts !== undefined ? { attempts } : {}),
|
|
1323
|
+
...(spend ? { spend } : {}),
|
|
1324
|
+
// "unknown" is recorded rather than omitted whenever usage exists: a row that reported tokens
|
|
1325
|
+
// with no cache detail at all is a different fact from a row with no usage, and the summary
|
|
1326
|
+
// has to refuse both as a hit-rate denominator.
|
|
1327
|
+
...(loggedUsage || cacheProvenance !== "unknown" ? { cacheProvenance } : {}),
|
|
1087
1328
|
...(logCtx.affinity ? { affinity: logCtx.affinity } : {}),
|
|
1088
1329
|
...(logCtx.affinityReason ? { affinityReason: logCtx.affinityReason } : {}),
|
|
1330
|
+
...(logCtx.conversationStateScrub === "account-change"
|
|
1331
|
+
? { conversationStateScrub: "account-change" }
|
|
1332
|
+
: {}),
|
|
1089
1333
|
...(logCtx.transportPhase ? { transportPhase: logCtx.transportPhase } : {}),
|
|
1090
1334
|
...(logCtx.terminalSource ? { terminalSource: logCtx.terminalSource } : {}),
|
|
1091
1335
|
...(logCtx.routeDecision ? { routeDecision: logCtx.routeDecision } : {}),
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex pool account-change conversation-state portability (#4546).
|
|
3
|
+
*
|
|
4
|
+
* OpenAI `encrypted_content` blobs and `previous_response_id` are bound to the
|
|
5
|
+
* account that minted them. When pool routing serves a live conversation on a
|
|
6
|
+
* different account, the next turn must drop that state once before dispatch so
|
|
7
|
+
* the new account can continue from readable history instead of rejecting the
|
|
8
|
+
* ciphertext.
|
|
9
|
+
*
|
|
10
|
+
* The issuer association lives next to thread affinity in `src/codex/routing.ts`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { CodexAuthContext } from "../../codex/auth-context";
|
|
14
|
+
import {
|
|
15
|
+
peekConversationStateIssuer,
|
|
16
|
+
rememberConversationStateIssuer,
|
|
17
|
+
} from "../../codex/routing";
|
|
18
|
+
import type { OcxParsedRequest } from "../../types";
|
|
19
|
+
import type { RequestLogContext } from "../request-log";
|
|
20
|
+
|
|
21
|
+
export type ConversationStateScrubReason = "account-change";
|
|
22
|
+
|
|
23
|
+
export type PortabilityDenial =
|
|
24
|
+
| "previous-response-id"
|
|
25
|
+
| "provider-conversation-id"
|
|
26
|
+
| "uploaded-file-ids"
|
|
27
|
+
| "encrypted-reasoning";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The parts of a request that bind it to the credential that produced them.
|
|
31
|
+
* Presence is what matters; the values stay opaque so nothing here logs ids.
|
|
32
|
+
*/
|
|
33
|
+
export interface ConversationStateCarriers {
|
|
34
|
+
readonly previousResponseId?: string | null;
|
|
35
|
+
readonly providerConversationId?: string | null;
|
|
36
|
+
readonly fileIds?: readonly string[];
|
|
37
|
+
readonly encryptedReasoning?: unknown;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type PortabilityVerdict =
|
|
41
|
+
| { readonly portable: true }
|
|
42
|
+
| { readonly portable: false; readonly reason: PortabilityDenial };
|
|
43
|
+
|
|
44
|
+
function present(value: unknown): boolean {
|
|
45
|
+
if (value === undefined || value === null) return false;
|
|
46
|
+
if (typeof value === "string" || Array.isArray(value)) return value.length > 0;
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Whether a request's conversational state can move credentials at all.
|
|
52
|
+
*
|
|
53
|
+
* `src/routing/identity-domains.ts` owns this decision once that module lands
|
|
54
|
+
* on this integration line (#4546). Keep the check in this one function so it
|
|
55
|
+
* can be swapped for the shared export without hunting call sites.
|
|
56
|
+
*/
|
|
57
|
+
export function canPortConversationState(
|
|
58
|
+
state: ConversationStateCarriers,
|
|
59
|
+
): PortabilityVerdict {
|
|
60
|
+
if (present(state.previousResponseId)) {
|
|
61
|
+
return { portable: false, reason: "previous-response-id" };
|
|
62
|
+
}
|
|
63
|
+
if (present(state.providerConversationId)) {
|
|
64
|
+
return { portable: false, reason: "provider-conversation-id" };
|
|
65
|
+
}
|
|
66
|
+
if (present(state.fileIds)) {
|
|
67
|
+
return { portable: false, reason: "uploaded-file-ids" };
|
|
68
|
+
}
|
|
69
|
+
if (present(state.encryptedReasoning)) {
|
|
70
|
+
return { portable: false, reason: "encrypted-reasoning" };
|
|
71
|
+
}
|
|
72
|
+
return { portable: true };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function providerConversationIdFromBody(body: Record<string, unknown>): string | undefined {
|
|
76
|
+
const conversation = body.conversation;
|
|
77
|
+
if (typeof conversation === "string" && conversation.trim()) return conversation.trim();
|
|
78
|
+
if (conversation && typeof conversation === "object" && !Array.isArray(conversation)) {
|
|
79
|
+
const id = (conversation as { id?: unknown }).id;
|
|
80
|
+
if (typeof id === "string" && id.trim()) return id.trim();
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function collectFileIds(input: unknown): string[] {
|
|
86
|
+
const ids: string[] = [];
|
|
87
|
+
if (!Array.isArray(input)) return ids;
|
|
88
|
+
for (const item of input) {
|
|
89
|
+
if (!item || typeof item !== "object") continue;
|
|
90
|
+
const record = item as Record<string, unknown>;
|
|
91
|
+
if (typeof record.file_id === "string" && record.file_id.trim()) ids.push(record.file_id);
|
|
92
|
+
if (Array.isArray(record.file_ids)) {
|
|
93
|
+
for (const id of record.file_ids) {
|
|
94
|
+
if (typeof id === "string" && id.trim()) ids.push(id);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
for (const key of ["content", "output"]) {
|
|
98
|
+
const parts = record[key];
|
|
99
|
+
if (!Array.isArray(parts)) continue;
|
|
100
|
+
for (const part of parts) {
|
|
101
|
+
if (!part || typeof part !== "object") continue;
|
|
102
|
+
const partRecord = part as Record<string, unknown>;
|
|
103
|
+
if (typeof partRecord.file_id === "string" && partRecord.file_id.trim()) {
|
|
104
|
+
ids.push(partRecord.file_id);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return ids;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function hasEncryptedReasoning(input: unknown): boolean {
|
|
113
|
+
if (!Array.isArray(input)) return false;
|
|
114
|
+
for (const item of input) {
|
|
115
|
+
if (!item || typeof item !== "object") continue;
|
|
116
|
+
const record = item as Record<string, unknown>;
|
|
117
|
+
if (typeof record.encrypted_content === "string" && record.encrypted_content.length > 0) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
for (const key of ["content", "output"]) {
|
|
121
|
+
const parts = record[key];
|
|
122
|
+
if (!Array.isArray(parts)) continue;
|
|
123
|
+
for (const part of parts) {
|
|
124
|
+
if (!part || typeof part !== "object") continue;
|
|
125
|
+
const encrypted = (part as { encrypted_content?: unknown }).encrypted_content;
|
|
126
|
+
if (typeof encrypted === "string" && encrypted.length > 0) return true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function collectConversationStateCarriers(body: unknown): ConversationStateCarriers {
|
|
134
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return {};
|
|
135
|
+
const record = body as Record<string, unknown>;
|
|
136
|
+
const previousResponseId = typeof record.previous_response_id === "string"
|
|
137
|
+
? record.previous_response_id
|
|
138
|
+
: undefined;
|
|
139
|
+
return {
|
|
140
|
+
previousResponseId,
|
|
141
|
+
providerConversationId: providerConversationIdFromBody(record),
|
|
142
|
+
fileIds: collectFileIds(record.input),
|
|
143
|
+
encryptedReasoning: hasEncryptedReasoning(record.input) ? true : undefined,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Drop account-bound continuation from a request body in place. Readable user
|
|
150
|
+
* messages and plaintext survive; ciphertext and continuation ids do not.
|
|
151
|
+
*/
|
|
152
|
+
export function scrubUnportableConversationStateInPlace(body: unknown): boolean {
|
|
153
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return false;
|
|
154
|
+
const record = body as Record<string, unknown>;
|
|
155
|
+
let changed = false;
|
|
156
|
+
if (typeof record.previous_response_id === "string") {
|
|
157
|
+
delete record.previous_response_id;
|
|
158
|
+
changed = true;
|
|
159
|
+
}
|
|
160
|
+
if (record.conversation != null) {
|
|
161
|
+
delete record.conversation;
|
|
162
|
+
changed = true;
|
|
163
|
+
}
|
|
164
|
+
// Encrypted reasoning and compaction ciphertext are deliberately NOT touched here. #2247
|
|
165
|
+
// already strips them when a pooled thread moves accounts, and in a specific shape: the
|
|
166
|
+
// reasoning item keeps its readable summary with an emptied content array, and the compaction
|
|
167
|
+
// item becomes an operator-readable note. Stripping again from this side produced a different
|
|
168
|
+
// shape and broke that contract for no gain. What #2247 does not cover, and what this function
|
|
169
|
+
// owns, is the continuation state naming server-side objects the new account cannot read:
|
|
170
|
+
// `previous_response_id` and a provider-side conversation id.
|
|
171
|
+
return changed;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function conversationStateBindingFromAuth(
|
|
175
|
+
authCtx: CodexAuthContext,
|
|
176
|
+
fallbackAffinityKey?: string | null,
|
|
177
|
+
): { accountId: string; bindingKey: string } | null {
|
|
178
|
+
if (authCtx.kind !== "pool" && authCtx.kind !== "main-pool") return null;
|
|
179
|
+
const bindingKey = authCtx.affinityKey ?? fallbackAffinityKey ?? undefined;
|
|
180
|
+
if (!bindingKey || !authCtx.accountId) return null;
|
|
181
|
+
return { accountId: authCtx.accountId, bindingKey };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function rememberServingConversationStateIssuer(
|
|
185
|
+
authCtx: CodexAuthContext,
|
|
186
|
+
fallbackAffinityKey?: string | null,
|
|
187
|
+
): void {
|
|
188
|
+
const binding = conversationStateBindingFromAuth(authCtx, fallbackAffinityKey);
|
|
189
|
+
if (!binding) return;
|
|
190
|
+
rememberConversationStateIssuer(binding.bindingKey, binding.accountId);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export interface ApplyAccountChangeConversationStateScrubArgs {
|
|
194
|
+
body: unknown;
|
|
195
|
+
bindingKey: string;
|
|
196
|
+
servingAccountId: string;
|
|
197
|
+
/** Account this request body was prepared for, when this is an in-request move. */
|
|
198
|
+
priorAccountId?: string | null;
|
|
199
|
+
parsed?: Pick<OcxParsedRequest, "previousResponseId" | "_stripReasoningEncryptedContent">;
|
|
200
|
+
logCtx?: RequestLogContext;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* If the serving account is not the issuer of the carried state, strip that
|
|
205
|
+
* state from the outbound body before dispatch. One cold turn, not a permanent
|
|
206
|
+
* downgrade: the next successful serve records the new issuer.
|
|
207
|
+
*/
|
|
208
|
+
export function applyAccountChangeConversationStateScrub(
|
|
209
|
+
args: ApplyAccountChangeConversationStateScrubArgs,
|
|
210
|
+
): boolean {
|
|
211
|
+
const { body, bindingKey, servingAccountId, priorAccountId, parsed, logCtx } = args;
|
|
212
|
+
if (!servingAccountId || !bindingKey) return false;
|
|
213
|
+
const issuer = peekConversationStateIssuer(bindingKey);
|
|
214
|
+
const accountChanged = (issuer != null && issuer !== servingAccountId)
|
|
215
|
+
|| (priorAccountId != null && priorAccountId !== servingAccountId);
|
|
216
|
+
if (!accountChanged) return false;
|
|
217
|
+
if (canPortConversationState(collectConversationStateCarriers(body)).portable) return false;
|
|
218
|
+
const scrubbed = scrubUnportableConversationStateInPlace(body);
|
|
219
|
+
if (!scrubbed) return false;
|
|
220
|
+
if (parsed) {
|
|
221
|
+
delete parsed.previousResponseId;
|
|
222
|
+
parsed._stripReasoningEncryptedContent = true;
|
|
223
|
+
}
|
|
224
|
+
if (logCtx && logCtx.conversationStateScrub !== "account-change") {
|
|
225
|
+
console.warn(
|
|
226
|
+
"[opencodex] dropped continuation state after a Codex pool account change; continuing fresh",
|
|
227
|
+
);
|
|
228
|
+
logCtx.conversationStateScrub = "account-change";
|
|
229
|
+
} else if (logCtx) {
|
|
230
|
+
logCtx.conversationStateScrub = "account-change";
|
|
231
|
+
}
|
|
232
|
+
return true;
|
|
233
|
+
}
|