@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
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import {
|
|
1
|
+
import { closeSync, fsyncSync, openSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { atomicWriteFileStreamed } from "../config/atomic-write";
|
|
4
|
+
import { withConfigMutationLockSync } from "../config/mutation-lock";
|
|
4
5
|
import { getConfigDir } from "../config/paths";
|
|
5
6
|
|
|
6
|
-
export type GrokResetCouponOperationKind = "execute" | "replay" | "identity-mismatch" | "capacity";
|
|
7
|
+
export type GrokResetCouponOperationKind = "execute" | "replay" | "identity-mismatch" | "token-mismatch" | "capacity";
|
|
7
8
|
|
|
8
9
|
export interface GrokResetCouponOperationIdentity {
|
|
9
10
|
accountId: string;
|
|
@@ -16,21 +17,33 @@ export interface GrokResetCouponOperationRecord {
|
|
|
16
17
|
operationId: string;
|
|
17
18
|
accountId?: string;
|
|
18
19
|
tokenId?: string;
|
|
20
|
+
/** Token expiry (ms) captured at claim time as journal metadata. Remaining
|
|
21
|
+
* availability or expiry cannot establish this operation's outcome. */
|
|
22
|
+
tokenValidityEnd?: number;
|
|
23
|
+
/** When the attempt was marked (ms) — set only on an "attempted" replay so
|
|
24
|
+
* the route can defer inspection while the attempt may still be in flight. */
|
|
25
|
+
attemptedAt?: number;
|
|
19
26
|
code?: string;
|
|
20
27
|
settledAt?: number;
|
|
28
|
+
/** Durable status of a replayed record, so callers apply the terminal-replay predicate. */
|
|
29
|
+
status?: "attempted" | "settled" | "failed";
|
|
21
30
|
}
|
|
22
31
|
|
|
23
32
|
interface GrokResetCouponOperationState {
|
|
24
33
|
accountId: string;
|
|
25
34
|
tokenId?: string;
|
|
26
|
-
|
|
35
|
+
// "attempted" sits between open and settled: the spend call may have fired,
|
|
36
|
+
// so the operation must never execute again. Its replay carries no code —
|
|
37
|
+
// the route reconciles it against upstream instead of trusting a status.
|
|
38
|
+
status: "open" | "attempted" | "settled" | "failed";
|
|
27
39
|
code?: string;
|
|
40
|
+
tokenValidityEnd?: number;
|
|
28
41
|
createdAt: number;
|
|
29
42
|
updatedAt: number;
|
|
30
43
|
}
|
|
31
44
|
|
|
32
45
|
interface GrokResetCouponLedger {
|
|
33
|
-
version:
|
|
46
|
+
version: 2;
|
|
34
47
|
operations: Record<string, GrokResetCouponOperationState>;
|
|
35
48
|
}
|
|
36
49
|
|
|
@@ -40,95 +53,317 @@ export function grokCouponJournalPath(customDir?: string): string {
|
|
|
40
53
|
}
|
|
41
54
|
|
|
42
55
|
function readGrokCouponLedger(filePath: string): GrokResetCouponLedger {
|
|
43
|
-
|
|
44
|
-
|
|
56
|
+
let raw: string;
|
|
57
|
+
try { raw = readFileSync(filePath, "utf-8"); }
|
|
58
|
+
catch (error) {
|
|
59
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return { version: 2, operations: {} };
|
|
60
|
+
throw new Error("Grok coupon ledger is unavailable; existing records were preserved.");
|
|
45
61
|
}
|
|
46
62
|
try {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
63
|
+
const parsed = JSON.parse(raw) as Omit<GrokResetCouponLedger, "version"> & { version: unknown };
|
|
64
|
+
if (!parsed || (parsed.version !== 1 && parsed.version !== 2) || !parsed.operations
|
|
65
|
+
|| typeof parsed.operations !== "object" || Array.isArray(parsed.operations)) throw new Error();
|
|
66
|
+
for (const op of Object.values(parsed.operations)) {
|
|
67
|
+
if (!op || typeof op.accountId !== "string" || !op.accountId
|
|
68
|
+
|| !["open", "attempted", "settled", "failed"].includes(op.status)
|
|
69
|
+
|| !Number.isFinite(op.createdAt) || !Number.isFinite(op.updatedAt)
|
|
70
|
+
|| (op.tokenId !== undefined && typeof op.tokenId !== "string")
|
|
71
|
+
|| (op.code !== undefined && typeof op.code !== "string")
|
|
72
|
+
|| (op.tokenValidityEnd !== undefined && !Number.isFinite(op.tokenValidityEnd))) throw new Error();
|
|
73
|
+
}
|
|
74
|
+
if (parsed.version === 1) {
|
|
75
|
+
for (const op of Object.values(parsed.operations)) {
|
|
76
|
+
// Old opens (and transport failures recorded as failed) may already
|
|
77
|
+
// have dispatched. Only version-two opens prove pre-dispatch state.
|
|
78
|
+
if (op.status === "open" || (op.status === "failed" && op.code === "redeem_failed")) {
|
|
79
|
+
op.status = "attempted";
|
|
80
|
+
delete op.code;
|
|
81
|
+
}
|
|
82
|
+
// A v1 attempted record never carried a confirmed outcome.
|
|
83
|
+
if (op.status === "attempted") delete op.code;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Only a settled or failed record carries an outcome. An open or attempted
|
|
87
|
+
// record with a code would replay an unconfirmed result as definitive.
|
|
88
|
+
for (const op of Object.values(parsed.operations)) {
|
|
89
|
+
if ((op.status === "open" || op.status === "attempted") && op.code !== undefined) throw new Error();
|
|
90
|
+
}
|
|
91
|
+
return { version: 2, operations: parsed.operations };
|
|
52
92
|
} catch {
|
|
53
|
-
|
|
93
|
+
throw new Error("Grok coupon ledger is unreadable; existing records were preserved.");
|
|
54
94
|
}
|
|
55
95
|
}
|
|
56
96
|
|
|
57
97
|
function writeGrokCouponLedger(filePath: string, ledger: GrokResetCouponLedger, now = Date.now()): void {
|
|
58
|
-
// Prune
|
|
98
|
+
// Prune every record past the 30-day window — including "open" and
|
|
99
|
+
// "attempted". Idempotency retention is explicitly bounded to 30 days;
|
|
100
|
+
// operations are not retry authorities after that window.
|
|
59
101
|
const retentionCutoff = now - 30 * 24 * 60 * 60_000;
|
|
60
102
|
ledger.operations = Object.fromEntries(
|
|
61
|
-
Object.entries(ledger.operations).filter(
|
|
62
|
-
([, op]) => op.status === "open" || op.updatedAt > retentionCutoff,
|
|
63
|
-
),
|
|
103
|
+
Object.entries(ledger.operations).filter(([, op]) => op.updatedAt > retentionCutoff),
|
|
64
104
|
);
|
|
65
|
-
|
|
105
|
+
atomicWriteFileStreamed(filePath, descriptor => writeFileSync(descriptor, JSON.stringify(ledger, null, 2)), {
|
|
106
|
+
afterRename: target => {
|
|
107
|
+
// The shared streamed writer flushes the temp. A spend claim must also
|
|
108
|
+
// refuse on a POSIX directory-sync failure before dispatch, rather than
|
|
109
|
+
// relying on that writer's best-effort parent sync. Windows has no
|
|
110
|
+
// portable directory descriptor and retains the writer's platform contract.
|
|
111
|
+
if (process.platform === "win32") return;
|
|
112
|
+
const descriptor = openSync(dirname(target), "r");
|
|
113
|
+
try { fsyncSync(descriptor); }
|
|
114
|
+
finally { closeSync(descriptor); }
|
|
115
|
+
},
|
|
116
|
+
});
|
|
66
117
|
}
|
|
67
118
|
|
|
68
119
|
const MAX_GROK_RESET_COUPON_OPERATION_IDS = 256;
|
|
69
120
|
|
|
121
|
+
/** Resolve an omitted-account retry from its durable identity, within retention. */
|
|
122
|
+
export function getGrokResetCouponOperationAccountId(
|
|
123
|
+
operationId: string,
|
|
124
|
+
now = Date.now(),
|
|
125
|
+
journalPath?: string,
|
|
126
|
+
): string | undefined {
|
|
127
|
+
return withConfigMutationLockSync(() => {
|
|
128
|
+
const record = readGrokCouponLedger(journalPath ?? grokCouponJournalPath()).operations[operationId];
|
|
129
|
+
if (!record || record.updatedAt <= now - 30 * 24 * 60 * 60_000) return undefined;
|
|
130
|
+
return record.accountId;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
70
134
|
export function openGrokResetCouponOperation(
|
|
71
135
|
identity: GrokResetCouponOperationIdentity,
|
|
72
136
|
now = Date.now(),
|
|
73
137
|
journalPath?: string,
|
|
74
138
|
): GrokResetCouponOperationRecord {
|
|
75
|
-
|
|
76
|
-
|
|
139
|
+
// Read→decide→write under the shared config mutation lock: two concurrent
|
|
140
|
+
// coupon requests on the same home must not lose an operation record — the
|
|
141
|
+
// ledger exists precisely to make an irreversible spend replay-safe.
|
|
142
|
+
return withConfigMutationLockSync(() => {
|
|
143
|
+
const filePath = journalPath ?? grokCouponJournalPath();
|
|
144
|
+
const ledger = readGrokCouponLedger(filePath);
|
|
77
145
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
146
|
+
// Prune BEFORE the capacity check: expiry is enforced on write, so a ledger
|
|
147
|
+
// full of stale records would otherwise reject every new operation forever
|
|
148
|
+
// (the prune path inside writeGrokCouponLedger is never reached when all
|
|
149
|
+
// openings are rejected).
|
|
150
|
+
const retentionCutoff = now - 30 * 24 * 60 * 60_000;
|
|
151
|
+
ledger.operations = Object.fromEntries(
|
|
152
|
+
Object.entries(ledger.operations).filter(([, op]) => op.updatedAt > retentionCutoff),
|
|
153
|
+
);
|
|
81
154
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
155
|
+
const existing = ledger.operations[identity.operationId];
|
|
156
|
+
if (existing) {
|
|
157
|
+
if (existing.accountId !== identity.accountId) {
|
|
158
|
+
return { kind: "identity-mismatch", operationId: identity.operationId };
|
|
159
|
+
}
|
|
160
|
+
if (identity.tokenId !== undefined && existing.tokenId !== undefined && existing.tokenId !== identity.tokenId) {
|
|
161
|
+
return { kind: "token-mismatch", operationId: identity.operationId };
|
|
162
|
+
}
|
|
163
|
+
if (existing.status !== "open") {
|
|
164
|
+
// Non-open: replay the recorded outcome. "attempted" records carry no
|
|
165
|
+
// code — the caller must reconcile them against upstream, never re-run.
|
|
166
|
+
return {
|
|
167
|
+
kind: "replay",
|
|
168
|
+
operationId: identity.operationId,
|
|
169
|
+
accountId: existing.accountId,
|
|
170
|
+
tokenId: existing.tokenId,
|
|
171
|
+
tokenValidityEnd: existing.tokenValidityEnd,
|
|
172
|
+
attemptedAt: existing.status === "attempted" ? existing.updatedAt : undefined,
|
|
173
|
+
code: existing.code,
|
|
174
|
+
settledAt: existing.updatedAt,
|
|
175
|
+
status: existing.status,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
90
178
|
return {
|
|
91
|
-
kind: "
|
|
179
|
+
kind: "execute",
|
|
92
180
|
operationId: identity.operationId,
|
|
93
181
|
accountId: existing.accountId,
|
|
94
182
|
tokenId: existing.tokenId,
|
|
95
|
-
code: existing.code,
|
|
96
|
-
settledAt: existing.updatedAt,
|
|
97
183
|
};
|
|
98
184
|
}
|
|
185
|
+
|
|
186
|
+
if (Object.keys(ledger.operations).length >= MAX_GROK_RESET_COUPON_OPERATION_IDS) {
|
|
187
|
+
return { kind: "capacity", operationId: identity.operationId };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
ledger.operations[identity.operationId] = {
|
|
191
|
+
accountId: identity.accountId,
|
|
192
|
+
...(identity.tokenId === undefined ? {} : { tokenId: identity.tokenId }),
|
|
193
|
+
status: "open",
|
|
194
|
+
createdAt: now,
|
|
195
|
+
updatedAt: now,
|
|
196
|
+
};
|
|
197
|
+
writeGrokCouponLedger(filePath, ledger, now);
|
|
99
198
|
return {
|
|
100
199
|
kind: "execute",
|
|
101
200
|
operationId: identity.operationId,
|
|
102
|
-
accountId:
|
|
103
|
-
tokenId:
|
|
201
|
+
accountId: identity.accountId,
|
|
202
|
+
tokenId: identity.tokenId,
|
|
104
203
|
};
|
|
105
|
-
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
106
206
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
207
|
+
/**
|
|
208
|
+
* Flip an open operation to `attempted` BEFORE the upstream spend call. The
|
|
209
|
+
* write happens while nothing is irreversible yet: if it throws, the caller
|
|
210
|
+
* aborts without spending; if a later settle write dies instead, the
|
|
211
|
+
* operation still refuses blind re-execution — the next open returns a
|
|
212
|
+
* code-less replay the route resolves via the upstream remaining-resets list.
|
|
213
|
+
*/
|
|
214
|
+
export function markGrokResetCouponAttempt(
|
|
215
|
+
operationId: string,
|
|
216
|
+
tokenId: string,
|
|
217
|
+
now = Date.now(),
|
|
218
|
+
journalPath?: string,
|
|
219
|
+
tokenValidityEnd?: number,
|
|
220
|
+
): boolean {
|
|
221
|
+
return withConfigMutationLockSync(() => {
|
|
222
|
+
const filePath = journalPath ?? grokCouponJournalPath();
|
|
223
|
+
const ledger = readGrokCouponLedger(filePath);
|
|
224
|
+
const existing = ledger.operations[operationId];
|
|
225
|
+
if (!existing || existing.status !== "open"
|
|
226
|
+
|| (existing.tokenId !== undefined && existing.tokenId !== tokenId)) return false;
|
|
227
|
+
existing.status = "attempted";
|
|
228
|
+
existing.tokenId = tokenId;
|
|
229
|
+
if (tokenValidityEnd !== undefined) existing.tokenValidityEnd = tokenValidityEnd;
|
|
230
|
+
existing.updatedAt = now;
|
|
231
|
+
writeGrokCouponLedger(filePath, ledger, now);
|
|
232
|
+
return true;
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
type GrokResetCouponSettlement = { operationId: string; accountId: string; code: string } & (
|
|
237
|
+
| { tokenId?: string; status: "failed"; expectedStatus: "open" }
|
|
238
|
+
| { tokenId: string; status: "success"; expectedStatus: "attempted" }
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
type GrokResetCouponPreflightResult =
|
|
242
|
+
| { kind: "recorded" | "changed" }
|
|
243
|
+
| { kind: "replay"; code: string; tokenId?: string; settledAt: number };
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Codes that never prove an outcome. A record carrying one is not replayed as
|
|
247
|
+
* definitive, even if a restored or hand-edited ledger stores it as settled/failed.
|
|
248
|
+
*/
|
|
249
|
+
const UNRESOLVED_COUPON_CODES = new Set([
|
|
250
|
+
"redeem_failed", "attempt_unresolved", "attempt_in_progress", "attempt_reconcile_failed", "operation_state_changed",
|
|
251
|
+
"attempt_mark_failed", "operation_token_mismatch", "ledger_unavailable", "capacity",
|
|
252
|
+
]);
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Whether a durable record is a definitive outcome this account and token may replay:
|
|
256
|
+
* settled or failed, same account, same token when both are known, and a code that
|
|
257
|
+
* does not itself mean "unconfirmed".
|
|
258
|
+
*/
|
|
259
|
+
export function isTerminalGrokCouponReplay(
|
|
260
|
+
record: { status?: string; accountId?: string; tokenId?: string; code?: string } | undefined,
|
|
261
|
+
accountId: string, tokenId: string | undefined,
|
|
262
|
+
): boolean {
|
|
263
|
+
if (!record || record.accountId !== accountId) return false;
|
|
264
|
+
if (tokenId !== undefined && record.tokenId !== undefined && record.tokenId !== tokenId) return false;
|
|
265
|
+
return (record.status === "settled" || record.status === "failed")
|
|
266
|
+
&& !!record.code && !UNRESOLVED_COUPON_CODES.has(record.code);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** The durable definitive outcome for this account and token, if the operation has one. */
|
|
270
|
+
function terminalReplay(
|
|
271
|
+
ledger: GrokResetCouponLedger, operationId: string, accountId: string, tokenId: string | undefined,
|
|
272
|
+
): Extract<GrokResetCouponPreflightResult, { kind: "replay" }> | null {
|
|
273
|
+
const existing = ledger.operations[operationId];
|
|
274
|
+
if (!existing || !isTerminalGrokCouponReplay(existing, accountId, tokenId)) return null;
|
|
275
|
+
return { kind: "replay", code: existing.code!, tokenId: existing.tokenId, settledAt: existing.updatedAt };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Settle a refusal or inspect its winner without releasing the mutation transaction. */
|
|
279
|
+
export function settleGrokResetCouponPreflightRefusal(
|
|
280
|
+
settlement: Extract<GrokResetCouponSettlement, { status: "failed" }>,
|
|
281
|
+
now = Date.now(),
|
|
282
|
+
journalPath?: string,
|
|
283
|
+
): GrokResetCouponPreflightResult {
|
|
284
|
+
return withConfigMutationLockSync(() => {
|
|
285
|
+
const filePath = journalPath ?? grokCouponJournalPath();
|
|
286
|
+
const ledger = readGrokCouponLedger(filePath);
|
|
287
|
+
if (recordSettlement(ledger, filePath, settlement, now)) return { kind: "recorded" };
|
|
288
|
+
return terminalReplay(ledger, settlement.operationId, settlement.accountId, settlement.tokenId) ?? { kind: "changed" };
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export type GrokResetCouponPreOpenRefusalResult =
|
|
293
|
+
| GrokResetCouponPreflightResult
|
|
294
|
+
| { kind: "identity-mismatch" | "capacity" };
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Refuse a request that never opened its supplied operationId. Under the mutation lock
|
|
298
|
+
* an absent id is reserved as a failed record, so a later open of the same id replays
|
|
299
|
+
* the refusal and can never be claimed (within the 30-day retention window). An id
|
|
300
|
+
* that already exists is never overwritten: a definitive outcome replays, anything
|
|
301
|
+
* else reports "changed" so the caller keeps its hold.
|
|
302
|
+
*/
|
|
303
|
+
export function refuseGrokResetCouponBeforeOpen(
|
|
304
|
+
refusal: { operationId: string; accountId: string; tokenId?: string; code: string },
|
|
305
|
+
now = Date.now(),
|
|
306
|
+
journalPath?: string,
|
|
307
|
+
): GrokResetCouponPreOpenRefusalResult {
|
|
308
|
+
return withConfigMutationLockSync(() => {
|
|
309
|
+
const filePath = journalPath ?? grokCouponJournalPath();
|
|
310
|
+
const ledger = readGrokCouponLedger(filePath);
|
|
311
|
+
const retentionCutoff = now - 30 * 24 * 60 * 60_000;
|
|
312
|
+
ledger.operations = Object.fromEntries(
|
|
313
|
+
Object.entries(ledger.operations).filter(([, op]) => op.updatedAt > retentionCutoff),
|
|
314
|
+
);
|
|
315
|
+
const existing = ledger.operations[refusal.operationId];
|
|
316
|
+
if (existing) {
|
|
317
|
+
if (existing.accountId !== refusal.accountId) return { kind: "identity-mismatch" };
|
|
318
|
+
// Same token binding as a normal replay: a different requested coupon never
|
|
319
|
+
// replays another coupon's outcome.
|
|
320
|
+
return terminalReplay(ledger, refusal.operationId, refusal.accountId, refusal.tokenId) ?? { kind: "changed" };
|
|
321
|
+
}
|
|
322
|
+
if (Object.keys(ledger.operations).length >= MAX_GROK_RESET_COUPON_OPERATION_IDS) return { kind: "capacity" };
|
|
323
|
+
ledger.operations[refusal.operationId] = {
|
|
324
|
+
accountId: refusal.accountId, ...(refusal.tokenId !== undefined ? { tokenId: refusal.tokenId } : {}),
|
|
325
|
+
status: "failed", code: refusal.code, createdAt: now, updatedAt: now,
|
|
326
|
+
};
|
|
327
|
+
writeGrokCouponLedger(filePath, ledger, now);
|
|
328
|
+
return { kind: "recorded" };
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* After a lost claim: the definitive winner to replay, or null while the
|
|
334
|
+
* operation is still unresolved. Terminal states never revert, so reading after
|
|
335
|
+
* the failed claim cannot replay an outcome that is later replaced.
|
|
336
|
+
*/
|
|
337
|
+
export function readGrokResetCouponTerminalReplay(
|
|
338
|
+
operationId: string, accountId: string, tokenId?: string, journalPath?: string,
|
|
339
|
+
): Extract<GrokResetCouponPreflightResult, { kind: "replay" }> | null {
|
|
340
|
+
return withConfigMutationLockSync(() => {
|
|
341
|
+
const filePath = journalPath ?? grokCouponJournalPath();
|
|
342
|
+
return terminalReplay(readGrokCouponLedger(filePath), operationId, accountId, tokenId);
|
|
343
|
+
});
|
|
121
344
|
}
|
|
122
345
|
|
|
346
|
+
/** A preflight refusal cannot replace a claim or a terminal result. */
|
|
123
347
|
export function recordGrokResetCouponSettlement(
|
|
124
|
-
settlement:
|
|
348
|
+
settlement: GrokResetCouponSettlement,
|
|
125
349
|
now = Date.now(),
|
|
126
350
|
journalPath?: string,
|
|
127
|
-
):
|
|
128
|
-
|
|
129
|
-
|
|
351
|
+
): boolean {
|
|
352
|
+
return withConfigMutationLockSync(() => {
|
|
353
|
+
const filePath = journalPath ?? grokCouponJournalPath();
|
|
354
|
+
const ledger = readGrokCouponLedger(filePath);
|
|
355
|
+
return recordSettlement(ledger, filePath, settlement, now);
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function recordSettlement(
|
|
360
|
+
ledger: GrokResetCouponLedger, filePath: string, settlement: GrokResetCouponSettlement, now: number,
|
|
361
|
+
): boolean {
|
|
130
362
|
const existing = ledger.operations[settlement.operationId];
|
|
131
|
-
if (!existing
|
|
363
|
+
if (!existing || existing.status !== settlement.expectedStatus
|
|
364
|
+
|| existing.accountId !== settlement.accountId
|
|
365
|
+
|| (settlement.tokenId !== undefined && existing.tokenId !== undefined && existing.tokenId !== settlement.tokenId)
|
|
366
|
+
|| (settlement.status === "success" && existing.tokenId !== settlement.tokenId)) return false;
|
|
132
367
|
|
|
133
368
|
existing.status = settlement.status === "success" ? "settled" : "failed";
|
|
134
369
|
existing.code = settlement.code;
|
|
@@ -136,4 +371,5 @@ export function recordGrokResetCouponSettlement(
|
|
|
136
371
|
existing.updatedAt = now;
|
|
137
372
|
|
|
138
373
|
writeGrokCouponLedger(filePath, ledger, now);
|
|
374
|
+
return true;
|
|
139
375
|
}
|
|
@@ -230,6 +230,19 @@ function buildGrokHeaders(accessToken: string): Record<string, string> {
|
|
|
230
230
|
/**
|
|
231
231
|
* Reads available Grok reset tokens for the authenticated xAI account.
|
|
232
232
|
*/
|
|
233
|
+
function assertConfirmedCouponStatus(decoded: ReturnType<typeof decodeGrpcWebResponse>): void {
|
|
234
|
+
const rawStatus = decoded.statusValues[0];
|
|
235
|
+
// The shared decoder defaults a missing/malformed status to zero. Coupon
|
|
236
|
+
// availability and an irreversible redemption need affirmative confirmation.
|
|
237
|
+
if (decoded.framingComplete !== true || rawStatus === undefined
|
|
238
|
+
|| decoded.statusValues.some((value) => !/^\d+$/.test(value)
|
|
239
|
+
|| !Number.isSafeInteger(Number(value)) || value !== rawStatus)) {
|
|
240
|
+
throw new Error("Grok coupon response has no confirmed gRPC status");
|
|
241
|
+
}
|
|
242
|
+
const status = Number(rawStatus);
|
|
243
|
+
if (status !== 0) throw new GrpcWebError(status, decoded.statusMessage ?? "Unknown gRPC error");
|
|
244
|
+
}
|
|
245
|
+
|
|
233
246
|
export async function getGrokRemainingResets(options: GetRemainingResetsOptions): Promise<{ tokens: GrokResetCoupon[] }> {
|
|
234
247
|
const fetchImpl = options.fetchFn ?? globalThis.fetch;
|
|
235
248
|
const endpoint = options.endpoint ?? GROK_GET_REMAINING_RESETS_ENDPOINT;
|
|
@@ -251,9 +264,7 @@ export async function getGrokRemainingResets(options: GetRemainingResetsOptions)
|
|
|
251
264
|
const rawBytes = new Uint8Array(await res.arrayBuffer());
|
|
252
265
|
const decoded = decodeGrpcWebResponse(rawBytes);
|
|
253
266
|
|
|
254
|
-
|
|
255
|
-
throw new GrpcWebError(decoded.status, decoded.statusMessage ?? "Unknown gRPC error");
|
|
256
|
-
}
|
|
267
|
+
assertConfirmedCouponStatus(decoded);
|
|
257
268
|
|
|
258
269
|
if (decoded.messages.length === 0) {
|
|
259
270
|
return { tokens: [] };
|
|
@@ -285,9 +296,7 @@ export async function redeemGrokResetCoupon(options: RedeemResetOptions): Promis
|
|
|
285
296
|
const rawBytes = new Uint8Array(await res.arrayBuffer());
|
|
286
297
|
const decoded = decodeGrpcWebResponse(rawBytes);
|
|
287
298
|
|
|
288
|
-
|
|
289
|
-
throw new GrpcWebError(decoded.status, decoded.statusMessage ?? "Unknown gRPC error");
|
|
290
|
-
}
|
|
299
|
+
assertConfirmedCouponStatus(decoded);
|
|
291
300
|
|
|
292
301
|
return {
|
|
293
302
|
success: true,
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Detection is read-only and injectable: the proxy never writes anything into a Cursor
|
|
11
11
|
* install, its state database, or its keychain entries (the T20 exclusion in
|
|
12
|
-
* devlog/
|
|
12
|
+
* devlog/_fin/260822_senpi_cursor_transfer/090), and the tests run against a temp tree.
|
|
13
13
|
*/
|
|
14
14
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
15
15
|
import { homedir } from "node:os";
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
assertDroidRecordedSettingsUnambiguous,
|
|
33
33
|
isLoopbackOnly,
|
|
34
34
|
resolveIntegrationPaths,
|
|
35
|
+
type ResolvedIntegrationPaths,
|
|
35
36
|
restoreOwnershipCollision,
|
|
36
37
|
type IntegrationClientId,
|
|
37
38
|
} from "./registry";
|
|
@@ -567,7 +568,7 @@ export function restoreStoreDirectoryRefusal(
|
|
|
567
568
|
if (!declared?.lockFile) return null;
|
|
568
569
|
let storePath: string;
|
|
569
570
|
try {
|
|
570
|
-
storePath = declared.path(input.env, input.home);
|
|
571
|
+
storePath = declared.path(input.env, input.home, io);
|
|
571
572
|
} catch (error) {
|
|
572
573
|
if (error instanceof ClientPathError) return null;
|
|
573
574
|
throw error;
|
|
@@ -602,10 +603,11 @@ export function observeRestore(
|
|
|
602
603
|
const store = selectedOperation?.store ?? input.store ?? createIntegrationStateStore();
|
|
603
604
|
let io = input.io ?? defaultIntegrationIO(store);
|
|
604
605
|
const clientId = input.clientId;
|
|
605
|
-
let resolved:
|
|
606
|
+
let resolved: ResolvedIntegrationPaths;
|
|
606
607
|
try {
|
|
607
608
|
const context = exportContextOf(input);
|
|
608
|
-
resolved =
|
|
609
|
+
resolved = clientId === "zcode" || !input.resolvedPaths
|
|
610
|
+
? resolveIntegrationPaths(clientId, input.env, input.home, context, io) : input.resolvedPaths;
|
|
609
611
|
if (clientId === "droid" && input.resolvedPaths) assertDroidPathsUnambiguous(resolved.detectDir, context);
|
|
610
612
|
} catch (error) {
|
|
611
613
|
if (!(error instanceof ClientPathError)) throw error;
|
|
@@ -630,7 +632,7 @@ export function observeRestore(
|
|
|
630
632
|
* config file. The answer also carries the document shape those bytes are in.
|
|
631
633
|
*/
|
|
632
634
|
const rowTarget = declaredIntegrationTarget({
|
|
633
|
-
clientId, configPath, resolvedConfigPath: resolved.configPath, env: input.env, home: input.home,
|
|
635
|
+
clientId, configPath, resolvedConfigPath: resolved.configPath, env: input.env, home: input.home, io,
|
|
634
636
|
});
|
|
635
637
|
if (rowTarget === null) {
|
|
636
638
|
return {
|
|
@@ -848,7 +850,7 @@ export interface IntegrationWriteInput {
|
|
|
848
850
|
store?: IntegrationStateStore;
|
|
849
851
|
io?: IntegrationIO;
|
|
850
852
|
/** Frozen once by the async coordinator; synchronous callers may omit it. */
|
|
851
|
-
resolvedPaths?:
|
|
853
|
+
resolvedPaths?: ResolvedIntegrationPaths;
|
|
852
854
|
}
|
|
853
855
|
|
|
854
856
|
/** A refusal in the planner's own vocabulary, so observation does not depend on the writer's result type. */
|
|
@@ -922,7 +924,8 @@ export function observeIntegration(
|
|
|
922
924
|
* verify account 1 was installed and then write account 0's catalog.
|
|
923
925
|
*/
|
|
924
926
|
const context = exportContextOf(input);
|
|
925
|
-
const resolved =
|
|
927
|
+
const resolved = clientId === "zcode" || !input.resolvedPaths
|
|
928
|
+
? resolveIntegrationPaths(clientId, input.env, input.home, context, io) : input.resolvedPaths;
|
|
926
929
|
if (clientId === "droid" && input.resolvedPaths) assertDroidPathsUnambiguous(resolved.detectDir, context);
|
|
927
930
|
detectDir = resolved.detectDir;
|
|
928
931
|
if (clientId === "cline") io = createClineIO(io, resolved.configPath, store, effects.recover);
|
|
@@ -945,9 +948,12 @@ export function observeIntegration(
|
|
|
945
948
|
* collection route.
|
|
946
949
|
*/
|
|
947
950
|
effective = resolveIntegrationTarget({
|
|
948
|
-
clientId, configPath: recordedPath, io, record: stored, env: input.env, home: input.home,
|
|
951
|
+
clientId, configPath: recordedPath, io, record: stored, env: input.env, home: input.home, resolvedPaths: resolved,
|
|
949
952
|
});
|
|
950
953
|
configPath = effective.configPath;
|
|
954
|
+
if (effective.pathError && operation !== "disable") {
|
|
955
|
+
return { failed: observationFailure("unsafe", "unsafe", effective.pathError) } as const;
|
|
956
|
+
}
|
|
951
957
|
} catch (error) {
|
|
952
958
|
if (error instanceof ClineTransactionError) {
|
|
953
959
|
return { failed: { ...observationFailure("unsafe", "unsafe", error.message, error.snapshotPath), residual: true } } as const;
|