@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.
Files changed (266) hide show
  1. package/gui/dist/assets/App-CZtqiGcw.js +56 -0
  2. package/gui/dist/assets/Tray-C9TGtwad.js +1 -0
  3. package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
  4. package/gui/dist/assets/index-CTm4l-HQ.js +94 -0
  5. package/gui/dist/assets/{quota-summary-DTBC8n7-.js → quota-summary-BbEeRfPY.js} +1 -1
  6. package/gui/dist/index.html +2 -2
  7. package/package.json +2 -2
  8. package/src/adapters/coding-agent/protocol.ts +15 -8
  9. package/src/adapters/cursor/catalog.ts +2 -2
  10. package/src/adapters/cursor/effort-map.ts +1 -1
  11. package/src/adapters/devin/live-models.ts +2 -2
  12. package/src/adapters/google-truncation.ts +22 -0
  13. package/src/adapters/google.ts +7 -3
  14. package/src/adapters/ollama-native.ts +47 -15
  15. package/src/adapters/openai-chat/messages.ts +8 -4
  16. package/src/adapters/openai-responses/empty-assistant.ts +32 -0
  17. package/src/adapters/openai-responses/passthrough.ts +5 -0
  18. package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
  19. package/src/adapters/openai-responses/reasoning.ts +11 -1
  20. package/src/adapters/openai-responses/request-strips.ts +10 -2
  21. package/src/adapters/qoder/adapter.ts +16 -14
  22. package/src/bridge/errors.ts +26 -1
  23. package/src/bridge.ts +1 -1
  24. package/src/claude/context-windows.ts +57 -4
  25. package/src/claude/desktop-picker.ts +1 -1
  26. package/src/claude/intercept/connect-proxy.ts +10 -3
  27. package/src/claude/intercept/picker-ca-persistence.ts +3 -1
  28. package/src/claude/intercept/picker-listener.ts +49 -1
  29. package/src/claude/intercept/settings-no-proxy.ts +146 -0
  30. package/src/claude/intercept/settings.ts +101 -9
  31. package/src/cli/account-api.ts +65 -0
  32. package/src/cli/account-auth.ts +44 -12
  33. package/src/cli/account.ts +41 -5
  34. package/src/cli/capabilities-access-remote.ts +8 -0
  35. package/src/cli/capabilities-base.ts +13 -1
  36. package/src/cli/catalog-command-result.ts +2 -1
  37. package/src/cli/claude.ts +15 -6
  38. package/src/cli/config-command.ts +8 -2
  39. package/src/cli/debug.ts +8 -0
  40. package/src/cli/dispatch.ts +16 -3
  41. package/src/cli/export-command.ts +14 -1
  42. package/src/cli/hub-disable.ts +99 -0
  43. package/src/cli/hub.ts +4 -2
  44. package/src/cli/index.ts +13 -4
  45. package/src/cli/link-child.ts +104 -0
  46. package/src/cli/link-enrollment.ts +96 -0
  47. package/src/cli/link.ts +39 -24
  48. package/src/cli/observe-stream.ts +6 -1
  49. package/src/cli/observe.ts +10 -2
  50. package/src/cli/provider.ts +12 -0
  51. package/src/cli/registry.ts +5 -4
  52. package/src/cli/resolve.ts +1 -1
  53. package/src/cli/runtime-api.ts +27 -2
  54. package/src/cli/status.ts +104 -20
  55. package/src/client/connect.ts +39 -4
  56. package/src/client/hub-client.ts +14 -7
  57. package/src/client/hub-state.ts +25 -14
  58. package/src/client/link-home.ts +248 -0
  59. package/src/client/link-management-relay.ts +61 -0
  60. package/src/client/link-relay.ts +6 -1
  61. package/src/client/link-status.ts +14 -0
  62. package/src/client/machine-api.ts +14 -7
  63. package/src/client/machine-listener.ts +29 -4
  64. package/src/clients/config-export/model-metadata.ts +16 -0
  65. package/src/clients/config-export/zcode-store.ts +46 -11
  66. package/src/clients/config-export/zcode.ts +3 -5
  67. package/src/clients/config-export.ts +47 -17
  68. package/src/clients/omo-role-models.ts +6 -3
  69. package/src/codex/agent-role-models.ts +26 -8
  70. package/src/codex/auth-api/account-list.ts +17 -2
  71. package/src/codex/auth-api/account-selection.ts +17 -7
  72. package/src/codex/auth-api/pool-mode-gate.ts +4 -5
  73. package/src/codex/auth-api/routes.ts +5 -1
  74. package/src/codex/auth-context.ts +8 -3
  75. package/src/codex/autostart-health.ts +3 -1
  76. package/src/codex/catalog/bundled.ts +5 -1
  77. package/src/codex/catalog/effort.ts +2 -2
  78. package/src/codex/catalog/metadata.ts +4 -2
  79. package/src/codex/catalog/native-models.ts +17 -3
  80. package/src/codex/catalog/provider-models.ts +25 -0
  81. package/src/codex/desktop-app/darwin.ts +1 -1
  82. package/src/codex/desktop-app/handoff.ts +1 -1
  83. package/src/codex/desktop-app/linux.ts +1 -1
  84. package/src/codex/desktop-app/lock.ts +1 -1
  85. package/src/codex/desktop-app/types.ts +1 -1
  86. package/src/codex/desktop-app/windows.ts +1 -1
  87. package/src/codex/desktop-app-restart.ts +1 -1
  88. package/src/codex/main-account.ts +6 -7
  89. package/src/codex/native-profile-startup.ts +4 -1
  90. package/src/codex/prompt-journal.ts +20 -2
  91. package/src/codex/prompt-layers/encoding.ts +49 -0
  92. package/src/codex/prompt-layers/import-source.ts +30 -0
  93. package/src/codex/prompt-layers/toml-edit.ts +269 -37
  94. package/src/codex/prompt-layers/toml-read.ts +52 -4
  95. package/src/codex/prompt-layers.ts +383 -20
  96. package/src/codex/prompt-lock-claim.ts +13 -3
  97. package/src/codex/prompt-lock-evidence.ts +3 -3
  98. package/src/codex/prompt-lock-owner.ts +14 -6
  99. package/src/codex/prompt-text-probe.ts +34 -5
  100. package/src/codex/quota.ts +13 -6
  101. package/src/codex/role-auto-assign.ts +2 -5
  102. package/src/codex/routing/active-account.ts +21 -0
  103. package/src/codex/routing.ts +1 -1
  104. package/src/codex/runtime.ts +196 -8
  105. package/src/codex/shim-overlay.ts +8 -4
  106. package/src/codex/shim.ts +1 -1
  107. package/src/combos/failover.ts +3 -0
  108. package/src/config/api-key-reconcile.ts +34 -0
  109. package/src/config/diagnostics.ts +16 -0
  110. package/src/config/feature-flags.ts +5 -0
  111. package/src/config/live-reconcile.ts +43 -29
  112. package/src/config/live-topology.ts +29 -0
  113. package/src/config/load-degrade.ts +2 -1
  114. package/src/config/paths.ts +3 -0
  115. package/src/config/salvage.ts +3 -1
  116. package/src/config/schema/config-schema.ts +2 -0
  117. package/src/generated/compatibility-version.json +373 -217
  118. package/src/grok/grpc-web.ts +23 -1
  119. package/src/grok/inject.ts +245 -221
  120. package/src/grok/reset-coupon-ledger.ts +293 -57
  121. package/src/grok/reset-coupons.ts +15 -6
  122. package/src/integrations/cursor-detect.ts +1 -1
  123. package/src/integrations/mutation-plan.ts +13 -7
  124. package/src/integrations/registry.ts +45 -11
  125. package/src/integrations/state.ts +19 -7
  126. package/src/integrations/target.ts +42 -5
  127. package/src/integrations/writer.ts +14 -6
  128. package/src/lib/app-owned-memory-stores.ts +4 -0
  129. package/src/lib/app-owned-memory.ts +11 -0
  130. package/src/lib/errors.ts +25 -0
  131. package/src/lib/json-byte-size.ts +31 -0
  132. package/src/lib/optional-shutdown-hooks.ts +35 -0
  133. package/src/lib/outbound-credential-registry.ts +210 -0
  134. package/src/lib/passive-read.ts +35 -0
  135. package/src/lib/redact.ts +2 -2
  136. package/src/lib/shadow-call.ts +44 -5
  137. package/src/link/codex-settings.ts +155 -0
  138. package/src/link/management-routes.ts +89 -0
  139. package/src/link/remote-probe.ts +38 -0
  140. package/src/link/ssh-argv.ts +26 -5
  141. package/src/link/ssh-runner.ts +58 -27
  142. package/src/link/status-projection.ts +22 -4
  143. package/src/link/supervisor.ts +144 -49
  144. package/src/link/tunnel-state.ts +11 -4
  145. package/src/oauth/anthropic-continuity.ts +4 -3
  146. package/src/oauth/anthropic-identity.ts +13 -2
  147. package/src/oauth/anthropic-model-quota.ts +13 -1
  148. package/src/oauth/anthropic-routing.ts +230 -23
  149. package/src/oauth/anthropic.ts +18 -4
  150. package/src/oauth/google-antigravity.ts +24 -7
  151. package/src/oauth/index.ts +25 -16
  152. package/src/oauth/kiro-account-identity.ts +106 -0
  153. package/src/oauth/kiro.ts +48 -5
  154. package/src/oauth/meta-muse-device.ts +1 -1
  155. package/src/oauth/plan.ts +29 -0
  156. package/src/oauth/store.ts +40 -3
  157. package/src/oauth/types.ts +3 -1
  158. package/src/plugins/loader.ts +11 -0
  159. package/src/providers/anthropic-billing-ownership.ts +74 -0
  160. package/src/providers/anthropic-billing-recovery.ts +31 -0
  161. package/src/providers/anthropic-billing-refusal.ts +55 -0
  162. package/src/providers/anthropic-fast.ts +1 -1
  163. package/src/providers/anthropic-reset-admission.ts +62 -0
  164. package/src/providers/anthropic-reset-grant-ledger.ts +29 -7
  165. package/src/providers/api-key-resolve.ts +17 -2
  166. package/src/providers/devin-cli-authmode-migration.ts +1 -1
  167. package/src/providers/devin-provider-merge-migration.ts +1 -1
  168. package/src/providers/github-copilot-auto.ts +169 -0
  169. package/src/providers/key-failover.ts +52 -5
  170. package/src/providers/model-presets.ts +1 -1
  171. package/src/providers/model-rename-startup.ts +5 -3
  172. package/src/providers/muse-subscription-usage.ts +1 -1
  173. package/src/providers/quota/account-cache.ts +11 -0
  174. package/src/providers/quota/anthropic-account-quota.ts +4 -2
  175. package/src/providers/quota/report-cache.ts +14 -0
  176. package/src/providers/quota/vendor-probes-oauth.ts +3 -3
  177. package/src/providers/quota.ts +4 -1
  178. package/src/providers/registry/entries-core.ts +20 -20
  179. package/src/providers/registry/entries-extended.ts +4 -1
  180. package/src/providers/registry/model-seeds.ts +20 -3
  181. package/src/providers/request-pacing.ts +9 -0
  182. package/src/providers/stale-no-vision-seed-migration.ts +72 -0
  183. package/src/providers/xai-fast-model.ts +1 -1
  184. package/src/responses/compaction.ts +14 -0
  185. package/src/responses/parser.ts +7 -1
  186. package/src/server/auth-cors.ts +42 -0
  187. package/src/server/configured-credentials.ts +194 -0
  188. package/src/server/inbound-body-admission.ts +24 -7
  189. package/src/server/index/link-listener.ts +30 -17
  190. package/src/server/index/link-management.ts +58 -0
  191. package/src/server/index/optional-listeners.ts +5 -1
  192. package/src/server/index/serve-options.ts +21 -2
  193. package/src/server/index.ts +1 -1
  194. package/src/server/lifecycle.ts +33 -5
  195. package/src/server/management/agent-settings-routes.ts +28 -10
  196. package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
  197. package/src/server/management/codex-agent-role-routes.ts +19 -6
  198. package/src/server/management/codex-prompt-routes.ts +136 -5
  199. package/src/server/management/codex-role-auto-assign.ts +1 -1
  200. package/src/server/management/grok-coupon-routes.ts +333 -57
  201. package/src/server/management/link-enrollment.ts +192 -0
  202. package/src/server/management/link-route-state.ts +185 -0
  203. package/src/server/management/link-routes.ts +92 -390
  204. package/src/server/management/model-rows.ts +3 -1
  205. package/src/server/management/provider-overwrite-carry.ts +10 -0
  206. package/src/server/management/provider-routes.ts +1 -0
  207. package/src/server/management/route-registry.ts +7 -5
  208. package/src/server/management/shared.ts +3 -0
  209. package/src/server/management-api.ts +3 -1
  210. package/src/server/messages-native.ts +68 -42
  211. package/src/server/relay-eager.ts +4 -0
  212. package/src/server/relay.ts +18 -7
  213. package/src/server/request-log-failure-attribution.ts +3 -0
  214. package/src/server/request-log-filter.ts +6 -1
  215. package/src/server/request-log-terminal-status.ts +243 -0
  216. package/src/server/request-log.ts +15 -118
  217. package/src/server/responses/adapter-continuation.ts +9 -1
  218. package/src/server/responses/adapter-dispatch.ts +293 -81
  219. package/src/server/responses/code-mode-collab-surface.ts +205 -0
  220. package/src/server/responses/codex-ws-exchange.ts +147 -6
  221. package/src/server/responses/codex-ws-wire.ts +203 -0
  222. package/src/server/responses/collaboration.ts +36 -7
  223. package/src/server/responses/compact.ts +129 -50
  224. package/src/server/responses/context-overflow.ts +13 -1
  225. package/src/server/responses/core-codex-account.ts +105 -1
  226. package/src/server/responses/core-combo-failure.ts +14 -0
  227. package/src/server/responses/core-combo.ts +7 -2
  228. package/src/server/responses/core-errors.ts +29 -1
  229. package/src/server/responses/core-opaque-recovery.ts +5 -5
  230. package/src/server/responses/core-options.ts +2 -0
  231. package/src/server/responses/core.ts +6 -6
  232. package/src/server/responses/deferred-collab-surface.ts +158 -0
  233. package/src/server/responses/deferred-send-reporter.ts +25 -0
  234. package/src/server/responses/empty-completion-guard.ts +18 -2
  235. package/src/server/responses/fetch-helpers.ts +12 -1
  236. package/src/server/responses/jev-model-invoke.ts +1 -1
  237. package/src/server/responses/passthrough-delivery.ts +4 -1
  238. package/src/server/responses/passthrough-dispatch.ts +130 -64
  239. package/src/server/responses/passthrough-execution.ts +3 -3
  240. package/src/server/responses/request-prepare.ts +6 -4
  241. package/src/server/responses/request-transport.ts +131 -13
  242. package/src/server/responses/run-turn-execution.ts +13 -5
  243. package/src/server/responses/sidecar-execution.ts +16 -2
  244. package/src/server/responses/spend-storage-error.ts +33 -0
  245. package/src/server/responses/ws-upstream.ts +9 -1
  246. package/src/server/startup-action-control.ts +22 -2
  247. package/src/server/startup-health-cache.ts +4 -0
  248. package/src/server/system-env-shell.ts +7 -2
  249. package/src/server/system-env.ts +11 -1
  250. package/src/service/cli.ts +5 -1
  251. package/src/service/desktop-startup.ts +147 -34
  252. package/src/service/desktop-supervision.d.mts +27 -1
  253. package/src/service/desktop-supervision.mjs +111 -5
  254. package/src/service/health-sentinel-launch.ts +176 -0
  255. package/src/service/health-sentinel-main.ts +447 -0
  256. package/src/service/health-sentinel-policy.ts +241 -0
  257. package/src/service/health-sentinel-probe.ts +54 -0
  258. package/src/service/health-sentinel-windows.ts +87 -0
  259. package/src/service/linux-linger.ts +20 -0
  260. package/src/types/config.ts +5 -0
  261. package/src/types/request.ts +5 -0
  262. package/src/usage/expected-prices.ts +5 -1
  263. package/src/usage/log.ts +19 -1
  264. package/gui/dist/assets/App-Ds_qL7o8.js +0 -54
  265. package/gui/dist/assets/Tray-BqHF5Trd.js +0 -1
  266. package/gui/dist/assets/index-D3FBCNiP.js +0 -94
