@bitkyc08/opencodex 2.81.0 → 2.83.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 +166 -73
- package/gui/dist/assets/App-Ba8uG696.js +54 -0
- package/gui/dist/assets/{Tray-EwAOsP0S.js → Tray-IImbNPRy.js} +1 -1
- package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
- package/gui/dist/assets/{index-RsoaAxXY.js → index-BHRZdJuv.js} +13 -13
- package/gui/dist/assets/{quota-summary-D2lN8tDm.js → quota-summary-DiJO6sYY.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +1 -1
- 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 +18 -14
- package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
- package/src/adapters/openai-responses/passthrough.ts +11 -4
- 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/tool-output-recovery.ts +20 -0
- package/src/bridge/errors.ts +7 -1
- package/src/claude/agents-inject.ts +14 -3
- package/src/claude/context-windows.ts +22 -20
- package/src/claude/desktop-3p.ts +88 -12
- package/src/claude/desktop-picker.ts +1 -1
- package/src/claude/desktop-profile.ts +2 -1
- package/src/claude/gateway-cache.ts +18 -1
- package/src/claude/inbound.ts +77 -3
- package/src/claude/intercept/cli-catalog.ts +3 -0
- package/src/claude/intercept/picker-ca-persistence.ts +7 -6
- package/src/claude/intercept/picker-models.ts +14 -6
- package/src/claude/intercept/settings.ts +3 -0
- package/src/claude/long-context.ts +66 -0
- package/src/claude/model-info.ts +19 -16
- package/src/claude/outbound.ts +94 -9
- package/src/claude/subagent-model.ts +40 -11
- package/src/cli/account-auth.ts +20 -12
- package/src/cli/agent.ts +9 -5
- package/src/cli/aside-profile-recovery.ts +33 -0
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-integrations.ts +2 -2
- package/src/cli/cli-path-diagnostics.ts +132 -0
- package/src/cli/desktop-runtime-guidance.ts +65 -0
- package/src/cli/dispatch.ts +5 -0
- package/src/cli/doctor.ts +16 -3
- package/src/cli/index.ts +14 -9
- package/src/cli/integration-aside-sync.ts +2 -1
- package/src/cli/integrations.ts +16 -3
- package/src/cli/resolve.ts +26 -3
- package/src/cli/root.ts +4 -2
- package/src/cli/status.ts +64 -12
- package/src/cli/update-restart-child.ts +4 -2
- package/src/cli/update-restart-home.ts +28 -10
- package/src/cli/update-restart-service-record.ts +116 -0
- package/src/cli/update-restart-supervision.ts +96 -0
- package/src/cli/update-restart.ts +59 -13
- package/src/cli/v2.ts +27 -20
- package/src/cli/version-skew-notice.ts +77 -0
- 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 +1 -1
- package/src/clients/lazycodex.ts +1 -1
- package/src/clients/omo-role-models.ts +32 -7
- package/src/codex/auth-api/account-selection.ts +107 -0
- package/src/codex/auth-api/pool-mode-gate.ts +4 -5
- package/src/codex/auth-api/routes.ts +10 -49
- package/src/codex/auth-context.ts +28 -4
- package/src/codex/autostart-health.ts +12 -3
- package/src/codex/catalog/build-entries.ts +18 -0
- package/src/codex/catalog/bundled.ts +155 -26
- package/src/codex/catalog/combo-member.ts +3 -2
- package/src/codex/catalog/effort.ts +26 -5
- package/src/codex/catalog/gather-capture.ts +5 -0
- package/src/codex/catalog/model-hints.ts +15 -8
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/native-models.ts +1 -1
- package/src/codex/catalog/operator-tiers.ts +38 -0
- package/src/codex/catalog/provider-models.ts +68 -35
- package/src/codex/catalog/retained-sync.ts +7 -5
- package/src/codex/catalog/routed-gather.ts +15 -1
- package/src/codex/catalog-auto-refresh-sources.ts +3 -4
- package/src/codex/catalog-auto-refresh.ts +19 -0
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-home-owner.ts +3 -0
- package/src/codex/config-write-lock.ts +333 -0
- package/src/codex/convergence.ts +2 -1
- 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/features.ts +257 -200
- package/src/codex/inject/config-write-section.ts +34 -0
- package/src/codex/inject/multi-agent-v2.ts +14 -6
- package/src/codex/inject/remove.ts +36 -11
- package/src/codex/inject/restore.ts +85 -20
- package/src/codex/inject-coordination.ts +15 -6
- package/src/codex/inject.ts +62 -20
- package/src/codex/journal.ts +62 -17
- package/src/codex/main-account-cache.ts +43 -0
- package/src/codex/main-account.ts +6 -7
- package/src/codex/paths.ts +2 -0
- package/src/codex/prepared-home.ts +18 -0
- package/src/codex/prompt-journal.ts +25 -12
- package/src/codex/prompt-layers.ts +49 -26
- package/src/codex/prompt-lock-claim.ts +134 -0
- package/src/codex/prompt-lock-evidence.ts +26 -0
- package/src/codex/prompt-lock-io.ts +16 -0
- package/src/codex/prompt-lock-owner.ts +130 -0
- package/src/codex/prompt-lock.ts +84 -53
- package/src/codex/routing/failure-window.ts +146 -0
- package/src/codex/routing/selection.ts +4 -1
- package/src/codex/routing.ts +13 -17
- package/src/codex/runtime.ts +106 -18
- package/src/codex/shim.ts +45 -19
- package/src/combos/jev-service-exchange.ts +281 -0
- package/src/combos/jev.ts +34 -220
- package/src/config/atomic-write.ts +1 -1
- package/src/config/codex-ws-reuse-setting.ts +10 -0
- package/src/config/live-reconcile.ts +17 -0
- package/src/config/load-degrade.ts +8 -0
- package/src/config/rebase-provenance.ts +11 -0
- package/src/config/schema/config-schema.ts +4 -0
- package/src/generated/compatibility-version.json +375 -227
- package/src/generated/model-metadata.ts +2 -2
- package/src/grok/inject.ts +276 -206
- package/src/grok/reset-coupon-ledger.ts +223 -57
- package/src/grok/reset-coupons.ts +13 -6
- package/src/grok/sync.ts +2 -1
- package/src/images/loop.ts +29 -11
- package/src/integrations/aside-profiles.ts +10 -3
- package/src/integrations/catalog-refresh.ts +18 -2
- package/src/integrations/cursor-detect.ts +1 -1
- package/src/integrations/writer.ts +9 -3
- package/src/lib/bun-path-runtime.d.mts +32 -0
- package/src/lib/bun-path-runtime.mjs +95 -0
- package/src/lib/bun-runtime-preflight.ts +140 -0
- package/src/lib/desktop-cli-handoff.d.mts +20 -0
- package/src/lib/desktop-cli-handoff.mjs +92 -0
- package/src/lib/desktop-cli-record.d.mts +29 -0
- package/src/lib/desktop-cli-record.mjs +102 -0
- package/src/lib/errors.ts +25 -0
- package/src/lib/redact.ts +2 -2
- package/src/lib/request-execution-budget.ts +8 -0
- package/src/lib/synced-state-location.ts +46 -6
- package/src/lib/test-home-guard.ts +87 -1
- package/src/lib/translator-budget.ts +17 -4
- package/src/lib/windows-atomic-replace.ts +38 -16
- package/src/lib/windows-elevation.ts +6 -0
- package/src/lib/windows-secret-acl.ts +92 -25
- package/src/lib/winsw.ts +2 -3
- package/src/oauth/anthropic-account-refusal.ts +39 -1
- package/src/oauth/anthropic-routing.ts +1 -1
- package/src/oauth/anthropic-send-ownership.ts +3 -3
- package/src/oauth/chatgpt.ts +113 -41
- package/src/oauth/index.ts +7 -3
- package/src/oauth/meta-muse-device.ts +1 -1
- package/src/oauth/store.ts +1 -1
- package/src/oauth/types.ts +1 -1
- package/src/providers/anthropic-fast.ts +1 -1
- package/src/providers/anthropic-reset-grant-ledger.ts +1 -1
- package/src/providers/azure-model-metadata.ts +115 -0
- package/src/providers/derive.ts +21 -0
- 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/model-presets.ts +1 -1
- package/src/providers/muse-subscription-usage.ts +1 -1
- package/src/providers/quota/vendor-probes-key.ts +127 -17
- package/src/providers/quota.ts +1 -1
- package/src/providers/registry/entries-core.ts +6 -6
- package/src/providers/registry/entries-extended.ts +4 -0
- package/src/providers/registry/model-seeds.ts +3 -3
- package/src/providers/xai-fast-model.ts +1 -1
- package/src/responses/reasoning-envelope.ts +29 -1
- package/src/responses/reasoning-replay-cache.ts +17 -1
- package/src/responses/spill-store.ts +15 -2
- package/src/responses/state/metrics.ts +8 -0
- package/src/responses/state/spill-failure.ts +12 -0
- package/src/responses/state/spill-queue.ts +12 -5
- package/src/responses/state.ts +18 -10
- package/src/responses/task-input.ts +17 -0
- package/src/server/background-lifecycle.ts +27 -0
- package/src/server/chat-completions.ts +1 -0
- package/src/server/chat-native.ts +28 -12
- package/src/server/claude-messages.ts +43 -6
- package/src/server/inbound-body-admission.ts +25 -7
- package/src/server/index/claude-intercept-lifecycle.ts +3 -1
- package/src/server/index/link-listener.ts +30 -17
- package/src/server/index/serve-options.ts +18 -4
- package/src/server/index/spend-ledger-lifecycle.ts +2 -1
- package/src/server/index/startup-warnings.ts +19 -0
- package/src/server/index.ts +2 -1
- package/src/server/lifecycle.ts +31 -4
- package/src/server/management/agent-settings-routes.ts +93 -32
- package/src/server/management/codex-agent-role-routes.ts +4 -2
- package/src/server/management/codex-prompt-routes.ts +1 -0
- package/src/server/management/codex-role-auto-assign.ts +13 -2
- package/src/server/management/config-routes.ts +59 -8
- package/src/server/management/context.ts +2 -2
- package/src/server/management/grok-coupon-routes.ts +277 -52
- package/src/server/management/link-routes.ts +42 -20
- package/src/server/management/route-registry.ts +2 -2
- package/src/server/management/shared.ts +3 -3
- package/src/server/messages-native-oauth.ts +9 -8
- package/src/server/messages-native.ts +49 -13
- package/src/server/messages-response-headers.ts +14 -0
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +18 -7
- package/src/server/request-log-conversation.ts +48 -5
- package/src/server/request-log-failure-attribution.ts +3 -0
- package/src/server/request-log-terminal-status.ts +186 -0
- package/src/server/request-log.ts +15 -89
- package/src/server/responses/adapter-continuation.ts +11 -3
- package/src/server/responses/adapter-dispatch.ts +99 -19
- package/src/server/responses/codex-ws-exchange.ts +17 -2
- package/src/server/responses/codex-ws-pool.ts +100 -13
- package/src/server/responses/codex-ws-wire.ts +9 -0
- package/src/server/responses/combo-requested-effort.ts +3 -1
- package/src/server/responses/compact.ts +3 -2
- package/src/server/responses/core-codex-account.ts +22 -1
- package/src/server/responses/core-combo-native.ts +20 -3
- package/src/server/responses/core-combo.ts +22 -14
- package/src/server/responses/core-lifetime.ts +3 -36
- package/src/server/responses/core-opaque-recovery.ts +5 -5
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-replay.ts +73 -0
- package/src/server/responses/core.ts +9 -9
- package/src/server/responses/empty-completion-guard.ts +18 -2
- package/src/server/responses/fetch-helpers.ts +26 -3
- package/src/server/responses/jev-model-invoke.ts +1 -1
- package/src/server/responses/passthrough-delivery.ts +24 -2
- package/src/server/responses/passthrough-dispatch.ts +134 -62
- package/src/server/responses/passthrough-execution.ts +3 -3
- package/src/server/responses/request-prepare.ts +4 -0
- package/src/server/responses/request-send-budget.ts +112 -49
- package/src/server/responses/request-transport.ts +125 -5
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +15 -5
- package/src/server/responses/sidecar-execution.ts +30 -16
- package/src/server/responses/sidecar-send-budget.ts +58 -0
- package/src/server/responses/spend-storage-error.ts +33 -0
- package/src/server/responses/ws-upstream.ts +19 -6
- package/src/server/startup-health-cache.ts +2 -1
- package/src/server/workflow-refusal.ts +3 -0
- package/src/service/cli.ts +20 -4
- package/src/service/desktop-command-guard.ts +32 -0
- package/src/service/desktop-startup.ts +66 -40
- package/src/service/desktop-supervision.d.mts +26 -0
- package/src/service/desktop-supervision.mjs +140 -0
- package/src/service/managing-cli.ts +1 -0
- package/src/service/orchestration.ts +46 -20
- package/src/service/repair.ts +25 -9
- package/src/service/state.ts +4 -3
- package/src/service/systemd.ts +17 -8
- package/src/service/windows-ops.ts +61 -22
- package/src/storage/policy-job.ts +5 -1
- package/src/storage/policy-worker.ts +3 -1
- package/src/storage/policy.ts +9 -0
- package/src/types/config.ts +21 -0
- package/src/types/request.ts +10 -0
- package/src/types/wire.ts +1 -0
- package/src/update/index.ts +57 -9
- package/src/update/restart-ownership.ts +10 -3
- package/src/update/runtime-ownership.d.mts +6 -0
- package/src/update/runtime-ownership.mjs +23 -7
- package/src/usage/expected-prices.ts +1 -1
- package/src/web-search/loop.ts +5 -2
- package/gui/dist/assets/App-7yhoys8a.js +0 -54
|
@@ -9,6 +9,7 @@ import { isAnthropicInstanceId } from "../../providers/anthropic-instance-id";
|
|
|
9
9
|
import { captureConfigTopLevelRollback } from "../../config/rebase-provenance";
|
|
10
10
|
import type { IntegrationClientId } from "../../integrations/registry";
|
|
11
11
|
import { randomUUID } from "node:crypto";
|
|
12
|
+
import { isDeepStrictEqual } from "node:util";
|
|
12
13
|
import { readFileSync } from "node:fs";
|
|
13
14
|
import type { CatalogModel } from "../../codex/catalog";
|
|
14
15
|
import { catalogModelSlug, invalidateCodexModelsCache, nativeContextLimits, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
|
|
@@ -182,6 +183,17 @@ interface ClientIntegrationSyncOutcome {
|
|
|
182
183
|
readonly profileId?: number;
|
|
183
184
|
}
|
|
184
185
|
|
|
186
|
+
/** Only the Desktop write's own applied markers may change during a background projection. */
|
|
187
|
+
function clientProjectionConfig(config: OcxConfig): OcxConfig {
|
|
188
|
+
const snapshot = structuredClone(config);
|
|
189
|
+
const profile = snapshot.claudeCode?.desktopProfile;
|
|
190
|
+
if (profile) {
|
|
191
|
+
delete profile.appliedFingerprint;
|
|
192
|
+
delete profile.appliedAt;
|
|
193
|
+
}
|
|
194
|
+
return snapshot;
|
|
195
|
+
}
|
|
196
|
+
|
|
185
197
|
/**
|
|
186
198
|
* Re-inject native clients that are switched ON and every file integration whose
|
|
187
199
|
* OpenCodex ownership record is the operator's durable opt-in.
|
|
@@ -205,17 +217,41 @@ export async function syncEnabledClientIntegrations(
|
|
|
205
217
|
config: OcxConfig,
|
|
206
218
|
deps: Pick<ManagementContext["deps"],
|
|
207
219
|
"fetchAllModels" | "refreshOwnedCatalogIntegrations" | "writeDesktop3pConfig"> = {},
|
|
220
|
+
options: { unattended?: { isCurrent: () => boolean } } = {},
|
|
208
221
|
): Promise<ClientIntegrationSyncOutcome[]> {
|
|
209
222
|
// A sibling instance passes its OWN port here; the Grok fence and the Desktop gateway profile
|
|
210
223
|
// stay on the live owner's (`src/codex/sibling-start.ts`).
|
|
211
224
|
if (port === undefined || siblingOfLivePort() !== null) return [];
|
|
212
|
-
const
|
|
225
|
+
const projectionConfig = options.unattended ? clientProjectionConfig(config) : undefined;
|
|
226
|
+
const { claudeDesktopIntegrationEnabled, grokIntegrationEnabled, localClientSyncAllowed } = await import("../../codex/desired-state");
|
|
213
227
|
const out: ClientIntegrationSyncOutcome[] = [];
|
|
228
|
+
const unattended = options.unattended;
|
|
229
|
+
const admit = () => {
|
|
230
|
+
if (!unattended!.isCurrent()) return false;
|
|
231
|
+
const fresh = loadConfig();
|
|
232
|
+
return localClientSyncAllowed(fresh) && isDeepStrictEqual(clientProjectionConfig(fresh), projectionConfig);
|
|
233
|
+
};
|
|
234
|
+
const stale = () => unattended !== undefined && !admit();
|
|
235
|
+
if (stale()) return out;
|
|
236
|
+
const grok = unattended ? await import("../../grok/inject") : undefined;
|
|
214
237
|
|
|
215
|
-
if (grokIntegrationEnabled(config)) {
|
|
238
|
+
if (grokIntegrationEnabled(config) && (!grok || grok.grokManagedBlockPresent())) {
|
|
216
239
|
try {
|
|
217
240
|
const { syncGrokConfig } = await import("../../grok/sync");
|
|
218
|
-
|
|
241
|
+
if (stale()) return out;
|
|
242
|
+
const grokDeps = grok ? {
|
|
243
|
+
fetchAllModels: deps.fetchAllModels ?? (await import("../management-api")).fetchAllModels,
|
|
244
|
+
injectGrokConfig: ((...args: Parameters<typeof grok.injectGrokConfig>) =>
|
|
245
|
+
admit() && grokIntegrationEnabled(loadConfig()) && grok.grokManagedBlockPresent()
|
|
246
|
+
? grok.injectGrokConfig(...args)
|
|
247
|
+
: { ok: true, changed: false, message: "Grok refresh skipped" }),
|
|
248
|
+
} : undefined;
|
|
249
|
+
const r = unattended
|
|
250
|
+
? await syncGrokConfig(port, config, {
|
|
251
|
+
...config.hostname ? { hostname: config.hostname } : {},
|
|
252
|
+
refreshOnly: { admit },
|
|
253
|
+
}, grokDeps)
|
|
254
|
+
: await syncGrokConfig(port, config, config.hostname ? { hostname: config.hostname } : {}, grokDeps);
|
|
219
255
|
out.push(r.ok
|
|
220
256
|
? { client: "grok", ok: true, changed: r.changed === true }
|
|
221
257
|
: { client: "grok", ok: false, reason: r.message });
|
|
@@ -224,9 +260,11 @@ export async function syncEnabledClientIntegrations(
|
|
|
224
260
|
}
|
|
225
261
|
}
|
|
226
262
|
|
|
263
|
+
if (stale()) return out;
|
|
227
264
|
const { observeClaudeDesktopMode, resolveClaudeDesktopMode } = await import("../../claude/desktop-first-party");
|
|
228
265
|
// A first-party Desktop must never get a gateway profile written and selected by a sync.
|
|
229
|
-
if (claudeDesktopIntegrationEnabled(config) && resolveClaudeDesktopMode(config, observeClaudeDesktopMode(config)) !== "first-party"
|
|
266
|
+
if (claudeDesktopIntegrationEnabled(config) && resolveClaudeDesktopMode(config, observeClaudeDesktopMode(config)) !== "first-party"
|
|
267
|
+
&& (!unattended || !!config.claudeCode?.desktopProfile?.appliedFingerprint)) {
|
|
230
268
|
try {
|
|
231
269
|
const { writeDesktop3pConfig } = await import("../../claude/desktop-3p");
|
|
232
270
|
const { desktopVisibleNativeSlugs, filterCatalogVisibleModels } = await import("../../codex/catalog");
|
|
@@ -240,12 +278,15 @@ export async function syncEnabledClientIntegrations(
|
|
|
240
278
|
const latest = loadConfig();
|
|
241
279
|
// Discovery awaited: the mode may have changed meanwhile. Re-resolve on the fresh read,
|
|
242
280
|
// immediately before the writer, so a first-party switch during fetchAllModels still wins.
|
|
243
|
-
if (
|
|
281
|
+
if (stale()) return;
|
|
282
|
+
if (claudeDesktopIntegrationEnabled(latest) && resolveClaudeDesktopMode(latest, observeClaudeDesktopMode(latest)) !== "first-party"
|
|
283
|
+
&& (!unattended || !!latest.claudeCode?.desktopProfile?.appliedFingerprint)) {
|
|
244
284
|
const routed = filterCatalogVisibleModels(models, latest)
|
|
245
285
|
.map(model => ({ provider: model.provider, id: model.id, contextWindow: model.contextWindow }));
|
|
246
286
|
const writtenProfile = latest.claudeCode?.desktopProfile;
|
|
247
287
|
const markerBaseline = captureDesktopAppliedMarker(writtenProfile);
|
|
248
|
-
|
|
288
|
+
// Keep the attended writer's argument count unchanged, including its lock test seam.
|
|
289
|
+
const writeArgs: Parameters<typeof writeDesktop3pConfig> = [
|
|
249
290
|
port,
|
|
250
291
|
[...desktopVisibleNativeSlugs(latest)],
|
|
251
292
|
routed,
|
|
@@ -253,7 +294,13 @@ export async function syncEnabledClientIntegrations(
|
|
|
253
294
|
"static",
|
|
254
295
|
writtenProfile,
|
|
255
296
|
nativeContextLimits(latest),
|
|
256
|
-
|
|
297
|
+
];
|
|
298
|
+
if (unattended) {
|
|
299
|
+
writeArgs[7] = undefined;
|
|
300
|
+
writeArgs[8] = { appliedFingerprint: writtenProfile!.appliedFingerprint!, admit };
|
|
301
|
+
}
|
|
302
|
+
const r = (deps.writeDesktop3pConfig ?? writeDesktop3pConfig)(...writeArgs);
|
|
303
|
+
if (unattended && r.reason === "desktop_refresh_only_skipped") return;
|
|
257
304
|
if (!r.written || !r.fingerprint) {
|
|
258
305
|
out.push({ client: "claude-desktop", ok: false, reason: r.reason ?? "Claude Desktop write failed" });
|
|
259
306
|
} else {
|
|
@@ -271,6 +318,7 @@ export async function syncEnabledClientIntegrations(
|
|
|
271
318
|
}
|
|
272
319
|
}
|
|
273
320
|
|
|
321
|
+
if (stale()) return out;
|
|
274
322
|
const { refreshOwnedCatalogIntegrations } = await import("../../integrations/catalog-refresh");
|
|
275
323
|
const refreshOwned = deps.refreshOwnedCatalogIntegrations ?? refreshOwnedCatalogIntegrations;
|
|
276
324
|
out.push(...await refreshOwned({
|
|
@@ -280,7 +328,10 @@ export async function syncEnabledClientIntegrations(
|
|
|
280
328
|
},
|
|
281
329
|
config,
|
|
282
330
|
port,
|
|
283
|
-
},
|
|
331
|
+
}, unattended
|
|
332
|
+
? ["mcode", "pi", "aside", "raycast", "omo", "commandcode", "droid", "opencode", "kilo"]
|
|
333
|
+
: ["mcode", "pi", "aside", "raycast", "omo", "cline", "commandcode", "droid", "opencode", "kilo"],
|
|
334
|
+
...unattended ? [{ refreshOnly: true, admit }] as const : []));
|
|
284
335
|
|
|
285
336
|
return out;
|
|
286
337
|
}
|
|
@@ -64,8 +64,8 @@ export interface ManagementApiDeps {
|
|
|
64
64
|
refreshOwnedCatalogIntegrations?: typeof refreshOwnedCatalogIntegrations;
|
|
65
65
|
/** Platform seam for capability projections; does not alter host-level startup behavior. */
|
|
66
66
|
platform?: NodeJS.Platform;
|
|
67
|
-
toggleCodexMultiAgentV2?: (enabled: boolean) => void;
|
|
68
|
-
toggleDefaultModeRequestUserInput?: (enabled: boolean) => void;
|
|
67
|
+
toggleCodexMultiAgentV2?: (enabled: boolean, env: NodeJS.ProcessEnv, validateBeforeSpawn: () => void) => void;
|
|
68
|
+
toggleDefaultModeRequestUserInput?: (enabled: boolean, env: NodeJS.ProcessEnv, validateBeforeSpawn: () => void) => void;
|
|
69
69
|
createManagementConvergeCodex?: (config: Readonly<OcxConfig>) => ConvergeCodex;
|
|
70
70
|
/** Test-only destination for best-effort Claude agent-definition sync. */
|
|
71
71
|
claudeAgentConfigDir?: string;
|
|
@@ -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,12 @@ import {
|
|
|
20
21
|
type GrokResetCoupon,
|
|
21
22
|
} from "../../grok/reset-coupons";
|
|
22
23
|
import {
|
|
24
|
+
getGrokResetCouponOperationAccountId,
|
|
25
|
+
markGrokResetCouponAttempt,
|
|
23
26
|
openGrokResetCouponOperation,
|
|
24
27
|
recordGrokResetCouponSettlement,
|
|
28
|
+
readGrokResetCouponTerminalReplay,
|
|
29
|
+
settleGrokResetCouponPreflightRefusal,
|
|
25
30
|
type GrokResetCouponOperationRecord,
|
|
26
31
|
} from "../../grok/reset-coupon-ledger";
|
|
27
32
|
|
|
@@ -41,6 +46,11 @@ export interface GrokConsumeCouponRequestBody {
|
|
|
41
46
|
operationId?: string;
|
|
42
47
|
}
|
|
43
48
|
|
|
49
|
+
// A recent attempt can still be in flight. Older attempts may be inspected,
|
|
50
|
+
// but a remaining token is not proof that the original request cannot land.
|
|
51
|
+
const GROK_COUPON_ATTEMPT_STALE_MS = 90_000;
|
|
52
|
+
const grokCouponInFlightAttempts = new Set<string>();
|
|
53
|
+
|
|
44
54
|
function resolveTargetAccountId(requestedAccountId?: string): string {
|
|
45
55
|
if (requestedAccountId && requestedAccountId.trim() !== "") {
|
|
46
56
|
return requestedAccountId.trim();
|
|
@@ -130,6 +140,10 @@ export async function handleGrokCouponRoutes(ctx: ManagementContext): Promise<Re
|
|
|
130
140
|
|
|
131
141
|
const { accountId: rawAccountId, tokenId: requestedTokenId, operationId } = body;
|
|
132
142
|
|
|
143
|
+
if (requestedTokenId !== undefined && (typeof requestedTokenId !== "string" || requestedTokenId.trim() === "")) {
|
|
144
|
+
return jsonResponse({ error: { code: "invalid_token_id", message: "tokenId must be a non-empty string" } }, 400, req, config);
|
|
145
|
+
}
|
|
146
|
+
|
|
133
147
|
if (operationId !== undefined && !isCodexResetCreditOperationId(operationId)) {
|
|
134
148
|
return jsonResponse(
|
|
135
149
|
{ error: { code: "invalid_operation_id", message: "operationId must be a valid UUIDv4" } },
|
|
@@ -139,9 +153,16 @@ export async function handleGrokCouponRoutes(ctx: ManagementContext): Promise<Re
|
|
|
139
153
|
);
|
|
140
154
|
}
|
|
141
155
|
|
|
156
|
+
let recordedAccountId: string | undefined;
|
|
157
|
+
if (rawAccountId === undefined && operationId !== undefined) {
|
|
158
|
+
try { recordedAccountId = getGrokResetCouponOperationAccountId(operationId); }
|
|
159
|
+
catch {
|
|
160
|
+
return jsonResponse({ error: { code: "ledger_unavailable", message: "Coupon ledger could not be read or locked; no redemption was attempted" } }, 503, req, config);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
142
163
|
let accountId: string;
|
|
143
164
|
try {
|
|
144
|
-
accountId = resolveTargetAccountId(rawAccountId);
|
|
165
|
+
accountId = resolveTargetAccountId(rawAccountId ?? recordedAccountId);
|
|
145
166
|
} catch (err) {
|
|
146
167
|
return jsonResponse(
|
|
147
168
|
{ error: { code: "no_account", message: err instanceof Error ? err.message : String(err) } },
|
|
@@ -165,26 +186,106 @@ export async function handleGrokCouponRoutes(ctx: ManagementContext): Promise<Re
|
|
|
165
186
|
|
|
166
187
|
// Journaling and Idempotency settlement check
|
|
167
188
|
const effectiveOpId = operationId ?? crypto.randomUUID();
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
189
|
+
if (grokCouponInFlightAttempts.has(effectiveOpId)) return jsonResponse({ error: {
|
|
190
|
+
code: "attempt_in_progress", message: "This operation still has an active request; retry later",
|
|
191
|
+
} }, 409, req, config);
|
|
192
|
+
let opRecord: GrokResetCouponOperationRecord;
|
|
193
|
+
try {
|
|
194
|
+
opRecord = openGrokResetCouponOperation({ accountId, tokenId: requestedTokenId, operationId: effectiveOpId });
|
|
195
|
+
} catch {
|
|
196
|
+
return jsonResponse({ error: { code: "ledger_unavailable", message: "Coupon ledger could not be read or locked; no redemption was attempted" } }, 503, req, config);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let resolvedTokenId = requestedTokenId ?? opRecord.tokenId;
|
|
200
|
+
let resolvedTokenValidityEnd: number | undefined;
|
|
173
201
|
|
|
174
202
|
if (opRecord.kind === "replay") {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
203
|
+
if (opRecord.code !== undefined) {
|
|
204
|
+
return jsonResponse(
|
|
205
|
+
{
|
|
206
|
+
code: opRecord.code,
|
|
207
|
+
replayed: true,
|
|
208
|
+
tokenId: opRecord.tokenId,
|
|
209
|
+
settledAt: opRecord.settledAt,
|
|
210
|
+
},
|
|
211
|
+
200,
|
|
212
|
+
req,
|
|
213
|
+
config,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
if (opRecord.tokenId === undefined) {
|
|
217
|
+
// Legacy opens may already have dispatched without recording a token.
|
|
218
|
+
return jsonResponse({ operationId: effectiveOpId, error: {
|
|
219
|
+
code: "attempt_unresolved", message: "The prior operation has no recorded token; preserve this operationId and do not create a replacement attempt",
|
|
220
|
+
} }, 409, req, config);
|
|
221
|
+
}
|
|
222
|
+
// "attempted" with no recorded outcome: the spend call fired (or the
|
|
223
|
+
// process died right after the mark) but nothing was settled. Never
|
|
224
|
+
// replay this as a success — reconcile against upstream instead. A
|
|
225
|
+
// Remaining-token availability cannot identify the original request's
|
|
226
|
+
// outcome, and a listed token cannot rule out delayed completion.
|
|
227
|
+
if (requestedTokenId !== undefined && requestedTokenId !== opRecord.tokenId) {
|
|
228
|
+
// The retry names a different coupon than the one marked: spending
|
|
229
|
+
// either of them would surprise the caller — refuse and let them
|
|
230
|
+
// retry with the recorded token and preserve the operationId.
|
|
231
|
+
return jsonResponse(
|
|
232
|
+
{
|
|
233
|
+
error: {
|
|
234
|
+
code: "operation_token_mismatch",
|
|
235
|
+
message: "Operation was attempted with a different coupon; preserve the operationId and retry with the recorded tokenId",
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
409,
|
|
239
|
+
req,
|
|
240
|
+
config,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
if (
|
|
244
|
+
opRecord.attemptedAt === undefined ||
|
|
245
|
+
Date.now() - opRecord.attemptedAt < GROK_COUPON_ATTEMPT_STALE_MS
|
|
246
|
+
) {
|
|
247
|
+
// A fresh attempt may still be in flight in the original request —
|
|
248
|
+
// no additional upstream inspection is needed until that window ends.
|
|
249
|
+
// Older attempts are inspected without another redemption.
|
|
250
|
+
return jsonResponse(
|
|
251
|
+
{ operationId: effectiveOpId,
|
|
252
|
+
error: {
|
|
253
|
+
code: "attempt_in_progress",
|
|
254
|
+
message: "A redemption attempt for this operation is recent and may still be running; retry later",
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
409,
|
|
258
|
+
req,
|
|
259
|
+
config,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
let remainingTokens: GrokResetCoupon[];
|
|
263
|
+
try {
|
|
264
|
+
const remaining = await getGrokRemainingResets({ accessToken: tokenSnapshot.accessToken });
|
|
265
|
+
remainingTokens = remaining.tokens;
|
|
266
|
+
} catch (err) {
|
|
267
|
+
return jsonResponse(
|
|
268
|
+
{ operationId: effectiveOpId, error: { code: "attempt_reconcile_failed", message: err instanceof Error ? err.message : String(err) } },
|
|
269
|
+
502,
|
|
270
|
+
req,
|
|
271
|
+
config,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
// Availability cannot identify which request consumed a coupon. Even a
|
|
275
|
+
// missing, still-valid token does not confirm this operation succeeded.
|
|
276
|
+
const listed = remainingTokens.some((t) => t.tokenId === opRecord.tokenId);
|
|
277
|
+
return jsonResponse({ operationId: effectiveOpId, error: {
|
|
278
|
+
code: "attempt_unresolved",
|
|
279
|
+
message: listed
|
|
280
|
+
? "The prior redemption may still complete; its listed coupon will not be redeemed again"
|
|
281
|
+
: "The coupon is no longer listed, but this operation's outcome is unconfirmed; no redemption will be repeated",
|
|
282
|
+
} }, 409, req, config);
|
|
186
283
|
}
|
|
187
284
|
|
|
285
|
+
if (opRecord.kind === "token-mismatch") return jsonResponse({ error: {
|
|
286
|
+
code: "operation_token_mismatch", message: "Operation ID was previously registered with a different coupon",
|
|
287
|
+
} }, 409, req, config);
|
|
288
|
+
|
|
188
289
|
if (opRecord.kind === "identity-mismatch") {
|
|
189
290
|
return jsonResponse(
|
|
190
291
|
{
|
|
@@ -213,16 +314,48 @@ export async function handleGrokCouponRoutes(ctx: ManagementContext): Promise<Re
|
|
|
213
314
|
);
|
|
214
315
|
}
|
|
215
316
|
|
|
216
|
-
|
|
217
|
-
|
|
317
|
+
const settlePreflightRefusal = (code: string, tokenId?: string): Response | null => {
|
|
318
|
+
try {
|
|
319
|
+
const result = settleGrokResetCouponPreflightRefusal({
|
|
320
|
+
operationId: effectiveOpId, accountId, tokenId, code,
|
|
321
|
+
status: "failed", expectedStatus: "open",
|
|
322
|
+
});
|
|
323
|
+
if (result.kind === "recorded") return null;
|
|
324
|
+
if (result.kind === "replay") return jsonResponse({
|
|
325
|
+
code: result.code, replayed: true, tokenId: result.tokenId, settledAt: result.settledAt,
|
|
326
|
+
}, 200, req, config);
|
|
327
|
+
return jsonResponse({ error: {
|
|
328
|
+
code: "operation_state_changed",
|
|
329
|
+
message: "Another request changed this operation during inspection; retry the same operationId to read its durable state",
|
|
330
|
+
} }, 409, req, config);
|
|
331
|
+
} catch {
|
|
332
|
+
return jsonResponse({ error: {
|
|
333
|
+
code: "ledger_unavailable", message: "Coupon refusal could not be recorded; no redemption was attempted",
|
|
334
|
+
} }, 503, req, config);
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
{
|
|
339
|
+
// Always consult the upstream list: it resolves the token when the
|
|
340
|
+
// caller omits one, and — for an explicit tokenId — proves the coupon
|
|
341
|
+
// still exists. Captured validity remains journal metadata, never proof
|
|
342
|
+
// that this operation succeeded during a later reconciliation.
|
|
343
|
+
let tokens: GrokResetCoupon[];
|
|
218
344
|
try {
|
|
219
345
|
const remaining = await getGrokRemainingResets({ accessToken: tokenSnapshot.accessToken });
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
346
|
+
tokens = remaining.tokens ?? [];
|
|
347
|
+
} catch (err) {
|
|
348
|
+
return jsonResponse(
|
|
349
|
+
{ error: { code: "fetch_resets_failed", message: err instanceof Error ? err.message : String(err) } },
|
|
350
|
+
502,
|
|
351
|
+
req,
|
|
352
|
+
config,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
if (!resolvedTokenId) {
|
|
356
|
+
if (tokens.length === 0) {
|
|
357
|
+
const changed = settlePreflightRefusal("no_coupons_available");
|
|
358
|
+
if (changed) return changed;
|
|
226
359
|
return jsonResponse(
|
|
227
360
|
{ error: { code: "no_coupons_available", message: "No reset coupons available to redeem" } },
|
|
228
361
|
400,
|
|
@@ -230,29 +363,131 @@ export async function handleGrokCouponRoutes(ctx: ManagementContext): Promise<Re
|
|
|
230
363
|
config,
|
|
231
364
|
);
|
|
232
365
|
}
|
|
233
|
-
resolvedTokenId =
|
|
234
|
-
}
|
|
366
|
+
resolvedTokenId = tokens[0].tokenId;
|
|
367
|
+
}
|
|
368
|
+
const match = tokens.find((t) => t.tokenId === resolvedTokenId);
|
|
369
|
+
if (!match) {
|
|
370
|
+
// The requested coupon is already consumed or expired upstream —
|
|
371
|
+
// redeeming it would only surface an upstream error.
|
|
372
|
+
const changed = settlePreflightRefusal("coupon_unavailable", resolvedTokenId);
|
|
373
|
+
if (changed) return changed;
|
|
235
374
|
return jsonResponse(
|
|
236
|
-
{
|
|
237
|
-
|
|
375
|
+
{
|
|
376
|
+
error: {
|
|
377
|
+
code: "coupon_unavailable",
|
|
378
|
+
message: "The requested reset coupon is no longer available upstream",
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
409,
|
|
238
382
|
req,
|
|
239
383
|
config,
|
|
240
384
|
);
|
|
241
385
|
}
|
|
386
|
+
const parsedEnd = Date.parse(match.validityEnd);
|
|
387
|
+
if (!Number.isNaN(parsedEnd)) resolvedTokenValidityEnd = parsedEnd;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (resolvedTokenId === undefined) {
|
|
391
|
+
// Unreachable: every path above either resolves a token or returns.
|
|
392
|
+
return jsonResponse(
|
|
393
|
+
{ error: { code: "token_unresolved", message: "No reset coupon token could be resolved" } },
|
|
394
|
+
500,
|
|
395
|
+
req,
|
|
396
|
+
config,
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (grokCouponInFlightAttempts.has(effectiveOpId)) {
|
|
401
|
+
// The original request holding this operation is still running in this
|
|
402
|
+
// process — its upstream spend has not concluded, so nothing may spend
|
|
403
|
+
// on this operationId again regardless of how stale the mark looks.
|
|
404
|
+
return jsonResponse(
|
|
405
|
+
{
|
|
406
|
+
error: {
|
|
407
|
+
code: "attempt_in_progress",
|
|
408
|
+
message: "A redemption attempt for this operation is still running; retry later",
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
409,
|
|
412
|
+
req,
|
|
413
|
+
config,
|
|
414
|
+
);
|
|
242
415
|
}
|
|
243
416
|
|
|
417
|
+
grokCouponInFlightAttempts.add(effectiveOpId);
|
|
244
418
|
try {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
419
|
+
// Record the attempt BEFORE the spend call: a crash between redemption
|
|
420
|
+
// and settlement must still leave the operation non-open so a retry can
|
|
421
|
+
// never execute it again. The atomic claim admits exactly one contender;
|
|
422
|
+
// a failed mark write aborts here while nothing has been spent.
|
|
423
|
+
try {
|
|
424
|
+
const claimed = markGrokResetCouponAttempt(effectiveOpId, resolvedTokenId, undefined, undefined, resolvedTokenValidityEnd);
|
|
425
|
+
if (!claimed) {
|
|
426
|
+
// A concurrent request may already have reached a definitive outcome;
|
|
427
|
+
// replay it rather than holding the caller on a finished operation.
|
|
428
|
+
// A failed read proves nothing about the winner, so it stays uncertain:
|
|
429
|
+
// reporting a definitive error here would let the caller open a
|
|
430
|
+
// replacement operation while this one may already have spent.
|
|
431
|
+
let winner: ReturnType<typeof readGrokResetCouponTerminalReplay> = null;
|
|
432
|
+
try { winner = readGrokResetCouponTerminalReplay(effectiveOpId, accountId, resolvedTokenId); } catch { winner = null; }
|
|
433
|
+
if (winner) return jsonResponse({
|
|
434
|
+
code: winner.code, replayed: true, tokenId: winner.tokenId, settledAt: winner.settledAt,
|
|
435
|
+
}, 200, req, config);
|
|
436
|
+
return jsonResponse({ error: {
|
|
437
|
+
code: "attempt_in_progress", message: "Another request already claimed this operation; no redemption was attempted",
|
|
438
|
+
} }, 409, req, config);
|
|
439
|
+
}
|
|
440
|
+
} catch (err) {
|
|
441
|
+
return jsonResponse(
|
|
442
|
+
{ error: { code: "attempt_mark_failed", message: err instanceof Error ? err.message : String(err) } },
|
|
443
|
+
500,
|
|
444
|
+
req,
|
|
445
|
+
config,
|
|
446
|
+
);
|
|
447
|
+
}
|
|
249
448
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
})
|
|
449
|
+
try {
|
|
450
|
+
await redeemGrokResetCoupon({
|
|
451
|
+
accessToken: tokenSnapshot.accessToken,
|
|
452
|
+
tokenId: resolvedTokenId,
|
|
453
|
+
});
|
|
454
|
+
} catch (err) {
|
|
455
|
+
// A transport error cannot prove that the irreversible request failed.
|
|
456
|
+
// Keep "attempted" so a later retry can inspect without dispatching again.
|
|
457
|
+
return jsonResponse(
|
|
458
|
+
{ operationId: effectiveOpId, accountId, tokenId: resolvedTokenId, error: {
|
|
459
|
+
code: "attempt_unresolved",
|
|
460
|
+
message: "Redemption delivery is unconfirmed; reuse this operationId to inspect its state and do not create a replacement attempt",
|
|
461
|
+
} },
|
|
462
|
+
502,
|
|
463
|
+
req,
|
|
464
|
+
config,
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// A confirmed redemption remains successful even when settlement fails.
|
|
469
|
+
// The existing claim protects retries if settlement does not land.
|
|
470
|
+
let settlementRecorded = false;
|
|
471
|
+
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
472
|
+
try {
|
|
473
|
+
settlementRecorded = recordGrokResetCouponSettlement({
|
|
474
|
+
operationId: effectiveOpId,
|
|
475
|
+
accountId,
|
|
476
|
+
tokenId: resolvedTokenId,
|
|
477
|
+
code: "redeemed",
|
|
478
|
+
status: "success",
|
|
479
|
+
expectedStatus: "attempted",
|
|
480
|
+
});
|
|
481
|
+
break;
|
|
482
|
+
} catch (error) {
|
|
483
|
+
// Retry only transient acquisition contention, never an unsafe or
|
|
484
|
+
// unreadable ledger, a changed claim, or the upstream redemption.
|
|
485
|
+
const cause = error instanceof ConfigMutationLockError ? error.cause : undefined;
|
|
486
|
+
if (!cause || typeof cause !== "object" || !("code" in cause)
|
|
487
|
+
|| cause.code !== "SQLITE_BUSY" || attempt === 4) break;
|
|
488
|
+
await Bun.sleep(20);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
256
491
|
|
|
257
492
|
return jsonResponse(
|
|
258
493
|
{
|
|
@@ -262,24 +497,14 @@ export async function handleGrokCouponRoutes(ctx: ManagementContext): Promise<Re
|
|
|
262
497
|
tokenId: resolvedTokenId,
|
|
263
498
|
accountId,
|
|
264
499
|
operationId: effectiveOpId,
|
|
500
|
+
settlementRecorded,
|
|
265
501
|
},
|
|
266
502
|
200,
|
|
267
503
|
req,
|
|
268
504
|
config,
|
|
269
505
|
);
|
|
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
|
-
);
|
|
506
|
+
} finally {
|
|
507
|
+
grokCouponInFlightAttempts.delete(effectiveOpId);
|
|
283
508
|
}
|
|
284
509
|
}
|
|
285
510
|
|