@bitkyc08/opencodex 2.82.0 → 2.84.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/App-CZtqiGcw.js +56 -0
- package/gui/dist/assets/Tray-C9TGtwad.js +1 -0
- package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
- package/gui/dist/assets/index-CTm4l-HQ.js +94 -0
- package/gui/dist/assets/{quota-summary-DTBC8n7-.js → quota-summary-BbEeRfPY.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +2 -2
- package/src/adapters/coding-agent/protocol.ts +15 -8
- package/src/adapters/cursor/catalog.ts +2 -2
- package/src/adapters/cursor/effort-map.ts +1 -1
- package/src/adapters/devin/live-models.ts +2 -2
- package/src/adapters/google-truncation.ts +22 -0
- package/src/adapters/google.ts +7 -3
- package/src/adapters/ollama-native.ts +47 -15
- package/src/adapters/openai-chat/messages.ts +8 -4
- package/src/adapters/openai-responses/empty-assistant.ts +32 -0
- package/src/adapters/openai-responses/passthrough.ts +5 -0
- package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
- package/src/adapters/openai-responses/reasoning.ts +11 -1
- package/src/adapters/openai-responses/request-strips.ts +10 -2
- package/src/adapters/qoder/adapter.ts +16 -14
- package/src/bridge/errors.ts +26 -1
- package/src/bridge.ts +1 -1
- package/src/claude/context-windows.ts +57 -4
- package/src/claude/desktop-picker.ts +1 -1
- package/src/claude/intercept/connect-proxy.ts +10 -3
- package/src/claude/intercept/picker-ca-persistence.ts +3 -1
- package/src/claude/intercept/picker-listener.ts +49 -1
- package/src/claude/intercept/settings-no-proxy.ts +146 -0
- package/src/claude/intercept/settings.ts +101 -9
- package/src/cli/account-api.ts +65 -0
- package/src/cli/account-auth.ts +44 -12
- package/src/cli/account.ts +41 -5
- package/src/cli/capabilities-access-remote.ts +8 -0
- package/src/cli/capabilities-base.ts +13 -1
- package/src/cli/catalog-command-result.ts +2 -1
- package/src/cli/claude.ts +15 -6
- package/src/cli/config-command.ts +8 -2
- package/src/cli/debug.ts +8 -0
- package/src/cli/dispatch.ts +16 -3
- package/src/cli/export-command.ts +14 -1
- package/src/cli/hub-disable.ts +99 -0
- package/src/cli/hub.ts +4 -2
- package/src/cli/index.ts +13 -4
- package/src/cli/link-child.ts +104 -0
- package/src/cli/link-enrollment.ts +96 -0
- package/src/cli/link.ts +39 -24
- package/src/cli/observe-stream.ts +6 -1
- package/src/cli/observe.ts +10 -2
- package/src/cli/provider.ts +12 -0
- package/src/cli/registry.ts +5 -4
- package/src/cli/resolve.ts +1 -1
- package/src/cli/runtime-api.ts +27 -2
- package/src/cli/status.ts +104 -20
- package/src/client/connect.ts +39 -4
- package/src/client/hub-client.ts +14 -7
- package/src/client/hub-state.ts +25 -14
- package/src/client/link-home.ts +248 -0
- package/src/client/link-management-relay.ts +61 -0
- package/src/client/link-relay.ts +6 -1
- package/src/client/link-status.ts +14 -0
- package/src/client/machine-api.ts +14 -7
- package/src/client/machine-listener.ts +29 -4
- package/src/clients/config-export/model-metadata.ts +16 -0
- package/src/clients/config-export/zcode-store.ts +46 -11
- package/src/clients/config-export/zcode.ts +3 -5
- package/src/clients/config-export.ts +47 -17
- package/src/clients/omo-role-models.ts +6 -3
- package/src/codex/agent-role-models.ts +26 -8
- package/src/codex/auth-api/account-list.ts +17 -2
- package/src/codex/auth-api/account-selection.ts +17 -7
- package/src/codex/auth-api/pool-mode-gate.ts +4 -5
- package/src/codex/auth-api/routes.ts +5 -1
- package/src/codex/auth-context.ts +8 -3
- package/src/codex/autostart-health.ts +3 -1
- package/src/codex/catalog/bundled.ts +5 -1
- package/src/codex/catalog/effort.ts +2 -2
- package/src/codex/catalog/metadata.ts +4 -2
- package/src/codex/catalog/native-models.ts +17 -3
- package/src/codex/catalog/provider-models.ts +25 -0
- package/src/codex/desktop-app/darwin.ts +1 -1
- package/src/codex/desktop-app/handoff.ts +1 -1
- package/src/codex/desktop-app/linux.ts +1 -1
- package/src/codex/desktop-app/lock.ts +1 -1
- package/src/codex/desktop-app/types.ts +1 -1
- package/src/codex/desktop-app/windows.ts +1 -1
- package/src/codex/desktop-app-restart.ts +1 -1
- package/src/codex/main-account.ts +6 -7
- package/src/codex/native-profile-startup.ts +4 -1
- package/src/codex/prompt-journal.ts +20 -2
- package/src/codex/prompt-layers/encoding.ts +49 -0
- package/src/codex/prompt-layers/import-source.ts +30 -0
- package/src/codex/prompt-layers/toml-edit.ts +269 -37
- package/src/codex/prompt-layers/toml-read.ts +52 -4
- package/src/codex/prompt-layers.ts +383 -20
- package/src/codex/prompt-lock-claim.ts +13 -3
- package/src/codex/prompt-lock-evidence.ts +3 -3
- package/src/codex/prompt-lock-owner.ts +14 -6
- package/src/codex/prompt-text-probe.ts +34 -5
- package/src/codex/quota.ts +13 -6
- package/src/codex/role-auto-assign.ts +2 -5
- package/src/codex/routing/active-account.ts +21 -0
- package/src/codex/routing.ts +1 -1
- package/src/codex/runtime.ts +196 -8
- package/src/codex/shim-overlay.ts +8 -4
- package/src/codex/shim.ts +1 -1
- package/src/combos/failover.ts +3 -0
- package/src/config/api-key-reconcile.ts +34 -0
- package/src/config/diagnostics.ts +16 -0
- package/src/config/feature-flags.ts +5 -0
- package/src/config/live-reconcile.ts +43 -29
- package/src/config/live-topology.ts +29 -0
- package/src/config/load-degrade.ts +2 -1
- package/src/config/paths.ts +3 -0
- package/src/config/salvage.ts +3 -1
- package/src/config/schema/config-schema.ts +2 -0
- package/src/generated/compatibility-version.json +373 -217
- package/src/grok/grpc-web.ts +23 -1
- package/src/grok/inject.ts +245 -221
- package/src/grok/reset-coupon-ledger.ts +293 -57
- package/src/grok/reset-coupons.ts +15 -6
- package/src/integrations/cursor-detect.ts +1 -1
- package/src/integrations/mutation-plan.ts +13 -7
- package/src/integrations/registry.ts +45 -11
- package/src/integrations/state.ts +19 -7
- package/src/integrations/target.ts +42 -5
- package/src/integrations/writer.ts +14 -6
- package/src/lib/app-owned-memory-stores.ts +4 -0
- package/src/lib/app-owned-memory.ts +11 -0
- package/src/lib/errors.ts +25 -0
- package/src/lib/json-byte-size.ts +31 -0
- package/src/lib/optional-shutdown-hooks.ts +35 -0
- package/src/lib/outbound-credential-registry.ts +210 -0
- package/src/lib/passive-read.ts +35 -0
- package/src/lib/redact.ts +2 -2
- package/src/lib/shadow-call.ts +44 -5
- package/src/link/codex-settings.ts +155 -0
- package/src/link/management-routes.ts +89 -0
- package/src/link/remote-probe.ts +38 -0
- package/src/link/ssh-argv.ts +26 -5
- package/src/link/ssh-runner.ts +58 -27
- package/src/link/status-projection.ts +22 -4
- package/src/link/supervisor.ts +144 -49
- package/src/link/tunnel-state.ts +11 -4
- package/src/oauth/anthropic-continuity.ts +4 -3
- package/src/oauth/anthropic-identity.ts +13 -2
- package/src/oauth/anthropic-model-quota.ts +13 -1
- package/src/oauth/anthropic-routing.ts +230 -23
- package/src/oauth/anthropic.ts +18 -4
- package/src/oauth/google-antigravity.ts +24 -7
- package/src/oauth/index.ts +25 -16
- package/src/oauth/kiro-account-identity.ts +106 -0
- package/src/oauth/kiro.ts +48 -5
- package/src/oauth/meta-muse-device.ts +1 -1
- package/src/oauth/plan.ts +29 -0
- package/src/oauth/store.ts +40 -3
- package/src/oauth/types.ts +3 -1
- package/src/plugins/loader.ts +11 -0
- package/src/providers/anthropic-billing-ownership.ts +74 -0
- package/src/providers/anthropic-billing-recovery.ts +31 -0
- package/src/providers/anthropic-billing-refusal.ts +55 -0
- package/src/providers/anthropic-fast.ts +1 -1
- package/src/providers/anthropic-reset-admission.ts +62 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +29 -7
- package/src/providers/api-key-resolve.ts +17 -2
- package/src/providers/devin-cli-authmode-migration.ts +1 -1
- package/src/providers/devin-provider-merge-migration.ts +1 -1
- package/src/providers/github-copilot-auto.ts +169 -0
- package/src/providers/key-failover.ts +52 -5
- package/src/providers/model-presets.ts +1 -1
- package/src/providers/model-rename-startup.ts +5 -3
- package/src/providers/muse-subscription-usage.ts +1 -1
- package/src/providers/quota/account-cache.ts +11 -0
- package/src/providers/quota/anthropic-account-quota.ts +4 -2
- package/src/providers/quota/report-cache.ts +14 -0
- package/src/providers/quota/vendor-probes-oauth.ts +3 -3
- package/src/providers/quota.ts +4 -1
- package/src/providers/registry/entries-core.ts +20 -20
- package/src/providers/registry/entries-extended.ts +4 -1
- package/src/providers/registry/model-seeds.ts +20 -3
- package/src/providers/request-pacing.ts +9 -0
- package/src/providers/stale-no-vision-seed-migration.ts +72 -0
- package/src/providers/xai-fast-model.ts +1 -1
- package/src/responses/compaction.ts +14 -0
- package/src/responses/parser.ts +7 -1
- package/src/server/auth-cors.ts +42 -0
- package/src/server/configured-credentials.ts +194 -0
- package/src/server/inbound-body-admission.ts +24 -7
- package/src/server/index/link-listener.ts +30 -17
- package/src/server/index/link-management.ts +58 -0
- package/src/server/index/optional-listeners.ts +5 -1
- package/src/server/index/serve-options.ts +21 -2
- package/src/server/index.ts +1 -1
- package/src/server/lifecycle.ts +33 -5
- package/src/server/management/agent-settings-routes.ts +28 -10
- package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
- package/src/server/management/codex-agent-role-routes.ts +19 -6
- package/src/server/management/codex-prompt-routes.ts +136 -5
- package/src/server/management/codex-role-auto-assign.ts +1 -1
- package/src/server/management/grok-coupon-routes.ts +333 -57
- package/src/server/management/link-enrollment.ts +192 -0
- package/src/server/management/link-route-state.ts +185 -0
- package/src/server/management/link-routes.ts +92 -390
- package/src/server/management/model-rows.ts +3 -1
- package/src/server/management/provider-overwrite-carry.ts +10 -0
- package/src/server/management/provider-routes.ts +1 -0
- package/src/server/management/route-registry.ts +7 -5
- package/src/server/management/shared.ts +3 -0
- package/src/server/management-api.ts +3 -1
- package/src/server/messages-native.ts +68 -42
- package/src/server/relay-eager.ts +4 -0
- package/src/server/relay.ts +18 -7
- package/src/server/request-log-failure-attribution.ts +3 -0
- package/src/server/request-log-filter.ts +6 -1
- package/src/server/request-log-terminal-status.ts +243 -0
- package/src/server/request-log.ts +15 -118
- package/src/server/responses/adapter-continuation.ts +9 -1
- package/src/server/responses/adapter-dispatch.ts +293 -81
- package/src/server/responses/code-mode-collab-surface.ts +205 -0
- package/src/server/responses/codex-ws-exchange.ts +147 -6
- package/src/server/responses/codex-ws-wire.ts +203 -0
- package/src/server/responses/collaboration.ts +36 -7
- package/src/server/responses/compact.ts +129 -50
- package/src/server/responses/context-overflow.ts +13 -1
- package/src/server/responses/core-codex-account.ts +105 -1
- package/src/server/responses/core-combo-failure.ts +14 -0
- package/src/server/responses/core-combo.ts +7 -2
- package/src/server/responses/core-errors.ts +29 -1
- package/src/server/responses/core-opaque-recovery.ts +5 -5
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/core.ts +6 -6
- package/src/server/responses/deferred-collab-surface.ts +158 -0
- package/src/server/responses/deferred-send-reporter.ts +25 -0
- package/src/server/responses/empty-completion-guard.ts +18 -2
- package/src/server/responses/fetch-helpers.ts +12 -1
- package/src/server/responses/jev-model-invoke.ts +1 -1
- package/src/server/responses/passthrough-delivery.ts +4 -1
- package/src/server/responses/passthrough-dispatch.ts +130 -64
- package/src/server/responses/passthrough-execution.ts +3 -3
- package/src/server/responses/request-prepare.ts +6 -4
- package/src/server/responses/request-transport.ts +131 -13
- package/src/server/responses/run-turn-execution.ts +13 -5
- package/src/server/responses/sidecar-execution.ts +16 -2
- package/src/server/responses/spend-storage-error.ts +33 -0
- package/src/server/responses/ws-upstream.ts +9 -1
- package/src/server/startup-action-control.ts +22 -2
- package/src/server/startup-health-cache.ts +4 -0
- package/src/server/system-env-shell.ts +7 -2
- package/src/server/system-env.ts +11 -1
- package/src/service/cli.ts +5 -1
- package/src/service/desktop-startup.ts +147 -34
- package/src/service/desktop-supervision.d.mts +27 -1
- package/src/service/desktop-supervision.mjs +111 -5
- package/src/service/health-sentinel-launch.ts +176 -0
- package/src/service/health-sentinel-main.ts +447 -0
- package/src/service/health-sentinel-policy.ts +241 -0
- package/src/service/health-sentinel-probe.ts +54 -0
- package/src/service/health-sentinel-windows.ts +87 -0
- package/src/service/linux-linger.ts +20 -0
- package/src/types/config.ts +5 -0
- package/src/types/request.ts +5 -0
- package/src/usage/expected-prices.ts +5 -1
- package/src/usage/log.ts +19 -1
- package/gui/dist/assets/App-Ds_qL7o8.js +0 -54
- package/gui/dist/assets/Tray-BqHF5Trd.js +0 -1
- package/gui/dist/assets/index-D3FBCNiP.js +0 -94
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { jsonResponse } from "../auth-cors";
|
|
13
13
|
import type { ManagementContext } from "./context";
|
|
14
|
+
import { ConfigMutationLockError } from "../../config/mutation-lock";
|
|
14
15
|
import { isCodexResetCreditOperationId } from "../../codex/reset-credit-recovery";
|
|
15
16
|
import { getValidAccessSnapshotForAccount } from "../../oauth";
|
|
16
17
|
import { listAccounts, captureOAuthAccountSelection } from "../../oauth/store";
|
|
@@ -20,8 +21,14 @@ import {
|
|
|
20
21
|
type GrokResetCoupon,
|
|
21
22
|
} from "../../grok/reset-coupons";
|
|
22
23
|
import {
|
|
24
|
+
getGrokResetCouponOperationAccountId,
|
|
25
|
+
isTerminalGrokCouponReplay,
|
|
26
|
+
markGrokResetCouponAttempt,
|
|
23
27
|
openGrokResetCouponOperation,
|
|
24
28
|
recordGrokResetCouponSettlement,
|
|
29
|
+
readGrokResetCouponTerminalReplay,
|
|
30
|
+
refuseGrokResetCouponBeforeOpen,
|
|
31
|
+
settleGrokResetCouponPreflightRefusal,
|
|
25
32
|
type GrokResetCouponOperationRecord,
|
|
26
33
|
} from "../../grok/reset-coupon-ledger";
|
|
27
34
|
|
|
@@ -41,6 +48,11 @@ export interface GrokConsumeCouponRequestBody {
|
|
|
41
48
|
operationId?: string;
|
|
42
49
|
}
|
|
43
50
|
|
|
51
|
+
// A recent attempt can still be in flight. Older attempts may be inspected,
|
|
52
|
+
// but a remaining token is not proof that the original request cannot land.
|
|
53
|
+
const GROK_COUPON_ATTEMPT_STALE_MS = 90_000;
|
|
54
|
+
const grokCouponInFlightAttempts = new Set<string>();
|
|
55
|
+
|
|
44
56
|
function resolveTargetAccountId(requestedAccountId?: string): string {
|
|
45
57
|
if (requestedAccountId && requestedAccountId.trim() !== "") {
|
|
46
58
|
return requestedAccountId.trim();
|
|
@@ -139,52 +151,188 @@ export async function handleGrokCouponRoutes(ctx: ManagementContext): Promise<Re
|
|
|
139
151
|
);
|
|
140
152
|
}
|
|
141
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Once a caller supplies an operationId, that id may already name an operation that
|
|
156
|
+
* another request is spending. A refusal from this request must not read as
|
|
157
|
+
* definitive then, or the caller drops its hold and mints a replacement.
|
|
158
|
+
*/
|
|
159
|
+
const uncertain = (id: string): Response => jsonResponse({ operationId: id, error: {
|
|
160
|
+
code: "attempt_in_progress",
|
|
161
|
+
message: "This request did not attempt a redemption, but another request may hold this operation; retry the same operationId to read its durable state",
|
|
162
|
+
} }, 409, req, config);
|
|
163
|
+
/** Refuse before open: definitive only when the supplied id is durably reserved as failed. */
|
|
164
|
+
const preOpenRefusal = (definitive: Response, code: string, knownAccountId?: string): Response => {
|
|
165
|
+
if (operationId === undefined) return definitive;
|
|
166
|
+
if (knownAccountId === undefined) return uncertain(operationId);
|
|
167
|
+
try {
|
|
168
|
+
const result = refuseGrokResetCouponBeforeOpen({
|
|
169
|
+
operationId, accountId: knownAccountId, code,
|
|
170
|
+
tokenId: typeof requestedTokenId === "string" ? requestedTokenId : undefined,
|
|
171
|
+
});
|
|
172
|
+
if (result.kind === "recorded") return definitive;
|
|
173
|
+
if (result.kind === "replay") return jsonResponse({
|
|
174
|
+
code: result.code, replayed: true, tokenId: result.tokenId, settledAt: result.settledAt,
|
|
175
|
+
}, 200, req, config);
|
|
176
|
+
if (result.kind === "identity-mismatch") return jsonResponse({ error: {
|
|
177
|
+
code: "operation_id_owned_by_another_account",
|
|
178
|
+
message: "Operation ID was previously registered with a different account or token",
|
|
179
|
+
} }, 409, req, config);
|
|
180
|
+
return uncertain(operationId);
|
|
181
|
+
} catch {
|
|
182
|
+
return uncertain(operationId);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
if (requestedTokenId !== undefined && (typeof requestedTokenId !== "string" || requestedTokenId.trim() === "")) {
|
|
187
|
+
return preOpenRefusal(jsonResponse({ error: { code: "invalid_token_id", message: "tokenId must be a non-empty string" } }, 400, req, config), "invalid_token_id");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let recordedAccountId: string | undefined;
|
|
191
|
+
if (rawAccountId === undefined && operationId !== undefined) {
|
|
192
|
+
try { recordedAccountId = getGrokResetCouponOperationAccountId(operationId); }
|
|
193
|
+
catch {
|
|
194
|
+
return uncertain(operationId);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
142
197
|
let accountId: string;
|
|
143
198
|
try {
|
|
144
|
-
accountId = resolveTargetAccountId(rawAccountId);
|
|
199
|
+
accountId = resolveTargetAccountId(rawAccountId ?? recordedAccountId);
|
|
145
200
|
} catch (err) {
|
|
146
|
-
return jsonResponse(
|
|
201
|
+
return preOpenRefusal(jsonResponse(
|
|
147
202
|
{ error: { code: "no_account", message: err instanceof Error ? err.message : String(err) } },
|
|
148
203
|
400,
|
|
149
204
|
req,
|
|
150
205
|
config,
|
|
151
|
-
);
|
|
206
|
+
), "no_account");
|
|
152
207
|
}
|
|
153
208
|
|
|
154
209
|
let tokenSnapshot;
|
|
155
210
|
try {
|
|
156
211
|
tokenSnapshot = await getValidAccessSnapshotForAccount("xai", accountId, { requireUsableAccount: true });
|
|
157
212
|
} catch (err) {
|
|
158
|
-
return jsonResponse(
|
|
213
|
+
return preOpenRefusal(jsonResponse(
|
|
159
214
|
{ error: { code: "auth_failed", message: "Failed to resolve valid xAI credentials for account" } },
|
|
160
215
|
401,
|
|
161
216
|
req,
|
|
162
217
|
config,
|
|
163
|
-
);
|
|
218
|
+
), "auth_failed", accountId);
|
|
164
219
|
}
|
|
165
220
|
|
|
166
221
|
// Journaling and Idempotency settlement check
|
|
167
222
|
const effectiveOpId = operationId ?? crypto.randomUUID();
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
223
|
+
if (grokCouponInFlightAttempts.has(effectiveOpId)) return jsonResponse({ operationId: effectiveOpId, error: {
|
|
224
|
+
code: "attempt_in_progress", message: "This operation still has an active request; retry later",
|
|
225
|
+
} }, 409, req, config);
|
|
226
|
+
let opRecord: GrokResetCouponOperationRecord;
|
|
227
|
+
try {
|
|
228
|
+
opRecord = openGrokResetCouponOperation({ accountId, tokenId: requestedTokenId, operationId: effectiveOpId });
|
|
229
|
+
} catch {
|
|
230
|
+
// A failed open cannot prove that no earlier request already opened this id.
|
|
231
|
+
return jsonResponse({ operationId: effectiveOpId, error: {
|
|
232
|
+
code: "ledger_unavailable",
|
|
233
|
+
message: "Coupon ledger could not be read or locked; this request did not attempt a redemption. Retry the same operationId",
|
|
234
|
+
} }, 503, req, config);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let resolvedTokenId = requestedTokenId ?? opRecord.tokenId;
|
|
238
|
+
let resolvedTokenValidityEnd: number | undefined;
|
|
173
239
|
|
|
174
240
|
if (opRecord.kind === "replay") {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
241
|
+
if (opRecord.code !== undefined) {
|
|
242
|
+
// Replay only a definitive outcome; a recorded code that means "unconfirmed"
|
|
243
|
+
// must keep the caller on this operation.
|
|
244
|
+
if (!isTerminalGrokCouponReplay(opRecord, accountId, requestedTokenId)) {
|
|
245
|
+
return jsonResponse({ operationId: effectiveOpId, error: {
|
|
246
|
+
code: "attempt_unresolved",
|
|
247
|
+
message: "This operation's recorded outcome is unconfirmed; preserve this operationId and do not create a replacement attempt",
|
|
248
|
+
} }, 409, req, config);
|
|
249
|
+
}
|
|
250
|
+
return jsonResponse(
|
|
251
|
+
{
|
|
252
|
+
code: opRecord.code,
|
|
253
|
+
replayed: true,
|
|
254
|
+
tokenId: opRecord.tokenId,
|
|
255
|
+
settledAt: opRecord.settledAt,
|
|
256
|
+
},
|
|
257
|
+
200,
|
|
258
|
+
req,
|
|
259
|
+
config,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
if (opRecord.tokenId === undefined) {
|
|
263
|
+
// Legacy opens may already have dispatched without recording a token.
|
|
264
|
+
return jsonResponse({ operationId: effectiveOpId, error: {
|
|
265
|
+
code: "attempt_unresolved", message: "The prior operation has no recorded token; preserve this operationId and do not create a replacement attempt",
|
|
266
|
+
} }, 409, req, config);
|
|
267
|
+
}
|
|
268
|
+
// "attempted" with no recorded outcome: the spend call fired (or the
|
|
269
|
+
// process died right after the mark) but nothing was settled. Never
|
|
270
|
+
// replay this as a success — reconcile against upstream instead. A
|
|
271
|
+
// Remaining-token availability cannot identify the original request's
|
|
272
|
+
// outcome, and a listed token cannot rule out delayed completion.
|
|
273
|
+
if (requestedTokenId !== undefined && requestedTokenId !== opRecord.tokenId) {
|
|
274
|
+
// The retry names a different coupon than the one marked: spending
|
|
275
|
+
// either of them would surprise the caller — refuse and let them
|
|
276
|
+
// retry with the recorded token and preserve the operationId.
|
|
277
|
+
return jsonResponse(
|
|
278
|
+
{
|
|
279
|
+
operationId: effectiveOpId,
|
|
280
|
+
error: {
|
|
281
|
+
code: "operation_token_mismatch",
|
|
282
|
+
message: "Operation was attempted with a different coupon; preserve the operationId and retry with the recorded tokenId",
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
409,
|
|
286
|
+
req,
|
|
287
|
+
config,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
if (
|
|
291
|
+
opRecord.attemptedAt === undefined ||
|
|
292
|
+
Date.now() - opRecord.attemptedAt < GROK_COUPON_ATTEMPT_STALE_MS
|
|
293
|
+
) {
|
|
294
|
+
// A fresh attempt may still be in flight in the original request —
|
|
295
|
+
// no additional upstream inspection is needed until that window ends.
|
|
296
|
+
// Older attempts are inspected without another redemption.
|
|
297
|
+
return jsonResponse(
|
|
298
|
+
{ operationId: effectiveOpId,
|
|
299
|
+
error: {
|
|
300
|
+
code: "attempt_in_progress",
|
|
301
|
+
message: "A redemption attempt for this operation is recent and may still be running; retry later",
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
409,
|
|
305
|
+
req,
|
|
306
|
+
config,
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
let remainingTokens: GrokResetCoupon[];
|
|
310
|
+
try {
|
|
311
|
+
const remaining = await getGrokRemainingResets({ accessToken: tokenSnapshot.accessToken });
|
|
312
|
+
remainingTokens = remaining.tokens;
|
|
313
|
+
} catch (err) {
|
|
314
|
+
return jsonResponse(
|
|
315
|
+
{ operationId: effectiveOpId, error: { code: "attempt_reconcile_failed", message: err instanceof Error ? err.message : String(err) } },
|
|
316
|
+
502,
|
|
317
|
+
req,
|
|
318
|
+
config,
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
// Availability cannot identify which request consumed a coupon. Even a
|
|
322
|
+
// missing, still-valid token does not confirm this operation succeeded.
|
|
323
|
+
const listed = remainingTokens.some((t) => t.tokenId === opRecord.tokenId);
|
|
324
|
+
return jsonResponse({ operationId: effectiveOpId, error: {
|
|
325
|
+
code: "attempt_unresolved",
|
|
326
|
+
message: listed
|
|
327
|
+
? "The prior redemption may still complete; its listed coupon will not be redeemed again"
|
|
328
|
+
: "The coupon is no longer listed, but this operation's outcome is unconfirmed; no redemption will be repeated",
|
|
329
|
+
} }, 409, req, config);
|
|
186
330
|
}
|
|
187
331
|
|
|
332
|
+
if (opRecord.kind === "token-mismatch") return jsonResponse({ operationId: effectiveOpId, error: {
|
|
333
|
+
code: "operation_token_mismatch", message: "Operation ID was previously registered with a different coupon",
|
|
334
|
+
} }, 409, req, config);
|
|
335
|
+
|
|
188
336
|
if (opRecord.kind === "identity-mismatch") {
|
|
189
337
|
return jsonResponse(
|
|
190
338
|
{
|
|
@@ -200,11 +348,14 @@ export async function handleGrokCouponRoutes(ctx: ManagementContext): Promise<Re
|
|
|
200
348
|
}
|
|
201
349
|
|
|
202
350
|
if (opRecord.kind !== "execute") {
|
|
351
|
+
// A capacity refusal cannot reserve the id, so an earlier same-id request still in
|
|
352
|
+
// credential resolution may open it once records expire: keep the caller on the id.
|
|
203
353
|
return jsonResponse(
|
|
204
354
|
{
|
|
355
|
+
...(operationId !== undefined ? { operationId: effectiveOpId } : {}),
|
|
205
356
|
error: {
|
|
206
357
|
code: opRecord.kind,
|
|
207
|
-
message: "Coupon ledger capacity
|
|
358
|
+
message: "Coupon ledger is at capacity; this request did not attempt a redemption. Retry the same operationId later",
|
|
208
359
|
},
|
|
209
360
|
},
|
|
210
361
|
503,
|
|
@@ -213,16 +364,51 @@ export async function handleGrokCouponRoutes(ctx: ManagementContext): Promise<Re
|
|
|
213
364
|
);
|
|
214
365
|
}
|
|
215
366
|
|
|
216
|
-
|
|
217
|
-
|
|
367
|
+
const settlePreflightRefusal = (code: string, tokenId?: string): Response | null => {
|
|
368
|
+
try {
|
|
369
|
+
const result = settleGrokResetCouponPreflightRefusal({
|
|
370
|
+
operationId: effectiveOpId, accountId, tokenId, code,
|
|
371
|
+
status: "failed", expectedStatus: "open",
|
|
372
|
+
});
|
|
373
|
+
if (result.kind === "recorded") return null;
|
|
374
|
+
if (result.kind === "replay") return jsonResponse({
|
|
375
|
+
code: result.code, replayed: true, tokenId: result.tokenId, settledAt: result.settledAt,
|
|
376
|
+
}, 200, req, config);
|
|
377
|
+
return jsonResponse({ operationId: effectiveOpId, error: {
|
|
378
|
+
code: "operation_state_changed",
|
|
379
|
+
message: "Another request changed this operation during inspection; retry the same operationId to read its durable state",
|
|
380
|
+
} }, 409, req, config);
|
|
381
|
+
} catch {
|
|
382
|
+
// The refusal could not be recorded, so the operation may still be claimed elsewhere.
|
|
383
|
+
return uncertain(effectiveOpId);
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
{
|
|
388
|
+
// Always consult the upstream list: it resolves the token when the
|
|
389
|
+
// caller omits one, and — for an explicit tokenId — proves the coupon
|
|
390
|
+
// still exists. Captured validity remains journal metadata, never proof
|
|
391
|
+
// that this operation succeeded during a later reconciliation.
|
|
392
|
+
let tokens: GrokResetCoupon[];
|
|
218
393
|
try {
|
|
219
394
|
const remaining = await getGrokRemainingResets({ accessToken: tokenSnapshot.accessToken });
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
395
|
+
tokens = remaining.tokens ?? [];
|
|
396
|
+
} catch (err) {
|
|
397
|
+
// Close the still-open operation before reporting a definitive failure: once it is
|
|
398
|
+
// recorded as failed it can never be claimed, so the caller may safely start over.
|
|
399
|
+
const changed = settlePreflightRefusal("fetch_resets_failed", resolvedTokenId);
|
|
400
|
+
if (changed) return changed;
|
|
401
|
+
return jsonResponse(
|
|
402
|
+
{ error: { code: "fetch_resets_failed", message: err instanceof Error ? err.message : String(err) } },
|
|
403
|
+
502,
|
|
404
|
+
req,
|
|
405
|
+
config,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
if (!resolvedTokenId) {
|
|
409
|
+
if (tokens.length === 0) {
|
|
410
|
+
const changed = settlePreflightRefusal("no_coupons_available");
|
|
411
|
+
if (changed) return changed;
|
|
226
412
|
return jsonResponse(
|
|
227
413
|
{ error: { code: "no_coupons_available", message: "No reset coupons available to redeem" } },
|
|
228
414
|
400,
|
|
@@ -230,29 +416,129 @@ export async function handleGrokCouponRoutes(ctx: ManagementContext): Promise<Re
|
|
|
230
416
|
config,
|
|
231
417
|
);
|
|
232
418
|
}
|
|
233
|
-
resolvedTokenId =
|
|
234
|
-
}
|
|
419
|
+
resolvedTokenId = tokens[0].tokenId;
|
|
420
|
+
}
|
|
421
|
+
const match = tokens.find((t) => t.tokenId === resolvedTokenId);
|
|
422
|
+
if (!match) {
|
|
423
|
+
// The requested coupon is already consumed or expired upstream —
|
|
424
|
+
// redeeming it would only surface an upstream error.
|
|
425
|
+
const changed = settlePreflightRefusal("coupon_unavailable", resolvedTokenId);
|
|
426
|
+
if (changed) return changed;
|
|
235
427
|
return jsonResponse(
|
|
236
|
-
{
|
|
237
|
-
|
|
428
|
+
{
|
|
429
|
+
error: {
|
|
430
|
+
code: "coupon_unavailable",
|
|
431
|
+
message: "The requested reset coupon is no longer available upstream",
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
409,
|
|
238
435
|
req,
|
|
239
436
|
config,
|
|
240
437
|
);
|
|
241
438
|
}
|
|
439
|
+
const parsedEnd = Date.parse(match.validityEnd);
|
|
440
|
+
if (!Number.isNaN(parsedEnd)) resolvedTokenValidityEnd = parsedEnd;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (resolvedTokenId === undefined) {
|
|
444
|
+
// Unreachable: every path above either resolves a token or returns.
|
|
445
|
+
const changed = settlePreflightRefusal("token_unresolved");
|
|
446
|
+
if (changed) return changed;
|
|
447
|
+
return jsonResponse(
|
|
448
|
+
{ error: { code: "token_unresolved", message: "No reset coupon token could be resolved" } },
|
|
449
|
+
500,
|
|
450
|
+
req,
|
|
451
|
+
config,
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (grokCouponInFlightAttempts.has(effectiveOpId)) {
|
|
456
|
+
// The original request holding this operation is still running in this
|
|
457
|
+
// process — its upstream spend has not concluded, so nothing may spend
|
|
458
|
+
// on this operationId again regardless of how stale the mark looks.
|
|
459
|
+
return jsonResponse(
|
|
460
|
+
{
|
|
461
|
+
operationId: effectiveOpId,
|
|
462
|
+
error: {
|
|
463
|
+
code: "attempt_in_progress",
|
|
464
|
+
message: "A redemption attempt for this operation is still running; retry later",
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
409,
|
|
468
|
+
req,
|
|
469
|
+
config,
|
|
470
|
+
);
|
|
242
471
|
}
|
|
243
472
|
|
|
473
|
+
grokCouponInFlightAttempts.add(effectiveOpId);
|
|
244
474
|
try {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
475
|
+
// Record the attempt BEFORE the spend call: a crash between redemption
|
|
476
|
+
// and settlement must still leave the operation non-open so a retry can
|
|
477
|
+
// never execute it again. The atomic claim admits exactly one contender;
|
|
478
|
+
// a failed mark write aborts here while nothing has been spent.
|
|
479
|
+
try {
|
|
480
|
+
const claimed = markGrokResetCouponAttempt(effectiveOpId, resolvedTokenId, undefined, undefined, resolvedTokenValidityEnd);
|
|
481
|
+
if (!claimed) {
|
|
482
|
+
// A concurrent request may already have reached a definitive outcome;
|
|
483
|
+
// replay it rather than holding the caller on a finished operation.
|
|
484
|
+
// A failed read proves nothing about the winner, so it stays uncertain:
|
|
485
|
+
// reporting a definitive error here would let the caller open a
|
|
486
|
+
// replacement operation while this one may already have spent.
|
|
487
|
+
let winner: ReturnType<typeof readGrokResetCouponTerminalReplay> = null;
|
|
488
|
+
try { winner = readGrokResetCouponTerminalReplay(effectiveOpId, accountId, resolvedTokenId); } catch { winner = null; }
|
|
489
|
+
if (winner) return jsonResponse({
|
|
490
|
+
code: winner.code, replayed: true, tokenId: winner.tokenId, settledAt: winner.settledAt,
|
|
491
|
+
}, 200, req, config);
|
|
492
|
+
return uncertain(effectiveOpId);
|
|
493
|
+
}
|
|
494
|
+
} catch {
|
|
495
|
+
// The claim write failed or was contended: this request dispatched nothing, but a
|
|
496
|
+
// concurrent holder of this operation may be spending, so keep the caller on it.
|
|
497
|
+
return uncertain(effectiveOpId);
|
|
498
|
+
}
|
|
249
499
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
})
|
|
500
|
+
try {
|
|
501
|
+
await redeemGrokResetCoupon({
|
|
502
|
+
accessToken: tokenSnapshot.accessToken,
|
|
503
|
+
tokenId: resolvedTokenId,
|
|
504
|
+
});
|
|
505
|
+
} catch (err) {
|
|
506
|
+
// A transport error cannot prove that the irreversible request failed.
|
|
507
|
+
// Keep "attempted" so a later retry can inspect without dispatching again.
|
|
508
|
+
return jsonResponse(
|
|
509
|
+
{ operationId: effectiveOpId, accountId, tokenId: resolvedTokenId, error: {
|
|
510
|
+
code: "attempt_unresolved",
|
|
511
|
+
message: "Redemption delivery is unconfirmed; reuse this operationId to inspect its state and do not create a replacement attempt",
|
|
512
|
+
} },
|
|
513
|
+
502,
|
|
514
|
+
req,
|
|
515
|
+
config,
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// A confirmed redemption remains successful even when settlement fails.
|
|
520
|
+
// The existing claim protects retries if settlement does not land.
|
|
521
|
+
let settlementRecorded = false;
|
|
522
|
+
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
523
|
+
try {
|
|
524
|
+
settlementRecorded = recordGrokResetCouponSettlement({
|
|
525
|
+
operationId: effectiveOpId,
|
|
526
|
+
accountId,
|
|
527
|
+
tokenId: resolvedTokenId,
|
|
528
|
+
code: "redeemed",
|
|
529
|
+
status: "success",
|
|
530
|
+
expectedStatus: "attempted",
|
|
531
|
+
});
|
|
532
|
+
break;
|
|
533
|
+
} catch (error) {
|
|
534
|
+
// Retry only transient acquisition contention, never an unsafe or
|
|
535
|
+
// unreadable ledger, a changed claim, or the upstream redemption.
|
|
536
|
+
const cause = error instanceof ConfigMutationLockError ? error.cause : undefined;
|
|
537
|
+
if (!cause || typeof cause !== "object" || !("code" in cause)
|
|
538
|
+
|| cause.code !== "SQLITE_BUSY" || attempt === 4) break;
|
|
539
|
+
await Bun.sleep(20);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
256
542
|
|
|
257
543
|
return jsonResponse(
|
|
258
544
|
{
|
|
@@ -262,24 +548,14 @@ export async function handleGrokCouponRoutes(ctx: ManagementContext): Promise<Re
|
|
|
262
548
|
tokenId: resolvedTokenId,
|
|
263
549
|
accountId,
|
|
264
550
|
operationId: effectiveOpId,
|
|
551
|
+
settlementRecorded,
|
|
265
552
|
},
|
|
266
553
|
200,
|
|
267
554
|
req,
|
|
268
555
|
config,
|
|
269
556
|
);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
operationId: effectiveOpId,
|
|
273
|
-
tokenId: resolvedTokenId,
|
|
274
|
-
code: "redeem_failed",
|
|
275
|
-
status: "failed",
|
|
276
|
-
});
|
|
277
|
-
return jsonResponse(
|
|
278
|
-
{ error: { code: "redeem_failed", message: err instanceof Error ? err.message : String(err) } },
|
|
279
|
-
502,
|
|
280
|
-
req,
|
|
281
|
-
config,
|
|
282
|
-
);
|
|
557
|
+
} finally {
|
|
558
|
+
grokCouponInFlightAttempts.delete(effectiveOpId);
|
|
283
559
|
}
|
|
284
560
|
}
|
|
285
561
|
|