@bitkyc08/opencodex 2.55.0 → 2.57.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 +10 -0
- package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
- package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
- package/gui/dist/index.html +2 -2
- package/package.json +4 -3
- package/src/adapters/base.ts +21 -0
- package/src/adapters/codebuddy/adapter.ts +2 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
- package/src/adapters/command-code.ts +1 -1
- package/src/adapters/cursor/envelope-echo.ts +8 -2
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/google.ts +7 -7
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro/payload.ts +17 -3
- package/src/adapters/kiro/reasoning.ts +70 -7
- package/src/adapters/kiro/stream.ts +8 -2
- package/src/adapters/kiro/wire.ts +2 -1
- package/src/adapters/kiro-events.ts +21 -13
- 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-name-registry.ts +166 -0
- package/src/adapters/openai-chat/tool-schema.ts +495 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +40 -1452
- 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 +642 -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 +58 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +630 -0
- package/src/bridge/sse.ts +1462 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/claude/desktop-profile.ts +66 -9
- package/src/claude/outbound.ts +18 -0
- package/src/cli/account-main.ts +1 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/combo.ts +10 -1
- package/src/cli/index.ts +48 -5
- package/src/cli/registry.ts +2 -1
- package/src/cli/system-command.ts +4 -4
- package/src/clients/config-export.ts +7 -3
- package/src/codex/account-label.ts +14 -3
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +184 -35
- package/src/codex/account-usability.ts +21 -0
- 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 +566 -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 +431 -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 +252 -35
- package/src/codex/catalog/aggregation.ts +80 -1
- 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 +305 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/remote.ts +30 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +714 -0
- package/src/codex/catalog/routed-gather.ts +895 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/cli-install-provenance.ts +7 -1
- package/src/codex/convergence.ts +7 -2
- package/src/codex/desktop-app/types.ts +11 -2
- package/src/codex/desktop-app/windows.ts +5 -5
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +567 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +89 -1444
- package/src/codex/lineage.ts +458 -0
- package/src/codex/model-entitlements.ts +152 -15
- package/src/codex/pool-refresh-backoff.ts +161 -0
- package/src/codex/quota-rejection.ts +104 -15
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cache-affinity.ts +70 -0
- package/src/codex/routing/cooldown-math.ts +285 -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 +780 -0
- package/src/codex/routing/thread-affinity.ts +586 -0
- package/src/codex/routing/transient-hold-dispatch.ts +141 -0
- package/src/codex/routing.ts +370 -2271
- 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/codex/warmup.ts +1 -1
- package/src/combos/failover.ts +85 -0
- package/src/combos/request.ts +17 -10
- package/src/combos/types.ts +23 -2
- 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/pending-teardown.ts +31 -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 +649 -121
- package/src/images/loop.ts +1 -1
- package/src/lib/errors.ts +17 -0
- package/src/lib/request-execution-budget.ts +198 -23
- package/src/lib/spend-reservation-ledger.ts +958 -0
- package/src/lib/state-store-registrations.ts +6 -2
- package/src/lib/test-home-guard.ts +85 -1
- package/src/lib/upstream-retry.ts +132 -21
- package/src/lib/windows-elevation.ts +76 -14
- package/src/lib/workflow-budget.ts +553 -30
- package/src/oauth/index.ts +2 -2
- package/src/oauth/key-providers.ts +2 -2
- package/src/providers/kiro-models.ts +4 -3
- package/src/providers/label.ts +19 -1
- package/src/providers/model-discovery.ts +16 -0
- 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 +1228 -0
- package/src/providers/registry/entries-extended.ts +1213 -0
- package/src/providers/registry/model-seeds.ts +912 -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/reasoning-envelope.ts +6 -3
- 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 +456 -0
- package/src/routing/probe-lease.ts +613 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/chat-native.ts +37 -9
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +601 -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 +339 -0
- package/src/server/index.ts +45 -2552
- package/src/server/inspection-tee.ts +107 -0
- package/src/server/live.ts +46 -1
- package/src/server/management/combo-routes.ts +10 -1
- 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/relay-eager.ts +2 -0
- package/src/server/relay.ts +14 -19
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +372 -4
- package/src/server/response-log-body.ts +153 -0
- package/src/server/responses/account-change-state.ts +307 -0
- package/src/server/responses/adapter-continuation.ts +540 -0
- package/src/server/responses/adapter-delivery.ts +208 -0
- package/src/server/responses/adapter-dispatch.ts +1042 -0
- package/src/server/responses/codex-ws-wire.ts +5 -0
- package/src/server/responses/collaboration.ts +74 -4
- package/src/server/responses/combo-session-recall.ts +68 -8
- package/src/server/responses/compact.ts +113 -17
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +529 -0
- package/src/server/responses/core-codex-account.ts +907 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +787 -0
- package/src/server/responses/core-errors.ts +170 -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 +298 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/encrypted-payload.ts +0 -1
- package/src/server/responses/input-admission.ts +126 -6
- package/src/server/responses/passthrough-delivery.ts +869 -0
- package/src/server/responses/passthrough-dispatch.ts +1494 -0
- package/src/server/responses/passthrough-error.ts +38 -2
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +1080 -0
- package/src/server/responses/request-send-budget.ts +259 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-spend.ts +147 -0
- package/src/server/responses/request-transport.ts +803 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +476 -0
- package/src/server/responses/sidecar-execution.ts +463 -0
- package/src/server/responses/terminal-guard.ts +65 -4
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/responses-undeclared-tool-guard.ts +9 -5
- package/src/server/workflow-refusal.ts +84 -0
- package/src/service/windows-ops.ts +210 -16
- package/src/service/windows-scheduler.ts +28 -21
- package/src/service.ts +1 -1
- package/src/types/config.ts +34 -1
- package/src/types/request.ts +8 -5
- package/src/types/tools.ts +24 -0
- package/src/types.ts +2 -0
- package/src/update/index.ts +10 -0
- package/src/update/stop-contract.d.mts +1 -0
- package/src/update/stop-contract.mjs +19 -0
- package/src/update/stop-decision.d.mts +1 -1
- package/src/update/stop-decision.mjs +12 -3
- package/src/usage/log.ts +147 -1
- package/src/usage/summary.ts +171 -21
- package/src/vision/anthropic-describe.ts +1 -1
- package/src/vision/describe.ts +5 -5
- package/src/web-search/anthropic-executor.ts +1 -1
- package/src/web-search/exa-executor.ts +1 -1
- package/src/web-search/executor.ts +1 -1
- package/src/web-search/gemini-executor.ts +1 -1
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/ollama-executor.ts +1 -1
- package/src/web-search/parse.ts +67 -14
- package/src/web-search/passthrough-bridge.ts +64 -31
- package/src/web-search/xai-executor.ts +1 -1
package/src/images/loop.ts
CHANGED
|
@@ -615,7 +615,7 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
|
|
|
615
615
|
signal: headerDeadline.signal,
|
|
616
616
|
}, retryRecovery));
|
|
617
617
|
},
|
|
618
|
-
{ abortSignal: headerDeadline.signal, label: "image-bridge-loop" },
|
|
618
|
+
{ replaySafe: true, abortSignal: headerDeadline.signal, label: "image-bridge-loop" },
|
|
619
619
|
);
|
|
620
620
|
}
|
|
621
621
|
} finally {
|
package/src/lib/errors.ts
CHANGED
|
@@ -7,6 +7,15 @@ export interface OcxErrorPayload {
|
|
|
7
7
|
export const ENCRYPTED_FUNCTION_OUTPUT_REJECTION =
|
|
8
8
|
"Encrypted function output content could not be decrypted or decoded.";
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* The error identity for a send this proxy declined to make (#4708).
|
|
12
|
+
*
|
|
13
|
+
* Declared here rather than only on the error class because the classifier is what decides
|
|
14
|
+
* whether the identity survives serialization, and every dispatch path has to name the same
|
|
15
|
+
* string for a client to be able to tell this apart from a provider rate limit.
|
|
16
|
+
*/
|
|
17
|
+
export const SEND_BUDGET_EXHAUSTED_CODE = "request_send_budget_exhausted";
|
|
18
|
+
|
|
10
19
|
/** Canonical human-readable message paths used by Responses upstream failures. */
|
|
11
20
|
export function upstreamErrorMessageFromPayload(payload: unknown): string | undefined {
|
|
12
21
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return undefined;
|
|
@@ -253,6 +262,14 @@ export function classifyError(status: number, type: string, message: string): Oc
|
|
|
253
262
|
) {
|
|
254
263
|
return { message, type: "insufficient_quota", code: "insufficient_quota" };
|
|
255
264
|
}
|
|
265
|
+
// A refusal this proxy made itself, kept apart from the provider rate limits below. The HTTP
|
|
266
|
+
// semantics are identical -- 429, do not send this again now -- but the code is the only thing
|
|
267
|
+
// that tells an operator reading a log whether the provider throttled the request or whether
|
|
268
|
+
// this process declined to send it. Folding it into the generic rate-limit code sent them to
|
|
269
|
+
// the provider's dashboard to explain a decision that was never made there.
|
|
270
|
+
if (type === SEND_BUDGET_EXHAUSTED_CODE) {
|
|
271
|
+
return { message, type: "rate_limit_error", code: SEND_BUDGET_EXHAUSTED_CODE };
|
|
272
|
+
}
|
|
256
273
|
if (
|
|
257
274
|
status === 429 ||
|
|
258
275
|
text.includes("rate limit") ||
|
|
@@ -56,6 +56,7 @@ export type BudgetDenial =
|
|
|
56
56
|
| "final-recovery-spent"
|
|
57
57
|
| "alternate-target-exhausted"
|
|
58
58
|
| "target-transition-exhausted"
|
|
59
|
+
| "spend-exhausted"
|
|
59
60
|
| "not-replay-safe";
|
|
60
61
|
|
|
61
62
|
export interface DispatchIntent {
|
|
@@ -72,23 +73,67 @@ export interface DispatchIntent {
|
|
|
72
73
|
readonly replaySafe?: boolean;
|
|
73
74
|
/**
|
|
74
75
|
* True when the physical send is already reported through another counter -- the retry
|
|
75
|
-
* helpers' `onSendsConsumed` hook. The
|
|
76
|
-
*
|
|
77
|
-
*
|
|
76
|
+
* helpers' `onSendsConsumed` hook. The send is still booked at reservation time, because an
|
|
77
|
+
* advisory reservation cannot stop a concurrent leg; what changes is that the booking is
|
|
78
|
+
* PENDING, and the first send the external reporter names settles it instead of adding a
|
|
79
|
+
* second charge. Charging both is how a four-send cap silently becomes a two-send cap.
|
|
78
80
|
*/
|
|
79
81
|
readonly countedExternally?: boolean;
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
export interface SingleUseDispatchPermit {
|
|
83
85
|
readonly sendClass: SendClass;
|
|
84
|
-
/**
|
|
86
|
+
/**
|
|
87
|
+
* Confirm the dispatch this permit already paid for. The reservation is the charge, so this
|
|
88
|
+
* charges nothing; it is how a leg proves it is the one that sent. A second call returns
|
|
89
|
+
* false, which is what keeps a retry thunk from sending twice on one permit.
|
|
90
|
+
*/
|
|
85
91
|
use(): boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Hand back a reservation that never dispatched -- a credential move that found no alternate,
|
|
94
|
+
* a rebuild abandoned before the send. Idempotent, and a no-op once the permit was used or
|
|
95
|
+
* once an external send reporter already settled it.
|
|
96
|
+
*/
|
|
97
|
+
release(): void;
|
|
98
|
+
/**
|
|
99
|
+
* Take over an externally counted booking, because the layer holding this permit is the one
|
|
100
|
+
* that physically sends.
|
|
101
|
+
*
|
|
102
|
+
* `countedExternally` promises that a retry helper will name this send through
|
|
103
|
+
* `onSendsConsumed`. An adapter that owns its own dispatch ladder -- Kiro's reset loop,
|
|
104
|
+
* Cursor's transport loop -- reserves per physical send instead, so no reporter ever arrives
|
|
105
|
+
* and the pending booking would sit there until it silently swallowed an unrelated later
|
|
106
|
+
* report. Confirming through this method settles the permit AND closes the booking, so the
|
|
107
|
+
* send stays charged exactly once (#4709). Returns false once the permit is settled, which is
|
|
108
|
+
* what keeps one permit from admitting two sends.
|
|
109
|
+
*/
|
|
110
|
+
assumeCharge(): boolean;
|
|
86
111
|
}
|
|
87
112
|
|
|
88
113
|
export type DispatchDecision =
|
|
89
114
|
| { allowed: true; permit: SingleUseDispatchPermit }
|
|
90
115
|
| { allowed: false; reason: BudgetDenial };
|
|
91
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Notified when this request's physical-send count moves.
|
|
119
|
+
*
|
|
120
|
+
* `spent` is the only number here that counts SENDS rather than intentions: a reservation
|
|
121
|
+
* increments it, a refund decrements it, and an externally reported send settles against a
|
|
122
|
+
* booking that was already counted. Anything that books one entry per increment therefore
|
|
123
|
+
* books exactly one entry per physical send -- which is what lets the durable spend ledger
|
|
124
|
+
* have a production caller without every dispatch site in the tree remembering to call it.
|
|
125
|
+
*
|
|
126
|
+
* `charge` may refuse, and a refusal denies the dispatch. That is deliberate: the ledger is
|
|
127
|
+
* the only bound here that survives a restart, so a limit it enforces has to be able to stop a
|
|
128
|
+
* send rather than merely describe one.
|
|
129
|
+
*/
|
|
130
|
+
export interface RequestSendObserver {
|
|
131
|
+
/** Book one physical send. False refuses the dispatch before the budget charges it. */
|
|
132
|
+
charge(): boolean;
|
|
133
|
+
/** Give back a booking whose send never happened. */
|
|
134
|
+
refund(): void;
|
|
135
|
+
}
|
|
136
|
+
|
|
92
137
|
/**
|
|
93
138
|
* Carried on HandleResponsesOptions so a combo child, a rebuild and an alternate-account leg
|
|
94
139
|
* all decrement the same holder. `used` is the existing #4605 counter and still counts every
|
|
@@ -103,6 +148,9 @@ export interface RequestExecutionBudget extends TransientSendBudget {
|
|
|
103
148
|
* Sends still available from the base allowance, capped by a layer's own maximum.
|
|
104
149
|
* Returns 0 when the allowance is gone -- it never floors to 1, because a floor of 1 is
|
|
105
150
|
* what let every recovery leg send one more time forever.
|
|
151
|
+
*
|
|
152
|
+
* A reserved-but-unconfirmed send is spent for this purpose. The alternative -- counting only
|
|
153
|
+
* confirmed sends -- is what let two legs read the same remainder and both dispatch.
|
|
106
154
|
*/
|
|
107
155
|
remainingBaseSends(cap: number): number;
|
|
108
156
|
readonly reserveSpent: boolean;
|
|
@@ -120,17 +168,58 @@ const RESERVE_FUNDED_CLASSES: ReadonlySet<SendClass> = new Set<SendClass>([
|
|
|
120
168
|
|
|
121
169
|
let logicalRequestSeq = 0;
|
|
122
170
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
171
|
+
/**
|
|
172
|
+
* One request's physical-send ledger, held apart from the budget object so a derived policy
|
|
173
|
+
* scope can share the exact same one.
|
|
174
|
+
*
|
|
175
|
+
* `spent` and `pendingExternalSends` belong together: a pending booking is a send that is
|
|
176
|
+
* already counted in `spent` and awaiting its reporter, so a scope that shared one without the
|
|
177
|
+
* other would either charge that send twice or never charge it at all.
|
|
178
|
+
*
|
|
179
|
+
* The durable-spend observer belongs here for the same reason. It books one entry per physical
|
|
180
|
+
* send by watching this counter move, so a derived scope that spent the counter without
|
|
181
|
+
* carrying the observer would move it without booking, and a combo child's sends would go
|
|
182
|
+
* missing from the ledger (#4707).
|
|
183
|
+
*/
|
|
184
|
+
interface SharedSendLedger {
|
|
185
|
+
spent: number;
|
|
186
|
+
pendingExternalSends: number;
|
|
187
|
+
readonly observer?: RequestSendObserver;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const sharedSendLedgers = new WeakMap<RequestExecutionBudget, SharedSendLedger>();
|
|
191
|
+
|
|
192
|
+
function createRequestExecutionBudgetWithLedger(
|
|
193
|
+
policy: RequestExecutionBudgetPolicy,
|
|
194
|
+
logicalRequestId: string | undefined,
|
|
195
|
+
counter: SharedSendLedger,
|
|
126
196
|
): RequestExecutionBudget {
|
|
197
|
+
const observer = counter.observer;
|
|
127
198
|
let reserveSpent = false;
|
|
128
199
|
let alternateTargetSends = 0;
|
|
129
200
|
let targetTransitions = 0;
|
|
130
201
|
let lastTargetKey: string | undefined;
|
|
131
202
|
|
|
132
203
|
const budget: RequestExecutionBudget = {
|
|
133
|
-
used:
|
|
204
|
+
get used(): number { return counter.spent; },
|
|
205
|
+
set used(next: number) {
|
|
206
|
+
// The retry helpers report their real send count by assigning through this field. A
|
|
207
|
+
// reservation taken with `countedExternally` has already booked one of those sends, so
|
|
208
|
+
// the report settles the pending booking first and only the surplus is charged.
|
|
209
|
+
const delta = next - counter.spent;
|
|
210
|
+
if (delta <= 0) {
|
|
211
|
+
counter.spent = Math.max(0, next);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const settled = Math.min(delta, counter.pendingExternalSends);
|
|
215
|
+
counter.pendingExternalSends -= settled;
|
|
216
|
+
const charged = delta - settled;
|
|
217
|
+
counter.spent += charged;
|
|
218
|
+
// These sends have already left. The ledger records them even past a ceiling it would
|
|
219
|
+
// have refused, because refusing after the fact only hides spend that was really
|
|
220
|
+
// incurred -- the refusal has to happen at the reservation below, or not at all.
|
|
221
|
+
for (let index = 0; index < charged; index += 1) observer?.charge();
|
|
222
|
+
},
|
|
134
223
|
logicalRequestId: logicalRequestId ?? `lr-${Date.now().toString(36)}-${(logicalRequestSeq += 1).toString(36)}`,
|
|
135
224
|
policyVersion: REQUEST_BUDGET_POLICY_VERSION,
|
|
136
225
|
policy,
|
|
@@ -140,11 +229,11 @@ export function createRequestExecutionBudget(
|
|
|
140
229
|
get lastTargetKey() { return lastTargetKey; },
|
|
141
230
|
remainingBaseSends(cap: number): number {
|
|
142
231
|
const capped = Number.isFinite(cap) ? Math.trunc(cap) : 0;
|
|
143
|
-
return Math.max(0, Math.min(capped, policy.baseSendAllowance -
|
|
232
|
+
return Math.max(0, Math.min(capped, policy.baseSendAllowance - counter.spent));
|
|
144
233
|
},
|
|
145
234
|
reserveDispatch(intent: DispatchIntent): DispatchDecision {
|
|
146
235
|
if (intent.replaySafe === false) return { allowed: false, reason: "not-replay-safe" };
|
|
147
|
-
if (
|
|
236
|
+
if (counter.spent >= policy.maxTotalModelSends) return { allowed: false, reason: "total-exhausted" };
|
|
148
237
|
|
|
149
238
|
const changesTarget = lastTargetKey !== undefined && lastTargetKey !== intent.targetKey;
|
|
150
239
|
const isAlternateTarget = changesTarget || intent.sendClass === "account-failover"
|
|
@@ -159,7 +248,7 @@ export function createRequestExecutionBudget(
|
|
|
159
248
|
// The base allowance is spent first. Only once it is gone does a recovery class reach
|
|
160
249
|
// for the single shared reserve -- an account move and a validated rebuild cannot each
|
|
161
250
|
// take one.
|
|
162
|
-
const drawsReserve =
|
|
251
|
+
const drawsReserve = policy.baseSendAllowance - counter.spent <= 0;
|
|
163
252
|
if (drawsReserve) {
|
|
164
253
|
if (!RESERVE_FUNDED_CLASSES.has(intent.sendClass)) {
|
|
165
254
|
return { allowed: false, reason: "base-allowance-exhausted" };
|
|
@@ -169,32 +258,118 @@ export function createRequestExecutionBudget(
|
|
|
169
258
|
}
|
|
170
259
|
}
|
|
171
260
|
|
|
172
|
-
|
|
261
|
+
// Consulted last, because it is the only bound here that WRITES. A ledger entry booked
|
|
262
|
+
// for a dispatch a cheaper check above would have refused is spend this request never
|
|
263
|
+
// makes, and it would hold those tokens against the scope until retention expired.
|
|
264
|
+
if (observer && !observer.charge()) return { allowed: false, reason: "spend-exhausted" };
|
|
265
|
+
|
|
266
|
+
// THE RESERVATION IS THE CHARGE. Deciding here and charging in `use()` left a window in
|
|
267
|
+
// which two legs read the same remainder, both received a permit, and both dispatched:
|
|
268
|
+
// one remaining send admitted two physical sends, which is the per-request multiplication
|
|
269
|
+
// this budget exists to stop. Everything is booked now; `release()` is the way back.
|
|
270
|
+
const previousTargetKey = lastTargetKey;
|
|
271
|
+
counter.spent += 1;
|
|
272
|
+
if (intent.countedExternally === true) counter.pendingExternalSends += 1;
|
|
273
|
+
if (drawsReserve) reserveSpent = true;
|
|
274
|
+
if (isAlternateTarget) alternateTargetSends += 1;
|
|
275
|
+
if (changesTarget) targetTransitions += 1;
|
|
276
|
+
lastTargetKey = intent.targetKey;
|
|
277
|
+
|
|
278
|
+
let settled: "open" | "used" | "released" = "open";
|
|
173
279
|
return {
|
|
174
280
|
allowed: true,
|
|
175
281
|
permit: {
|
|
176
282
|
sendClass: intent.sendClass,
|
|
177
283
|
use(): boolean {
|
|
178
|
-
if (
|
|
179
|
-
|
|
180
|
-
// Charged here, immediately before the physical send, rather than reported after
|
|
181
|
-
// the helper returns: a counter that is only reconciled afterwards cannot stop two
|
|
182
|
-
// concurrent legs that both read the same remainder.
|
|
183
|
-
if (intent.countedExternally !== true) budget.used += 1;
|
|
184
|
-
if (drawsReserve) reserveSpent = true;
|
|
185
|
-
if (isAlternateTarget) alternateTargetSends += 1;
|
|
186
|
-
if (changesTarget) targetTransitions += 1;
|
|
187
|
-
lastTargetKey = intent.targetKey;
|
|
284
|
+
if (settled !== "open") return false;
|
|
285
|
+
settled = "used";
|
|
188
286
|
return true;
|
|
189
287
|
},
|
|
288
|
+
assumeCharge(): boolean {
|
|
289
|
+
if (settled !== "open") return false;
|
|
290
|
+
settled = "used";
|
|
291
|
+
// The booking this reservation made for an external reporter is now owned by the
|
|
292
|
+
// caller. Leaving it pending is not harmless: the next `used` report of this request
|
|
293
|
+
// would settle against it and one real send would go uncharged.
|
|
294
|
+
if (intent.countedExternally === true && counter.pendingExternalSends > 0) {
|
|
295
|
+
counter.pendingExternalSends -= 1;
|
|
296
|
+
}
|
|
297
|
+
return true;
|
|
298
|
+
},
|
|
299
|
+
release(): void {
|
|
300
|
+
if (settled !== "open") return;
|
|
301
|
+
settled = "released";
|
|
302
|
+
// An externally counted reservation the reporter already settled paid for a send
|
|
303
|
+
// that physically happened. Refunding it would hand the request a free send back.
|
|
304
|
+
if (intent.countedExternally === true) {
|
|
305
|
+
if (counter.pendingExternalSends === 0) return;
|
|
306
|
+
counter.pendingExternalSends -= 1;
|
|
307
|
+
}
|
|
308
|
+
counter.spent -= 1;
|
|
309
|
+
observer?.refund();
|
|
310
|
+
if (drawsReserve) reserveSpent = false;
|
|
311
|
+
if (isAlternateTarget) alternateTargetSends -= 1;
|
|
312
|
+
if (changesTarget) targetTransitions -= 1;
|
|
313
|
+
lastTargetKey = previousTargetKey;
|
|
314
|
+
},
|
|
190
315
|
},
|
|
191
316
|
};
|
|
192
317
|
},
|
|
193
318
|
};
|
|
194
|
-
|
|
319
|
+
sharedSendLedgers.set(budget, counter);
|
|
195
320
|
return budget;
|
|
196
321
|
}
|
|
197
322
|
|
|
323
|
+
export function createRequestExecutionBudget(
|
|
324
|
+
policy: RequestExecutionBudgetPolicy = CODEX_TEXT_GUARDED_BUDGET_POLICY,
|
|
325
|
+
logicalRequestId?: string,
|
|
326
|
+
observer?: RequestSendObserver,
|
|
327
|
+
): RequestExecutionBudget {
|
|
328
|
+
return createRequestExecutionBudgetWithLedger(policy, logicalRequestId, {
|
|
329
|
+
spent: 0,
|
|
330
|
+
pendingExternalSends: 0,
|
|
331
|
+
...(observer ? { observer } : {}),
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* A budget that applies its own policy and keeps its own recovery ledgers while spending the
|
|
337
|
+
* parent's exact physical-send ledger.
|
|
338
|
+
*
|
|
339
|
+
* Aliasing the public `used` property was not enough, and that is the whole defect. The factory
|
|
340
|
+
* reads its own private counter back in `remainingBaseSends`, in the total check, and in the
|
|
341
|
+
* reserve test, so an aliased scope answered every admission question from a counter that only
|
|
342
|
+
* ever saw its own reservations. A combo's per-target holdback is computed from
|
|
343
|
+
* `maxTotalModelSends` and is therefore unenforceable unless the scope actually observes what
|
|
344
|
+
* the request has already spent.
|
|
345
|
+
*/
|
|
346
|
+
export function deriveRequestExecutionBudget(
|
|
347
|
+
parent: RequestExecutionBudget,
|
|
348
|
+
policy: RequestExecutionBudgetPolicy,
|
|
349
|
+
): RequestExecutionBudget {
|
|
350
|
+
return createRequestExecutionBudgetWithLedger(policy, parent.logicalRequestId, ledgerFor(parent));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* A budget that did not come from this factory still honors the public `used` contract, so
|
|
355
|
+
* bridge onto it rather than failing the request. `isRequestExecutionBudget` is a shape test,
|
|
356
|
+
* so a stub can reach here; turning that into a thrown error would convert a routing request
|
|
357
|
+
* into a 500 to report a condition production never produces. Only a factory-backed parent can
|
|
358
|
+
* share pending external bookings and a durable-spend observer, which are private by
|
|
359
|
+
* construction; a bridged scope keeps the parent's spend accurate and books nothing of its own.
|
|
360
|
+
*/
|
|
361
|
+
function ledgerFor(parent: RequestExecutionBudget): SharedSendLedger {
|
|
362
|
+
const existing = sharedSendLedgers.get(parent);
|
|
363
|
+
if (existing) return existing;
|
|
364
|
+
let pendingExternalSends = 0;
|
|
365
|
+
return {
|
|
366
|
+
get spent(): number { return parent.used; },
|
|
367
|
+
set spent(next: number) { parent.used = next; },
|
|
368
|
+
get pendingExternalSends(): number { return pendingExternalSends; },
|
|
369
|
+
set pendingExternalSends(next: number) { pendingExternalSends = next; },
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
198
373
|
export function isRequestExecutionBudget(
|
|
199
374
|
value: TransientSendBudget | undefined,
|
|
200
375
|
): value is RequestExecutionBudget {
|