@bitkyc08/opencodex 2.79.0 → 2.81.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/gui/dist/assets/App-7yhoys8a.js +54 -0
- package/gui/dist/assets/Tray-EwAOsP0S.js +1 -0
- package/gui/dist/assets/index-RsoaAxXY.js +94 -0
- package/gui/dist/assets/quota-summary-D2lN8tDm.js +1 -0
- package/gui/dist/index.html +1 -1
- package/gui/dist/provider-icons/claude-green.svg +1 -0
- package/package.json +3 -2
- package/src/adapters/anthropic-model-contract.ts +8 -2
- package/src/adapters/anthropic.ts +7 -2
- package/src/adapters/codebuddy/mcp-server.ts +2 -25
- package/src/adapters/codebuddy/tool-bridge.ts +1 -7
- package/src/adapters/coding-agent/mcp-server.ts +33 -0
- package/src/adapters/coding-agent/protocol.ts +122 -17
- package/src/adapters/coding-agent/tool-catalog.ts +75 -0
- package/src/adapters/coding-agent/turn.ts +101 -44
- package/src/adapters/cursor/catalog.ts +8 -0
- package/src/adapters/cursor/effort-map.ts +2 -0
- package/src/adapters/cursor/live-transport.ts +5 -3
- package/src/adapters/cursor/native-exec-fs.ts +12 -9
- package/src/adapters/cursor/native-exec.ts +13 -5
- package/src/adapters/cursor/protobuf-request.ts +13 -7
- package/src/adapters/cursor/request-builder.ts +23 -10
- package/src/adapters/cursor/tool-definitions.ts +9 -6
- package/src/adapters/cursor/tool-guidance.ts +29 -8
- package/src/adapters/cursor/tool-naming.ts +40 -7
- package/src/adapters/cursor/tool-wording.ts +46 -0
- package/src/adapters/cursor.ts +3 -0
- package/src/adapters/devin/live-models.ts +2 -0
- package/src/adapters/devin.ts +25 -11
- package/src/adapters/kiro-retry.ts +9 -4
- package/src/adapters/openai-chat/messages.ts +7 -4
- package/src/adapters/physical-send.ts +8 -5
- package/src/adapters/qoder/adapter.ts +41 -6
- package/src/adapters/qoder/scaffold-guard.ts +2 -3
- package/src/adapters/zed.ts +16 -1
- package/src/claude/inbound-cache-stabilize.ts +4 -9
- package/src/claude/inbound.ts +10 -0
- package/src/claude/intercept/model-bindings.ts +1 -0
- package/src/claude/intercept/picker-listener.ts +71 -4
- package/src/cli/account-anthropic-threshold.ts +14 -5
- package/src/cli/account-api.ts +3 -1
- package/src/cli/account-extended.ts +13 -9
- package/src/cli/account-policy-dto.ts +1 -1
- package/src/cli/account-policy.ts +13 -4
- package/src/cli/account.ts +3 -3
- package/src/cli/capabilities-accounts.ts +2 -2
- package/src/cli/capabilities-agents-routing.ts +24 -0
- package/src/cli/capabilities-integrations.ts +16 -0
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/dispatch.ts +10 -1
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +11 -5
- package/src/cli/integration-plan-dto.ts +1 -0
- package/src/cli/integration-preview.ts +8 -1
- package/src/cli/integrations.ts +32 -3
- package/src/cli/message-args.ts +34 -0
- package/src/cli/message-command.ts +43 -0
- package/src/cli/message-runtime.ts +6 -0
- package/src/cli/observe.ts +3 -1
- package/src/cli/provider.ts +29 -6
- package/src/cli/registry.ts +24 -2
- package/src/clients/config-export/commandcode.ts +131 -0
- package/src/clients/config-export/contracts.ts +12 -0
- package/src/clients/config-export.ts +64 -1
- package/src/codex/auth-api/account-list.ts +11 -3
- package/src/codex/auth-api/main-account-probe.ts +25 -8
- package/src/codex/auth-api/pool-mode-gate.ts +33 -9
- package/src/codex/auth-api/pool-quota-probe.ts +15 -1
- package/src/codex/auth-api/routes.ts +1 -0
- package/src/codex/autostart-health.ts +3 -3
- package/src/codex/catalog/auto-review.ts +13 -6
- package/src/codex/catalog/build-entries.ts +15 -7
- package/src/codex/catalog/control-plane.ts +45 -0
- package/src/codex/catalog/effort.ts +11 -1
- package/src/codex/catalog/gather-capture.ts +5 -3
- package/src/codex/catalog/parsing.ts +2 -1
- package/src/codex/catalog/provider-models.ts +15 -9
- package/src/codex/control-plane-models.ts +6 -0
- package/src/codex/journal.ts +36 -5
- package/src/codex/shim-overlay.ts +9 -1
- package/src/combos/jev-decision-contract.ts +25 -2
- package/src/combos/jev.ts +6 -3
- package/src/combos/types.ts +1 -1
- package/src/config/diagnostics.ts +27 -16
- package/src/config/load-degrade.ts +34 -0
- package/src/config/schema/anthropic-account-pool.ts +143 -0
- package/src/config/schema/config-schema.ts +7 -1
- package/src/config/schema/leaf-validators.ts +4 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +347 -199
- package/src/generated/model-metadata.ts +5 -4
- package/src/images/loop.ts +2 -1
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/mutation-plan.ts +7 -0
- package/src/integrations/registry.ts +8 -0
- package/src/integrations/state.ts +9 -2
- package/src/integrations/writer.ts +8 -2
- package/src/lib/lab-live-route-production.ts +9 -1
- package/src/lib/request-execution-budget.ts +236 -58
- package/src/lib/spend-pool-alias-validation.ts +79 -0
- package/src/lib/spend-pool-continuity.ts +34 -0
- package/src/lib/spend-reservation-ledger.ts +412 -50
- package/src/lib/state-store-registrations.ts +20 -3
- package/src/lib/upstream-retry.ts +17 -5
- package/src/lib/workflow-budget.ts +53 -11
- package/src/messaging/budget.ts +45 -0
- package/src/messaging/discovery.ts +57 -0
- package/src/messaging/envelope.ts +49 -0
- package/src/messaging/input.ts +37 -0
- package/src/messaging/rpc.ts +196 -0
- package/src/messaging/send.ts +58 -0
- package/src/messaging/socket.ts +72 -0
- package/src/messaging/types.ts +29 -0
- package/src/oauth/anthropic-account-refusal.ts +51 -17
- package/src/oauth/anthropic-account-threshold.ts +8 -2
- package/src/oauth/anthropic-continuity.ts +15 -1
- package/src/oauth/anthropic-model-quota.ts +120 -48
- package/src/oauth/anthropic-model-routes.ts +11 -3
- package/src/oauth/anthropic-oauth-definitions.ts +98 -0
- package/src/oauth/anthropic-pool-config.ts +30 -0
- package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
- package/src/oauth/anthropic-routing.ts +1202 -866
- package/src/oauth/anthropic-send-ownership.ts +41 -0
- package/src/oauth/anthropic.ts +4 -1
- package/src/oauth/generic-account-failover.ts +3 -1
- package/src/oauth/health.ts +8 -6
- package/src/oauth/index.ts +90 -173
- package/src/oauth/kiro.ts +22 -9
- package/src/oauth/login-cli.ts +2 -0
- package/src/oauth/model-discovery-auth.ts +43 -0
- package/src/oauth/pool-kernel.ts +11 -5
- package/src/oauth/pool-settings-capability.ts +5 -3
- package/src/oauth/store-anthropic-instance.ts +87 -0
- package/src/oauth/store.ts +67 -9
- package/src/oauth/token-guardian.ts +2 -0
- package/src/oauth/types.ts +2 -0
- package/src/protocols/plan-snapshot.ts +5 -0
- package/src/protocols/settings.ts +20 -5
- package/src/providers/anthropic-instance-id.ts +47 -0
- package/src/providers/anthropic-instance.ts +48 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
- package/src/providers/derive.ts +2 -0
- package/src/providers/kiro-models.ts +3 -0
- package/src/providers/label.ts +1 -1
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/quota/account-cache.ts +37 -11
- package/src/providers/quota/anthropic-account-quota.ts +93 -0
- package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
- package/src/providers/quota/report-cache.ts +24 -1
- package/src/providers/quota/vendor-probes-oauth.ts +46 -14
- package/src/providers/quota.ts +22 -32
- package/src/providers/registry/entries-core.ts +35 -30
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/model-seeds.ts +7 -4
- package/src/providers/registry/types.ts +3 -0
- package/src/providers/registry.ts +3 -3
- package/src/responses/spill-store.ts +5 -1
- package/src/responses/state/metrics.ts +87 -0
- package/src/responses/state/snapshot-codec.ts +6 -0
- package/src/responses/state/spill-queue.ts +1 -0
- package/src/responses/state/unforced-store-false.ts +55 -0
- package/src/responses/state.ts +50 -66
- package/src/router.ts +28 -4
- package/src/routing/compatibility/assemble.ts +1 -1
- package/src/routing/quota.ts +7 -3
- package/src/server/auth-cors.ts +5 -0
- package/src/server/chat-completions.ts +6 -1
- package/src/server/chat-native.ts +45 -25
- package/src/server/claude-messages.ts +16 -2
- package/src/server/gui-static.ts +38 -4
- package/src/server/index/spend-ledger-lifecycle.ts +10 -4
- package/src/server/index.ts +16 -16
- package/src/server/inference/context.ts +1 -1
- package/src/server/lifecycle.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +30 -6
- package/src/server/management/anthropic-account-threshold.ts +12 -9
- package/src/server/management/anthropic-pool-settings.ts +72 -0
- package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
- package/src/server/management/config-routes.ts +27 -7
- package/src/server/management/decision-routes.ts +2 -1
- package/src/server/management/integration-routes.ts +25 -2
- package/src/server/management/oauth-account-routes.ts +79 -98
- package/src/server/management/provider-instance-ownership.ts +44 -0
- package/src/server/management/provider-routes.ts +21 -28
- package/src/server/management/route-registry.ts +3 -1
- package/src/server/management/routing-profile-routes.ts +4 -4
- package/src/server/management/system-restart.ts +44 -14
- package/src/server/management/system-routes.ts +24 -2
- package/src/server/management/vision-sidecar-options.ts +9 -6
- package/src/server/management/web-search-sidecar-options.ts +51 -2
- package/src/server/management-auth.ts +6 -0
- package/src/server/messages-native-eligibility.ts +3 -2
- package/src/server/messages-native-oauth.ts +86 -41
- package/src/server/messages-native-scope.ts +4 -1
- package/src/server/messages-native-selector.ts +39 -0
- package/src/server/messages-native.ts +84 -25
- package/src/server/request-log.ts +5 -0
- package/src/server/responses/adapter-continuation.ts +29 -17
- package/src/server/responses/adapter-dispatch.ts +47 -17
- package/src/server/responses/antigravity-validation-refusal.ts +20 -6
- package/src/server/responses/combo-requested-effort.ts +14 -0
- package/src/server/responses/compact.ts +25 -8
- package/src/server/responses/compaction-recovery.ts +1 -1
- package/src/server/responses/core-combo-native.ts +4 -1
- package/src/server/responses/core-combo.ts +21 -18
- package/src/server/responses/core-normalize.ts +1 -0
- package/src/server/responses/core-options.ts +3 -1
- package/src/server/responses/core-replay.ts +5 -0
- package/src/server/responses/passthrough-delivery.ts +7 -3
- package/src/server/responses/passthrough-dispatch.ts +21 -8
- package/src/server/responses/request-prepare.ts +37 -5
- package/src/server/responses/request-send-budget.ts +74 -37
- package/src/server/responses/request-spend.ts +325 -33
- package/src/server/responses/request-transport.ts +132 -52
- package/src/server/responses/run-turn-execution.ts +11 -3
- package/src/server/responses/sidecar-execution.ts +96 -14
- package/src/server/responses-undeclared-tool-guard.ts +5 -1
- package/src/server/workflow-refusal.ts +30 -0
- package/src/service/desktop-startup.ts +100 -3
- package/src/service/ownership-mutation-lease.d.mts +1 -1
- package/src/service/ownership-mutation-lease.mjs +5 -7
- package/src/sidecar/anthropic-binding.ts +79 -0
- package/src/sidecar/auth.ts +76 -2
- package/src/types/anthropic-account-pool.ts +33 -0
- package/src/types/config.ts +12 -28
- package/src/types/provider.ts +5 -0
- package/src/types/tools.ts +7 -0
- package/src/types.ts +4 -0
- package/src/usage/cost.ts +3 -2
- package/src/usage/expected-prices.ts +66 -14
- package/src/vision/anthropic-describe.ts +16 -3
- package/src/vision/index.ts +3 -0
- package/src/vision/plan.ts +19 -6
- package/src/web-search/alpha-search.ts +7 -1
- package/src/web-search/anthropic-executor.ts +16 -4
- package/src/web-search/header-deadline.ts +67 -0
- package/src/web-search/index.ts +7 -3
- package/src/web-search/loop.ts +32 -10
- package/src/web-search/passthrough-bridge.ts +4 -1
- package/src/web-search/sidecar-providers.ts +4 -3
- package/gui/dist/assets/App-BOUa-ek3.js +0 -54
- package/gui/dist/assets/Tray-DUtzB4zF.js +0 -1
- package/gui/dist/assets/index-DwHNDF4s.js +0 -94
- package/gui/dist/assets/quota-summary-BuL4WIBo.js +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import type { RequestSendObserver } from "../../lib/request-execution-budget";
|
|
3
|
-
import { sharedSpendLedger, type SpendReservationLedger, type SpendScopes } from "../../lib/spend-reservation-ledger";
|
|
3
|
+
import { sharedSpendLedger, sharedSpendPolicy, type SpendReservationLedger, type SpendScopes, type SpendSeed, type SpendReservationProof, type SpendAdmissionPolicy } from "../../lib/spend-reservation-ledger";
|
|
4
4
|
import { SpendLedgerOwnerError } from "../../lib/spend-ledger-owner";
|
|
5
5
|
import { markLocalRequestLogRefusal, type RequestLogContext } from "../request-log";
|
|
6
6
|
import { recordWorkflowRefusalEvent, workflowDenialSummary, type WorkflowDenial } from "../../lib/workflow-budget";
|
|
@@ -16,9 +16,22 @@ export interface RequestSpendSettlement {
|
|
|
16
16
|
settle(usage: TerminalSpendUsage | undefined): void;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
export interface SpendTargetIdentity extends SpendScopes { readonly targetKey?: string }
|
|
20
|
+
export interface RequestSpendReporter {
|
|
21
|
+
start(seed: SpendSeed, ordinal: number): void;
|
|
22
|
+
rebindTarget(target: SpendTargetIdentity, ordinal: number): boolean;
|
|
23
|
+
report(sends: number): void;
|
|
24
|
+
close(): void;
|
|
25
|
+
}
|
|
26
|
+
|
|
19
27
|
export interface RequestSpendTracker extends RequestSendObserver, RequestSpendSettlement {
|
|
20
28
|
/** Dispatches this request lost to a ledger ceiling. Zero on every ordinary request. */
|
|
21
29
|
readonly refusals: number;
|
|
30
|
+
readonly enforced: boolean;
|
|
31
|
+
ensureSeed(target: SpendTargetIdentity, prepaidProof?: SpendReservationProof): SpendSeed | undefined;
|
|
32
|
+
beginReporter(): RequestSpendReporter;
|
|
33
|
+
reportFromSeed(seed: SpendSeed, ordinal: number): void;
|
|
34
|
+
requestFinalSettlement(usage: TerminalSpendUsage | undefined): void;
|
|
22
35
|
}
|
|
23
36
|
|
|
24
37
|
/**
|
|
@@ -43,8 +56,8 @@ export interface RequestSpendTracker extends RequestSendObserver, RequestSpendSe
|
|
|
43
56
|
export function createRequestSpendTracker(
|
|
44
57
|
logCtx: Pick<
|
|
45
58
|
RequestLogContext,
|
|
46
|
-
"provider" | "accountLogLabel" | "usageLogInputTokens" | "spendOutputCeilingTokens" | "spendInputEstimateTokens"
|
|
47
|
-
> & Partial<Pick<RequestLogContext, "localTerminalReason" | "terminalSource" | "errorCode">>,
|
|
59
|
+
"provider" | "accountLogLabel" | "usageLogInputTokens" | "spendOutputCeilingTokens" | "spendInputEstimateTokens" | "spendPoolId"
|
|
60
|
+
> & Partial<Pick<RequestLogContext, "localTerminalReason" | "terminalSource" | "errorCode" | "spendRefusalDetail">>,
|
|
48
61
|
rootId: string | undefined,
|
|
49
62
|
injected?: SpendReservationLedger,
|
|
50
63
|
): RequestSpendTracker {
|
|
@@ -54,47 +67,312 @@ export function createRequestSpendTracker(
|
|
|
54
67
|
// one. It also means the home in effect at dispatch is the one that gets written.
|
|
55
68
|
let ledgerRef: SpendReservationLedger | undefined = injected;
|
|
56
69
|
const ledger = (): SpendReservationLedger => (ledgerRef ??= sharedSpendLedger());
|
|
57
|
-
|
|
70
|
+
let admissionPolicy: SpendAdmissionPolicy | undefined;
|
|
71
|
+
let initialEnforced: boolean | undefined;
|
|
72
|
+
// Outstanding entries; exact dispatch reports move their reservation to the end.
|
|
58
73
|
const live: string[] = [];
|
|
74
|
+
const pendingDispatch = new Set<string>();
|
|
59
75
|
let refusals = 0;
|
|
60
76
|
let resolved = false;
|
|
61
77
|
let terminalProcessed = false;
|
|
78
|
+
let seededAccounting = false;
|
|
79
|
+
let finalRequested = false;
|
|
80
|
+
let finalUsage: TerminalSpendUsage | undefined;
|
|
81
|
+
let reporters = 0;
|
|
82
|
+
const seeds = new Map<string, { seed: SpendSeed; started: boolean; reported: boolean }>();
|
|
83
|
+
// Abandoned capabilities retain cleanup ownership, never admission lookup.
|
|
84
|
+
const retiredSeeds = new Set<SpendSeed>();
|
|
85
|
+
const resolvedSendIds = new Set<string>();
|
|
86
|
+
const completedSeeds = new Set<SpendSeed>();
|
|
87
|
+
const physical = new Map<number, { seed: SpendSeed; sendId?: string; reportPending?: boolean; reportComplete?: boolean; estimate?: { inputTokens: number; outputCeilingTokens: number } }>();
|
|
88
|
+
const selectedScopes = (target: SpendTargetIdentity = {}): SpendScopes => ({
|
|
89
|
+
...(rootId !== undefined ? { rootId } : {}),
|
|
90
|
+
...(target.identityId ?? logCtx.accountLogLabel ? { identityId: target.identityId ?? logCtx.accountLogLabel } : {}),
|
|
91
|
+
...(target.poolId ?? logCtx.spendPoolId ?? logCtx.provider ? { poolId: target.poolId ?? logCtx.spendPoolId ?? logCtx.provider } : {}),
|
|
92
|
+
});
|
|
93
|
+
const applicable = (scopes: SpendScopes): boolean => {
|
|
94
|
+
if (initialEnforced !== undefined) return initialEnforced;
|
|
95
|
+
const policy = admissionPolicy ?? ledgerRef?.policy ?? sharedSpendPolicy();
|
|
96
|
+
return (scopes.rootId !== undefined && policy.root.maxTokens !== undefined)
|
|
97
|
+
|| (scopes.identityId !== undefined && policy.identity.maxTokens !== undefined)
|
|
98
|
+
|| (scopes.poolId !== undefined && policy.pool.maxTokens !== undefined);
|
|
99
|
+
};
|
|
100
|
+
const estimates = () => ({ inputTokens: logCtx.spendInputEstimateTokens ?? logCtx.usageLogInputTokens ?? 0,
|
|
101
|
+
outputCeilingTokens: logCtx.spendOutputCeilingTokens ?? 0 });
|
|
102
|
+
const seedState = (seed: SpendSeed) => [...seeds.values()].find(state => state.seed === seed);
|
|
103
|
+
const cleanupRetiredSeeds = (): boolean => {
|
|
104
|
+
for (const seed of retiredSeeds) if (ledger().finishSeed(seed, tracker)) retiredSeeds.delete(seed);
|
|
105
|
+
return retiredSeeds.size === 0;
|
|
106
|
+
};
|
|
107
|
+
const tryFinalizeSeeds = (): boolean => {
|
|
108
|
+
retryPhysicalReports();
|
|
109
|
+
// All starts are booked before producers close. Ordinal, never callback order, owns usage.
|
|
110
|
+
const entries = [...physical.entries()].sort((a, b) => a[0] - b[0]);
|
|
111
|
+
const terminal = entries.at(-1)?.[1].sendId;
|
|
112
|
+
const ids: string[] = [];
|
|
113
|
+
for (const [, entry] of entries) {
|
|
114
|
+
if (!entry.sendId) return false;
|
|
115
|
+
const reported = entry.sendId === terminal && (typeof finalUsage?.inputTokens === "number" || typeof finalUsage?.outputTokens === "number");
|
|
116
|
+
if (!resolvedSendIds.has(entry.sendId)) {
|
|
117
|
+
const ok = reported ? ledger().settle(entry.sendId, { inputTokens: finalUsage?.inputTokens ?? 0, outputTokens: finalUsage?.outputTokens ?? 0 })
|
|
118
|
+
: ledger().markLost(entry.sendId);
|
|
119
|
+
if (!ok) return false;
|
|
120
|
+
resolvedSendIds.add(entry.sendId);
|
|
121
|
+
}
|
|
122
|
+
ids.push(entry.sendId);
|
|
123
|
+
}
|
|
124
|
+
for (const state of seeds.values()) if (!state.started && !completedSeeds.has(state.seed)) {
|
|
125
|
+
if (!resolvedSendIds.has(state.seed.sendId) && !ledger().abandon(state.seed.sendId)) return false;
|
|
126
|
+
resolvedSendIds.add(state.seed.sendId);
|
|
127
|
+
ids.push(state.seed.sendId);
|
|
128
|
+
}
|
|
129
|
+
if (!cleanupRetiredSeeds()) return false;
|
|
130
|
+
if (!ledger().forgetResolved(ids, tracker)) return false;
|
|
131
|
+
for (const state of seeds.values()) if (!completedSeeds.has(state.seed)) {
|
|
132
|
+
if (!ledger().finishSeed(state.seed, tracker)) return false;
|
|
133
|
+
completedSeeds.add(state.seed);
|
|
134
|
+
}
|
|
135
|
+
resolved = true;
|
|
136
|
+
return true;
|
|
137
|
+
};
|
|
138
|
+
const retryPhysicalReports = (): void => {
|
|
139
|
+
for (const [ordinal, entry] of physical) if (entry.reportPending && !entry.reportComplete) tracker.reportFromSeed(entry.seed, ordinal);
|
|
140
|
+
};
|
|
141
|
+
const deferCleanup = (): void => ledger().deferCleanup(tracker, () => {
|
|
142
|
+
if (resolved) return true;
|
|
143
|
+
retryPhysicalReports();
|
|
144
|
+
if (finalRequested && reporters === 0) return tryFinalizeSeeds();
|
|
145
|
+
return cleanupRetiredSeeds() && reporters === 0;
|
|
146
|
+
});
|
|
147
|
+
const finalizeSeeds = (): void => {
|
|
148
|
+
if (!finalRequested || reporters !== 0 || resolved || !seededAccounting) return;
|
|
149
|
+
// Own terminal retry before an append can throw; propagation must not orphan the debt.
|
|
150
|
+
deferCleanup();
|
|
151
|
+
if (!tryFinalizeSeeds()) {
|
|
152
|
+
// Failed durable cleanup remains owned by this ledger until later activity retries it.
|
|
153
|
+
deferCleanup();
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
const retireSeed = (key: string, seed: SpendSeed): boolean => {
|
|
157
|
+
if (!resolvedSendIds.has(seed.sendId) && !ledger().abandon(seed.sendId)) return false;
|
|
158
|
+
resolvedSendIds.add(seed.sendId);
|
|
159
|
+
seeds.delete(key);
|
|
160
|
+
const index = live.indexOf(seed.sendId);
|
|
161
|
+
if (index >= 0) live.splice(index, 1);
|
|
162
|
+
pendingDispatch.delete(seed.sendId);
|
|
163
|
+
// Keep cleanup owned even if finishSeed throws after durable abandonment.
|
|
164
|
+
retiredSeeds.add(seed);
|
|
165
|
+
deferCleanup();
|
|
166
|
+
if (ledger().finishSeed(seed, tracker)) retiredSeeds.delete(seed);
|
|
167
|
+
return true;
|
|
168
|
+
};
|
|
62
169
|
/**
|
|
63
170
|
* Confirm the sends this request has already moved past.
|
|
64
171
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* abandoned before the wire -- can still be released for free while this process is alive.
|
|
172
|
+
* Legacy direct charges infer dispatch from a later send. Exact budget reservations wait
|
|
173
|
+
* for their own dispatch/report instead: reserving B does not prove that A left, and A
|
|
174
|
+
* must remain refundable if B reports first. The newest direct charge stays open.
|
|
69
175
|
* A crash resolves every surviving reservation as unresolved spend regardless of this mark,
|
|
70
176
|
* because a journal that lost its tail cannot prove a send never left.
|
|
71
177
|
*/
|
|
72
178
|
const confirmOlderSends = (): void => {
|
|
73
|
-
for (
|
|
179
|
+
for (const sendId of live.slice(0, -1)) {
|
|
180
|
+
if (!pendingDispatch.has(sendId)) ledger().markDispatched(sendId);
|
|
181
|
+
}
|
|
74
182
|
};
|
|
75
|
-
|
|
76
|
-
|
|
183
|
+
const refusal = (denial: import("../../lib/spend-reservation-ledger").SpendDenial): void => {
|
|
184
|
+
refusals += 1;
|
|
185
|
+
// Preserve the PR's explicit unresolved-history refusal and its distinct operator code.
|
|
186
|
+
if (denial.reason === "pool-history-unresolved") {
|
|
187
|
+
const summary = workflowDenialSummary("workflow-pool-history-unresolved");
|
|
188
|
+
markLocalRequestLogRefusal(logCtx, summary.code);
|
|
189
|
+
logCtx.errorCode = summary.code;
|
|
190
|
+
recordWorkflowRefusalEvent(rootId, "workflow-pool-history-unresolved", Date.now());
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const reason: WorkflowDenial = denial.reason === "duplicate-send-id"
|
|
194
|
+
? "workflow-send-replayed"
|
|
195
|
+
: denial.reason === "reserve-not-durable" || denial.reason === "journal-corrupt"
|
|
196
|
+
? "workflow-spend-undurable"
|
|
197
|
+
: denial.reason === "tracking-capacity-exhausted"
|
|
198
|
+
? "workflow-tracking-exhausted"
|
|
199
|
+
: "workflow-spend-exhausted";
|
|
200
|
+
const detail = denial.reason === "spend-limit-exceeded"
|
|
201
|
+
? {
|
|
202
|
+
scope: denial.scope,
|
|
203
|
+
limit: denial.limit,
|
|
204
|
+
projected: denial.projected,
|
|
205
|
+
...(denial.includesUnboundPoolHistory ? { includesUnboundPoolHistory: true } : {}),
|
|
206
|
+
}
|
|
207
|
+
: undefined;
|
|
208
|
+
const summary = workflowDenialSummary(reason, detail);
|
|
209
|
+
markLocalRequestLogRefusal(logCtx, summary.code);
|
|
210
|
+
logCtx.errorCode = summary.code;
|
|
211
|
+
if (detail?.includesUnboundPoolHistory) logCtx.spendRefusalDetail = detail;
|
|
212
|
+
recordWorkflowRefusalEvent(rootId, reason, Date.now(), detail);
|
|
213
|
+
return;
|
|
214
|
+
};
|
|
215
|
+
const tracker: RequestSpendTracker = {
|
|
216
|
+
startRequest(target) {
|
|
217
|
+
if (admissionPolicy) return;
|
|
218
|
+
const policy = ledgerRef?.policy ?? sharedSpendPolicy();
|
|
219
|
+
admissionPolicy = Object.freeze({ root: Object.freeze({ ...policy.root }), identity: Object.freeze({ ...policy.identity }), pool: Object.freeze({ ...policy.pool }) });
|
|
220
|
+
initialEnforced = applicable(selectedScopes(target));
|
|
221
|
+
if (!initialEnforced) admissionPolicy = Object.freeze({ root: Object.freeze({}), identity: Object.freeze({}), pool: Object.freeze({}) });
|
|
222
|
+
},
|
|
223
|
+
get enforced(): boolean { return applicable(selectedScopes()); },
|
|
224
|
+
get policyStarted(): boolean { return admissionPolicy !== undefined; },
|
|
225
|
+
get spendAdmissionPolicy(): SpendAdmissionPolicy | undefined { return admissionPolicy; },
|
|
226
|
+
ensureSeed(target, prepaidProof) {
|
|
227
|
+
tracker.startRequest?.(target);
|
|
228
|
+
if (resolved || finalRequested && reporters === 0) return undefined;
|
|
229
|
+
const scopes = selectedScopes(target);
|
|
230
|
+
if (!applicable(scopes)) return undefined;
|
|
231
|
+
const key = JSON.stringify([scopes.rootId, scopes.identityId, scopes.poolId, target.targetKey]);
|
|
232
|
+
const existing = seeds.get(key);
|
|
233
|
+
if (existing) return existing.seed;
|
|
234
|
+
if (prepaidProof?.ledger === ledger()) {
|
|
235
|
+
const prepaid = [...seeds.entries()].find(([, state]) => state.seed.sendId === prepaidProof.sendId);
|
|
236
|
+
if (prepaid && !prepaid[1].started) {
|
|
237
|
+
// The proof was reserved against the selected scopes by this tracker.
|
|
238
|
+
const oldIdentity = JSON.parse(prepaid[0]) as Array<string | undefined>;
|
|
239
|
+
if (oldIdentity[0] === (scopes.rootId ?? null) && oldIdentity[1] === (scopes.identityId ?? null) && oldIdentity[2] === (scopes.poolId ?? null)) {
|
|
240
|
+
seeds.delete(prepaid[0]); seeds.set(key, prepaid[1]); return prepaid[1].seed;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (target.targetKey === undefined) {
|
|
245
|
+
const same = [...seeds.entries()].find(([candidate]) => {
|
|
246
|
+
const identity = JSON.parse(candidate) as Array<string | undefined>;
|
|
247
|
+
return identity[0] === (scopes.rootId ?? null) && identity[1] === (scopes.identityId ?? null) && identity[2] === (scopes.poolId ?? null);
|
|
248
|
+
});
|
|
249
|
+
if (same) return same[1].seed;
|
|
250
|
+
}
|
|
251
|
+
// Rebind an unused provisional anchor before selection changes. Dispatched anchors stay live.
|
|
252
|
+
for (const [oldKey, state] of seeds) if (!state.started) {
|
|
253
|
+
if (!retireSeed(oldKey, state.seed)) return undefined;
|
|
254
|
+
}
|
|
255
|
+
// Exact prepaid proofs originate from this tracker and are found above; foreign receipts
|
|
256
|
+
// cannot enroll arbitrary scopes. Normal reservation remains the only initial admission.
|
|
257
|
+
if (prepaidProof && prepaidProof.ledger !== ledger()) return undefined;
|
|
258
|
+
const decision = ledger().reserveSeed({ sendId: randomUUID(), scopes, ...estimates(), admissionPolicy }, tracker);
|
|
259
|
+
if (!decision.reserved) {
|
|
260
|
+
// Use the established denial mapping without making another reservation.
|
|
261
|
+
refusal(decision.denial);
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
seededAccounting = true;
|
|
265
|
+
seeds.set(key, { seed: decision.seed, started: false, reported: false });
|
|
266
|
+
return decision.seed;
|
|
267
|
+
},
|
|
268
|
+
beginReporter() {
|
|
269
|
+
if (resolved || finalRequested && reporters === 0) throw new Error("Spend reporter registered after final closure");
|
|
270
|
+
deferCleanup();
|
|
271
|
+
const lease = ledger().registerReporter(tracker);
|
|
272
|
+
reporters++;
|
|
273
|
+
let closed = false;
|
|
274
|
+
let reported = 0;
|
|
275
|
+
const starts: Array<{ seed: SpendSeed; ordinal: number }> = [];
|
|
276
|
+
return {
|
|
277
|
+
start(seed, ordinal) {
|
|
278
|
+
if (closed || !seedState(seed) || physical.has(ordinal)) throw new Error("Invalid spend reporter start");
|
|
279
|
+
const state = seedState(seed)!;
|
|
280
|
+
state.started = true;
|
|
281
|
+
starts.push({ seed, ordinal });
|
|
282
|
+
physical.set(ordinal, { seed });
|
|
283
|
+
|
|
284
|
+
},
|
|
285
|
+
rebindTarget(target, ordinal) {
|
|
286
|
+
const entry = physical.get(ordinal);
|
|
287
|
+
const owned = starts.find(start => start.ordinal === ordinal);
|
|
288
|
+
if (closed || !entry || !owned || entry.sendId) throw new Error("Invalid spend reporter rebind");
|
|
289
|
+
const old = seedState(entry.seed)!;
|
|
290
|
+
// A helper start is provisional until selection reaches the executor. If this is
|
|
291
|
+
// the first start on its seed, abandon/rebind at NORMAL capacity before the wire.
|
|
292
|
+
physical.delete(ordinal);
|
|
293
|
+
old.started = [...physical.values()].some(start => start.seed === old.seed);
|
|
294
|
+
const seed = tracker.ensureSeed(target);
|
|
295
|
+
if (!seed) { starts.splice(starts.indexOf(owned), 1); return false; }
|
|
296
|
+
physical.set(ordinal, { seed });
|
|
297
|
+
owned.seed = seed;
|
|
298
|
+
seedState(seed)!.started = true;
|
|
299
|
+
return true;
|
|
300
|
+
},
|
|
301
|
+
report(sends) {
|
|
302
|
+
if (closed) return;
|
|
303
|
+
const count = Number.isFinite(sends) ? Math.max(0, Math.trunc(sends)) : 0;
|
|
304
|
+
// Own the whole reported batch before the first append can interrupt its cursor.
|
|
305
|
+
for (const start of starts.slice(reported, reported + count)) physical.get(start.ordinal)!.reportPending = true;
|
|
306
|
+
for (let i = 0; i < count && reported < starts.length; i++) {
|
|
307
|
+
const entry = starts[reported]!;
|
|
308
|
+
tracker.reportFromSeed(entry.seed, entry.ordinal);
|
|
309
|
+
reported++;
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
close() {
|
|
313
|
+
if (closed) return;
|
|
314
|
+
// Cancellation still books every claimed start conservatively.
|
|
315
|
+
try { this.report(starts.length - reported); }
|
|
316
|
+
finally {
|
|
317
|
+
closed = true;
|
|
318
|
+
reporters--;
|
|
319
|
+
try { lease.close(); } finally { finalizeSeeds(); }
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
},
|
|
324
|
+
reportFromSeed(seed, ordinal) {
|
|
325
|
+
const entry = physical.get(ordinal);
|
|
326
|
+
const state = seedState(seed);
|
|
327
|
+
if (!entry || entry.seed !== seed || entry.reportComplete || !state) return;
|
|
328
|
+
entry.reportPending = true;
|
|
329
|
+
const firstOrdinal = [...physical.entries()].filter(([, send]) => send.seed === seed).map(([order]) => order).sort((a, b) => a - b)[0];
|
|
330
|
+
const sendId = entry.sendId ??= ordinal === firstOrdinal ? seed.sendId : randomUUID();
|
|
331
|
+
const estimate = entry.estimate ??= estimates();
|
|
332
|
+
if (sendId !== seed.sendId && !ledger().reserveReportedFromSeed(seed, { sendId, ...estimate })) {
|
|
333
|
+
throw new Error("Already-sent spend could not be persisted");
|
|
334
|
+
}
|
|
335
|
+
state.reported = true;
|
|
336
|
+
if (!ledger().markDispatched(sendId)) throw new Error("Already-sent spend dispatch could not be persisted");
|
|
337
|
+
entry.reportComplete = true;
|
|
338
|
+
},
|
|
339
|
+
requestFinalSettlement(usage) {
|
|
340
|
+
if (!finalRequested) { finalRequested = true; finalUsage = usage; }
|
|
341
|
+
finalizeSeeds();
|
|
342
|
+
},
|
|
343
|
+
charge(options?: Parameters<RequestSendObserver["charge"]>[0]): boolean {
|
|
344
|
+
tracker.startRequest?.();
|
|
77
345
|
// A send that has already left is RECORDED, never refused: the tokens are spent, and a
|
|
78
346
|
// booking the ledger drops is a booking the ceiling can never see. This is the reporting
|
|
79
347
|
// transports' path -- the passthrough ladder reports through `onSendsConsumed` after the
|
|
80
348
|
// fetch -- so without it a root ceiling on the canonical Codex path would sit one send
|
|
81
349
|
// short of its limit forever and refuse nothing.
|
|
82
350
|
const alreadySent = options?.alreadySent === true;
|
|
351
|
+
if (!alreadySent && tracker.enforced) {
|
|
352
|
+
const seed = tracker.ensureSeed({ targetKey: options?.targetKey });
|
|
353
|
+
if (!seed) return false;
|
|
354
|
+
options?.onReserved?.({ ledger: ledger(), sendId: seed.sendId });
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
83
357
|
const sendId = randomUUID();
|
|
84
358
|
const bookedLedger = ledger();
|
|
85
359
|
const scopes: SpendScopes = {
|
|
86
360
|
...(rootId !== undefined ? { rootId } : {}),
|
|
87
|
-
//
|
|
361
|
+
// Already the privacy-safe label the request log uses, and the ledger aliases it
|
|
362
|
+
// again on the way to disk. A raw credential never reaches either.
|
|
88
363
|
...(logCtx.accountLogLabel !== undefined ? { identityId: logCtx.accountLogLabel } : {}),
|
|
89
|
-
...(logCtx.
|
|
364
|
+
...((logCtx.spendPoolId ?? logCtx.provider) !== undefined
|
|
365
|
+
? { poolId: logCtx.spendPoolId ?? logCtx.provider }
|
|
366
|
+
: {}),
|
|
90
367
|
};
|
|
91
|
-
const policy =
|
|
368
|
+
const policy = admissionPolicy!;
|
|
92
369
|
const enforced = (scopes.rootId !== undefined && policy.root.maxTokens !== undefined)
|
|
93
370
|
|| (scopes.identityId !== undefined && policy.identity.maxTokens !== undefined)
|
|
94
371
|
|| (scopes.poolId !== undefined && policy.pool.maxTokens !== undefined);
|
|
95
372
|
const decision = bookedLedger.reserve({
|
|
96
373
|
sendId,
|
|
97
374
|
scopes,
|
|
375
|
+
admissionPolicy,
|
|
98
376
|
inputTokens: logCtx.spendInputEstimateTokens ?? logCtx.usageLogInputTokens ?? 0,
|
|
99
377
|
outputCeilingTokens: logCtx.spendOutputCeilingTokens ?? 0,
|
|
100
378
|
...(alreadySent ? { alreadySent: true } : {}),
|
|
@@ -104,39 +382,47 @@ export function createRequestSpendTracker(
|
|
|
104
382
|
// capacity is full. Unconfigured/nonapplicable requests remain observe-only, and a
|
|
105
383
|
// physical send reported after dispatch cannot be refused retroactively.
|
|
106
384
|
if (alreadySent || !enforced) return true;
|
|
107
|
-
|
|
108
|
-
const denial = decision.denial;
|
|
109
|
-
const reason: WorkflowDenial = denial.reason === "duplicate-send-id"
|
|
110
|
-
? "workflow-send-replayed"
|
|
111
|
-
: denial.reason === "reserve-not-durable" || denial.reason === "journal-corrupt"
|
|
112
|
-
? "workflow-spend-undurable"
|
|
113
|
-
: denial.reason === "tracking-capacity-exhausted"
|
|
114
|
-
? "workflow-tracking-exhausted"
|
|
115
|
-
: "workflow-spend-exhausted";
|
|
116
|
-
const detail = denial.reason === "spend-limit-exceeded"
|
|
117
|
-
? { scope: denial.scope, limit: denial.limit, projected: denial.projected } : undefined;
|
|
118
|
-
const summary = workflowDenialSummary(reason, detail);
|
|
119
|
-
markLocalRequestLogRefusal(logCtx, summary.code);
|
|
120
|
-
logCtx.errorCode = summary.code;
|
|
121
|
-
recordWorkflowRefusalEvent(rootId, reason, Date.now(), detail);
|
|
385
|
+
refusal(decision.denial);
|
|
122
386
|
return false;
|
|
123
387
|
}
|
|
388
|
+
if (!alreadySent) options?.onReserved?.({ ledger: bookedLedger, sendId });
|
|
124
389
|
live.push(sendId);
|
|
125
|
-
|
|
390
|
+
if (options?.deferDispatch) pendingDispatch.add(sendId);
|
|
391
|
+
else confirmOlderSends();
|
|
126
392
|
// It has already left, so the reservation cannot be handed back for free: from here only
|
|
127
393
|
// a settlement or unresolved spend is honest about it.
|
|
128
394
|
if (alreadySent) ledger().markDispatched(sendId);
|
|
129
395
|
return true;
|
|
130
396
|
},
|
|
131
|
-
|
|
132
|
-
|
|
397
|
+
dispatch(proof): void {
|
|
398
|
+
if (seededAccounting && proof.ledger === ledgerRef) return;
|
|
399
|
+
if (proof.ledger !== ledgerRef || !pendingDispatch.has(proof.sendId)) return;
|
|
400
|
+
ledger().markDispatched(proof.sendId);
|
|
401
|
+
pendingDispatch.delete(proof.sendId);
|
|
402
|
+
// Terminal usage follows dispatch/report order, not reservation order.
|
|
403
|
+
const index = live.indexOf(proof.sendId);
|
|
404
|
+
if (index >= 0) live.push(...live.splice(index, 1));
|
|
405
|
+
},
|
|
406
|
+
refund(proof): void {
|
|
407
|
+
if (proof && proof.ledger === ledgerRef && seededAccounting) {
|
|
408
|
+
const match = [...seeds.entries()].find(([, state]) => state.seed.sendId === proof.sendId);
|
|
409
|
+
if (match && !match[1].started) retireSeed(match[0], match[1].seed);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
// null is an exact budget reservation that obtained no durable booking.
|
|
413
|
+
if (proof === null || (proof && proof.ledger !== ledgerRef)) return;
|
|
414
|
+
const index = proof ? live.indexOf(proof.sendId) : live.length - 1;
|
|
415
|
+
if (index < 0) return;
|
|
416
|
+
const [sendId] = live.splice(index, 1);
|
|
133
417
|
if (sendId === undefined) return;
|
|
418
|
+
pendingDispatch.delete(sendId);
|
|
134
419
|
// Undispatched, so this returns the tokens. If the send was already confirmed by a later
|
|
135
420
|
// one, `abandon` refuses and unresolved is the only honest outcome left.
|
|
136
421
|
if (!ledger().abandon(sendId)) ledger().markLost(sendId);
|
|
137
422
|
},
|
|
138
423
|
settle(usage: TerminalSpendUsage | undefined): void {
|
|
139
424
|
if (resolved) return;
|
|
425
|
+
if (seededAccounting) { tracker.requestFinalSettlement(usage); return; }
|
|
140
426
|
try {
|
|
141
427
|
if (!terminalProcessed && live.length > 0) {
|
|
142
428
|
const terminal = live[live.length - 1] as string;
|
|
@@ -169,6 +455,7 @@ export function createRequestSpendTracker(
|
|
|
169
455
|
},
|
|
170
456
|
get refusals(): number { return refusals; },
|
|
171
457
|
};
|
|
458
|
+
return tracker;
|
|
172
459
|
}
|
|
173
460
|
|
|
174
461
|
/**
|
|
@@ -178,15 +465,20 @@ export function createRequestSpendTracker(
|
|
|
178
465
|
* request passes exactly once, whatever transport served it and however it ended, and it is
|
|
179
466
|
* where the terminal usage is already known.
|
|
180
467
|
*/
|
|
468
|
+
const trackerRequests = new WeakMap<RequestSpendTracker, Pick<Request, "headers">>();
|
|
469
|
+
|
|
181
470
|
export function attachRequestSpendTracker(
|
|
182
471
|
req: Pick<Request, "headers">,
|
|
183
472
|
logCtx: RequestLogContext,
|
|
184
473
|
ledger?: SpendReservationLedger,
|
|
185
|
-
):
|
|
474
|
+
): RequestSpendTracker {
|
|
475
|
+
const existing = logCtx.spendTracker as RequestSpendTracker | undefined;
|
|
476
|
+
if (existing && trackerRequests.get(existing) === req) return existing;
|
|
186
477
|
const rootId = req.headers.get("x-codex-parent-thread-id")?.trim() || undefined;
|
|
187
478
|
const tracker = ledger === undefined
|
|
188
479
|
? createRequestSpendTracker(logCtx, rootId)
|
|
189
480
|
: createRequestSpendTracker(logCtx, rootId, ledger);
|
|
190
481
|
logCtx.spendTracker = tracker;
|
|
482
|
+
trackerRequests.set(tracker, req);
|
|
191
483
|
return tracker;
|
|
192
484
|
}
|