@@ -1,9 +1,10 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { atomicWriteFile } from "../config/atomic-write";
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
- status: "open" | "settled" | "failed";
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: 1;
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
- if (!existsSync(filePath)) {
44
- return { version: 1, operations: {} };
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 raw = readFileSync(filePath, "utf-8");
48
- const parsed = JSON.parse(raw) as GrokResetCouponLedger;
49
- return parsed && parsed.version === 1 && parsed.operations && typeof parsed.operations === "object"
50
- ? parsed
51
- : { version: 1, operations: {} };
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
- return { version: 1, operations: {} };
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 settled/failed operations older than 30 days to avoid unbounded growth
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
- atomicWriteFile(filePath, JSON.stringify(ledger, null, 2));
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
- const filePath = journalPath ?? grokCouponJournalPath();
76
- const ledger = readGrokCouponLedger(filePath);
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
- if (Object.keys(ledger.operations).length >= MAX_GROK_RESET_COUPON_OPERATION_IDS) {
79
- return { kind: "capacity", operationId: identity.operationId };
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
- const existing = ledger.operations[identity.operationId];
83
- if (existing) {
84
- if (existing.accountId !== identity.accountId) {
85
- return { kind: "identity-mismatch", operationId: identity.operationId };
86
- }
87
- if (existing.status !== "open") {
88
- // Durably settled already: replay the recorded outcome instead of
89
- // trusting upstream idempotency for an irreversible spend.
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: "replay",
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: existing.accountId,
103
- tokenId: existing.tokenId,
201
+ accountId: identity.accountId,
202
+ tokenId: identity.tokenId,
104
203
  };
105
- }
204
+ });
205
+ }
106
206
 
107
- ledger.operations[identity.operationId] = {
108
- accountId: identity.accountId,
109
- ...(identity.tokenId === undefined ? {} : { tokenId: identity.tokenId }),
110
- status: "open",
111
- createdAt: now,
112
- updatedAt: now,
113
- };
114
- writeGrokCouponLedger(filePath, ledger, now);
115
- return {
116
- kind: "execute",
117
- operationId: identity.operationId,
118
- accountId: identity.accountId,
119
- tokenId: identity.tokenId,
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: { operationId: string; tokenId?: string; code: string; status: "success" | "failed" },
348
+ settlement: GrokResetCouponSettlement,
125
349
  now = Date.now(),
126
350
  journalPath?: string,
127
- ): void {
128
- const filePath = journalPath ?? grokCouponJournalPath();
129
- const ledger = readGrokCouponLedger(filePath);
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) return;
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
- if (decoded.status !== 0) {
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
- if (decoded.status !== 0) {
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/_plan/260822_senpi_cursor_transfer/090), and the tests run against a temp tree.
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: { configPath: string; detectDir: string };
606
+ let resolved: ResolvedIntegrationPaths;
606
607
  try {
607
608
  const context = exportContextOf(input);
608
- resolved = input.resolvedPaths ?? resolveIntegrationPaths(clientId, input.env, input.home, context);
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?: { configPath: string; detectDir: string };
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 = input.resolvedPaths ?? resolveIntegrationPaths(clientId, input.env, input.home, context);
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;