@bitkyc08/opencodex 2.63.0 → 2.64.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 (262) hide show
  1. package/AGENTS_INSTALL.md +4 -3
  2. package/README.md +46 -33
  3. package/assets/download-linux.svg +10 -0
  4. package/assets/download-macos.svg +10 -0
  5. package/assets/download-windows.svg +10 -0
  6. package/bin/ocx.mjs +12 -4
  7. package/gui/dist/assets/App-CpuDF3ci.js +50 -0
  8. package/gui/dist/assets/App-I5AnaSLh.css +1 -0
  9. package/gui/dist/assets/{Tray-CncKDBTp.js → Tray-B4uEIa1O.js} +1 -1
  10. package/gui/dist/assets/index-DiBRuK-d.css +1 -0
  11. package/gui/dist/assets/index-SggB6t3z.js +86 -0
  12. package/gui/dist/assets/usage-companion-chart-DLbKOJml.js +1 -0
  13. package/gui/dist/index.html +2 -2
  14. package/native/remote-workspace-helper/src/protocol.rs +5 -0
  15. package/package.json +4 -1
  16. package/src/adapters/anthropic.ts +66 -7
  17. package/src/adapters/codebuddy/adapter.ts +112 -16
  18. package/src/adapters/codebuddy/mcp-server.ts +180 -0
  19. package/src/adapters/codebuddy/scaffold-guard.ts +25 -8
  20. package/src/adapters/codebuddy/tool-bridge.ts +597 -0
  21. package/src/adapters/coding-agent/protocol.ts +123 -10
  22. package/src/adapters/coding-agent/turn.ts +324 -2
  23. package/src/adapters/command-code-restored-schema.ts +112 -0
  24. package/src/adapters/command-code-tool-text.ts +589 -0
  25. package/src/adapters/command-code.ts +80 -6
  26. package/src/adapters/cursor/catalog.ts +12 -0
  27. package/src/adapters/cursor/current-request.ts +46 -0
  28. package/src/adapters/cursor/discovery.ts +1 -1
  29. package/src/adapters/cursor/effort-map.ts +4 -0
  30. package/src/adapters/cursor/native-exec.ts +15 -0
  31. package/src/adapters/cursor/protobuf-events.ts +40 -12
  32. package/src/adapters/cursor/protobuf-request.ts +83 -18
  33. package/src/adapters/cursor/request-builder.ts +5 -4
  34. package/src/adapters/cursor/tool-guidance.ts +1 -1
  35. package/src/adapters/devin/live-models.ts +7 -0
  36. package/src/adapters/inline-think-tags.ts +251 -0
  37. package/src/adapters/kiro/adapter.ts +1 -0
  38. package/src/adapters/kiro/stream.ts +5 -3
  39. package/src/adapters/kiro/usage.ts +4 -3
  40. package/src/adapters/mimo-free.ts +37 -18
  41. package/src/adapters/openai-chat/messages.ts +5 -5
  42. package/src/adapters/openai-chat/tool-schema.ts +124 -2
  43. package/src/adapters/openai-chat.ts +13 -3
  44. package/src/adapters/openai-responses/passthrough.ts +15 -2
  45. package/src/adapters/openai-responses/reasoning.ts +14 -1
  46. package/src/adapters/openai-responses/request-strips.ts +29 -3
  47. package/src/adapters/openai-responses/tool-output-recovery.ts +9 -3
  48. package/src/adapters/qoder/adapter.ts +15 -12
  49. package/src/adapters/qoder/scaffold-guard.ts +45 -13
  50. package/src/bridge/internal.ts +20 -0
  51. package/src/bridge/sse.ts +14 -5
  52. package/src/claude/agents-inject.ts +2 -1
  53. package/src/claude/desktop-applied-marker.ts +44 -0
  54. package/src/claude/desktop-profile.ts +22 -12
  55. package/src/claude/inbound.ts +8 -3
  56. package/src/cli/access.ts +22 -5
  57. package/src/cli/account-api.ts +6 -0
  58. package/src/cli/account-auth.ts +5 -2
  59. package/src/cli/account.ts +5 -6
  60. package/src/cli/aside-profiles.ts +61 -1
  61. package/src/cli/claude-desktop.ts +3 -2
  62. package/src/cli/claude.ts +3 -1
  63. package/src/cli/codex-cli-update.ts +2 -2
  64. package/src/cli/codex-shim-autorestore.ts +3 -0
  65. package/src/cli/dispatch.ts +12 -4
  66. package/src/cli/index.ts +98 -10
  67. package/src/cli/registry.ts +1 -1
  68. package/src/cli/resolve.ts +113 -1
  69. package/src/cli/root.ts +5 -0
  70. package/src/cli/stop-approval.ts +186 -0
  71. package/src/cli/stop-report.ts +1 -1
  72. package/src/cli/system-restart-client.ts +19 -4
  73. package/src/client/hub-client.ts +25 -18
  74. package/src/clients/config-export.ts +12 -4
  75. package/src/codex/account-auto-switch.ts +59 -0
  76. package/src/codex/account-lifecycle.ts +7 -0
  77. package/src/codex/account-priority.ts +10 -0
  78. package/src/codex/account-usability.ts +9 -0
  79. package/src/codex/auth-api/account-list.ts +5 -0
  80. package/src/codex/auth-api/login-flow.ts +15 -5
  81. package/src/codex/auth-api/login-state.ts +5 -16
  82. package/src/codex/auth-api/pool-mode-gate.ts +30 -7
  83. package/src/codex/auth-api/routes.ts +39 -3
  84. package/src/codex/auth-context.ts +145 -17
  85. package/src/codex/catalog/build-entries.ts +2 -1
  86. package/src/codex/catalog/effort.ts +8 -2
  87. package/src/codex/catalog/metadata.ts +33 -7
  88. package/src/codex/catalog/native-models.ts +102 -5
  89. package/src/codex/catalog/parsing.ts +2 -0
  90. package/src/codex/catalog/provider-models.ts +19 -6
  91. package/src/codex/cli-installation-targets.ts +44 -40
  92. package/src/codex/codex-write-lock.ts +32 -2
  93. package/src/codex/inject/multi-agent-v2.ts +90 -0
  94. package/src/codex/inject/plan.ts +365 -0
  95. package/src/codex/inject.ts +237 -362
  96. package/src/codex/log-guard/maintenance.ts +30 -13
  97. package/src/codex/project-config-warnings.ts +47 -7
  98. package/src/codex/prompt-layers/encoding.ts +41 -0
  99. package/src/codex/prompt-layers/toml-read.ts +1 -1
  100. package/src/codex/prompt-layers.ts +17 -13
  101. package/src/codex/prompt-text-probe.ts +20 -10
  102. package/src/codex/quota-observation-freshness.ts +34 -0
  103. package/src/codex/quota-rejection.ts +8 -4
  104. package/src/codex/quota.ts +4 -2
  105. package/src/codex/routing/selection.ts +32 -9
  106. package/src/codex/routing.ts +53 -11
  107. package/src/codex/subagent-model-fallback.ts +4 -3
  108. package/src/codex/sync.ts +11 -0
  109. package/src/codex/windows-installation-files.ts +33 -11
  110. package/src/codex/write-coordination.ts +4 -0
  111. package/src/combos/request.ts +6 -2
  112. package/src/companion/settings.ts +6 -1
  113. package/src/config/atomic-write.ts +12 -1
  114. package/src/config/derived-registries.ts +29 -0
  115. package/src/config/diagnostics.ts +17 -0
  116. package/src/config/live-reconcile.ts +11 -2
  117. package/src/config/load-degrade.ts +9 -2
  118. package/src/config/persist-unlocked.ts +4 -3
  119. package/src/config/persisted-mutation.ts +94 -0
  120. package/src/config/provider-validation.ts +1 -1
  121. package/src/config/proxy-env.ts +62 -12
  122. package/src/config/rebase-provenance.ts +80 -1
  123. package/src/config/schema/config-schema.ts +5 -0
  124. package/src/config/schema/leaf-validators.ts +50 -1
  125. package/src/config/subagent-models.ts +41 -15
  126. package/src/config.ts +18 -95
  127. package/src/generated/compatibility-version.json +330 -218
  128. package/src/generated/model-metadata.ts +8 -5
  129. package/src/github/star-state.ts +46 -6
  130. package/src/grok/inject.ts +74 -40
  131. package/src/grok/status.ts +11 -4
  132. package/src/integrations/cursor-effort-table.ts +50 -13
  133. package/src/integrations/raycast-detect.ts +19 -4
  134. package/src/integrations/serialize.ts +11 -1
  135. package/src/lib/bounded-body.ts +30 -0
  136. package/src/lib/crash-guard.ts +52 -4
  137. package/src/lib/local-aside-sync-contract.ts +41 -0
  138. package/src/lib/package-tree-integrity.ts +181 -9
  139. package/src/lib/proxy-env.ts +18 -1
  140. package/src/lib/request-failure-attribution.ts +1 -0
  141. package/src/lib/request-failure-model.ts +3 -0
  142. package/src/lib/service-secrets.ts +99 -2
  143. package/src/lib/socks5-fetch.ts +43 -14
  144. package/src/lib/token-estimate.ts +17 -2
  145. package/src/oauth/command-code.ts +3 -2
  146. package/src/oauth/generic-account-failover.ts +29 -1
  147. package/src/oauth/index.ts +24 -16
  148. package/src/oauth/login-flow-state.ts +5 -5
  149. package/src/oauth/meta-muse-device.ts +49 -7
  150. package/src/oauth/store.ts +74 -9
  151. package/src/providers/alibaba-region-backup.ts +16 -1
  152. package/src/providers/anthropic-fast.ts +89 -0
  153. package/src/providers/anthropic-reset-grant-ledger.ts +288 -0
  154. package/src/providers/anthropic-reset-grants.ts +329 -0
  155. package/src/providers/claude-cli-identity.ts +12 -0
  156. package/src/providers/command-code-efforts.ts +184 -102
  157. package/src/providers/derive.ts +11 -3
  158. package/src/providers/fastwire.ts +16 -3
  159. package/src/providers/label.ts +8 -3
  160. package/src/providers/model-rename-fields.ts +1 -0
  161. package/src/providers/openai-virtual-models.ts +1 -0
  162. package/src/providers/quota/vendor-probes-oauth.ts +2 -1
  163. package/src/providers/reasoning-metadata.ts +38 -15
  164. package/src/providers/registry/entries-core.ts +95 -7
  165. package/src/providers/registry/entries-extended.ts +21 -10
  166. package/src/providers/registry/model-ids.ts +1 -0
  167. package/src/providers/registry/model-seeds.ts +31 -3
  168. package/src/providers/registry/types.ts +3 -1
  169. package/src/providers/resolved-model-policy-merge.ts +38 -8
  170. package/src/providers/resolved-model-policy.ts +4 -2
  171. package/src/providers/service-tier.ts +3 -1
  172. package/src/reasoning-effort.ts +6 -5
  173. package/src/responses/code-mode-helper-compat.ts +14 -4
  174. package/src/responses/code-mode-shell-input.ts +54 -0
  175. package/src/responses/custom-tool-compat.ts +173 -5
  176. package/src/responses/parser.ts +38 -2
  177. package/src/responses/reasoning-replay-cache.ts +26 -0
  178. package/src/router.ts +11 -1
  179. package/src/routing/history/indexer.ts +7 -2
  180. package/src/routing/history/schema.ts +3 -1
  181. package/src/server/adapter-resolve.ts +2 -2
  182. package/src/server/auth-cors.ts +3 -0
  183. package/src/server/chat-completions.ts +14 -1
  184. package/src/server/chat-native.ts +17 -0
  185. package/src/server/claude-messages.ts +4 -3
  186. package/src/server/direct-local-http.ts +45 -19
  187. package/src/server/gui-session.ts +6 -15
  188. package/src/server/index/package-tree-guard.ts +53 -0
  189. package/src/server/index/serve-options.ts +17 -3
  190. package/src/server/index/startup-warnings.ts +17 -0
  191. package/src/server/index.ts +15 -17
  192. package/src/server/lifecycle.ts +8 -0
  193. package/src/server/live.ts +54 -4
  194. package/src/server/management/agent-settings-routes.ts +44 -8
  195. package/src/server/management/anthropic-reset-grant-routes.ts +252 -0
  196. package/src/server/management/codex-prompt-routes.ts +5 -1
  197. package/src/server/management/config-routes.ts +14 -4
  198. package/src/server/management/oauth-account-routes.ts +17 -1
  199. package/src/server/management/provider-overwrite-carry.ts +164 -0
  200. package/src/server/management/provider-routes.ts +36 -6
  201. package/src/server/management/remote-workspace-routes.ts +2 -2
  202. package/src/server/management/route-registry.ts +2 -0
  203. package/src/server/management/shadow-call-validation.ts +39 -0
  204. package/src/server/management/system-restart.ts +72 -6
  205. package/src/server/management-api.ts +16 -2
  206. package/src/server/management-auth.ts +52 -8
  207. package/src/server/proxy-liveness.ts +110 -4
  208. package/src/server/request-log.ts +26 -9
  209. package/src/server/request-metrics.ts +5 -0
  210. package/src/server/responses/adapter-continuation.ts +6 -3
  211. package/src/server/responses/adapter-dispatch.ts +65 -1
  212. package/src/server/responses/compact.ts +31 -1
  213. package/src/server/responses/compaction-routing.ts +28 -3
  214. package/src/server/responses/core-codex-account.ts +85 -26
  215. package/src/server/responses/core-combo-failure.ts +16 -15
  216. package/src/server/responses/core-normalize.ts +5 -1
  217. package/src/server/responses/core-opaque-recovery.ts +50 -2
  218. package/src/server/responses/core-options.ts +2 -0
  219. package/src/server/responses/core-replay.ts +7 -0
  220. package/src/server/responses/core.ts +1 -1
  221. package/src/server/responses/encrypted-payload.ts +23 -9
  222. package/src/server/responses/passthrough-delivery.ts +55 -35
  223. package/src/server/responses/passthrough-dispatch.ts +12 -13
  224. package/src/server/responses/policy-fallback.ts +12 -1
  225. package/src/server/responses/request-prepare.ts +46 -7
  226. package/src/server/responses/request-spend.ts +31 -15
  227. package/src/server/responses/run-turn-execution.ts +6 -5
  228. package/src/server/responses/shadow-target-availability.ts +61 -0
  229. package/src/server/responses-custom-tool-repair.ts +2 -0
  230. package/src/service/claim.ts +185 -0
  231. package/src/service/cli.ts +10 -1
  232. package/src/service/guarded-manager-target.ts +151 -0
  233. package/src/service/guards.ts +47 -48
  234. package/src/service/managing-cli.ts +170 -0
  235. package/src/service/orchestration.ts +3 -1
  236. package/src/service/state.ts +4 -0
  237. package/src/service/systemd.ts +16 -1
  238. package/src/service.ts +1 -1
  239. package/src/types/config.ts +8 -0
  240. package/src/types/provider.ts +16 -0
  241. package/src/types/request.ts +10 -0
  242. package/src/types/tools.ts +9 -1
  243. package/src/types/wire.ts +68 -4
  244. package/src/types.ts +1 -0
  245. package/src/update/job.ts +10 -16
  246. package/src/update/npm-invocation.mjs +17 -16
  247. package/src/update/transactional-install.d.mts +22 -1
  248. package/src/update/transactional-install.mjs +176 -19
  249. package/src/update/update-failure-guidance.d.mts +8 -0
  250. package/src/update/update-failure-guidance.mjs +47 -0
  251. package/src/usage/expected-prices.ts +54 -12
  252. package/src/usage/log.ts +117 -3
  253. package/src/usage/telemetry-contract.ts +1 -0
  254. package/src/usage/timeline.ts +34 -6
  255. package/src/usage/user-cost-overlay-reconciler.ts +3 -3
  256. package/src/vision/reasoning.ts +2 -4
  257. package/src/web-search/xai-executor.ts +14 -4
  258. package/gui/dist/assets/App-BqrsSrIR.js +0 -50
  259. package/gui/dist/assets/index-C6SJrh0N.js +0 -86
  260. package/gui/dist/assets/index-DdDunwDb.css +0 -1
  261. package/gui/dist/assets/usage-companion-chart-CzAAAB1o.js +0 -1
  262. package/src/adapters/kiro-thinking.ts +0 -112
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Anthropic fast mode (`speed: "fast"`): wire constants and refusal recognition.
3
+ *
4
+ * Fast mode is a separately entitled, separately rate-limited lane on the same Opus models
5
+ * (https://platform.claude.com/docs/en/build-with-claude/fast-mode). The Messages API never
6
+ * falls back on its own: an account without usage credits gets a 429, an organization that has
7
+ * not enabled the feature gets a 400, and a model outside the lane gets a 400 naming the
8
+ * `speed` parameter. Claude Code retries such a request at standard speed; the adapter
9
+ * dispatch loop does the same, once per request.
10
+ *
11
+ * Recognition is deliberately narrow. A generic 429 or 529 on a fast request may be an ordinary
12
+ * account rate limit and must keep flowing into the existing wait/rotation handling, so only an
13
+ * error message that names fast mode or the speed parameter, or a fast-pool header reporting
14
+ * zero remaining, counts. Live probe strings (2026-09-23,
15
+ * devlog/_plan/260923_anthropic_fast_speed/020_probe-evidence.md):
16
+ * 429 "Usage credits are required for fast mode."
17
+ * 400 "Fast mode is not enabled for your organization. ..."
18
+ * 400 "'<model>' does not support the `speed` parameter. ..."
19
+ */
20
+
21
+ export const ANTHROPIC_FAST_MODE_BETA = "fast-mode-2026-02-01";
22
+
23
+ const FAST_REFUSAL_MESSAGE = /\bfast mode\b|\bspeed\b\W{0,2} parameter/i;
24
+ const FAST_POOL_REMAINING_HEADER = /^anthropic-fast-(?:input|output)-tokens-remaining$/i;
25
+
26
+ function anthropicErrorMessage(bodyText: string): string | undefined {
27
+ try {
28
+ const parsed: unknown = JSON.parse(bodyText);
29
+ if (!parsed || typeof parsed !== "object") return undefined;
30
+ const error = (parsed as { error?: unknown }).error;
31
+ if (!error || typeof error !== "object") return undefined;
32
+ const message = (error as { message?: unknown }).message;
33
+ return typeof message === "string" ? message : undefined;
34
+ } catch {
35
+ return undefined;
36
+ }
37
+ }
38
+
39
+ function fastPoolExhausted(headers: Headers): boolean {
40
+ let exhausted = false;
41
+ headers.forEach((value, name) => {
42
+ if (FAST_POOL_REMAINING_HEADER.test(name) && value.trim() === "0") exhausted = true;
43
+ });
44
+ return exhausted;
45
+ }
46
+
47
+ /**
48
+ * Whether a response to a request that actually sent `speed: "fast"` is a fast-lane refusal a
49
+ * standard-speed resend can recover. Pure; the caller supplies a bounded body read.
50
+ */
51
+ export function isAnthropicFastRefusal(
52
+ status: number,
53
+ headers: Headers,
54
+ bodyText: string | undefined,
55
+ ): boolean {
56
+ if (status !== 400 && status !== 429) return false;
57
+ if (status === 429 && fastPoolExhausted(headers)) return true;
58
+ const message = bodyText === undefined ? undefined : anthropicErrorMessage(bodyText);
59
+ return message !== undefined && FAST_REFUSAL_MESSAGE.test(message);
60
+ }
61
+
62
+ /**
63
+ * Merge beta tokens into the single `anthropic-beta` header, case-insensitively. Header
64
+ * overrides may spell the name in any case; two spellings in one plain object would reach the
65
+ * wire as two headers (Headers comma-joins them), so every spelling collapses into one
66
+ * lowercase key with deduplicated tokens in first-seen order.
67
+ */
68
+ export function mergeAnthropicBetaHeader(
69
+ headers: Record<string, string>,
70
+ extraBetas: readonly string[] = [],
71
+ ): void {
72
+ const tokens: string[] = [];
73
+ const seen = new Set<string>();
74
+ const add = (raw: string) => {
75
+ for (const part of raw.split(",")) {
76
+ const token = part.trim();
77
+ if (!token || seen.has(token.toLowerCase())) continue;
78
+ seen.add(token.toLowerCase());
79
+ tokens.push(token);
80
+ }
81
+ };
82
+ for (const name of Object.keys(headers)) {
83
+ if (name.toLowerCase() !== "anthropic-beta") continue;
84
+ add(headers[name] ?? "");
85
+ delete headers[name];
86
+ }
87
+ for (const beta of extraBetas) add(beta);
88
+ if (tokens.length > 0) headers["anthropic-beta"] = tokens.join(",");
89
+ }
@@ -0,0 +1,288 @@
1
+ /**
2
+ * Durable journal for Anthropic reset-grant claims.
3
+ *
4
+ * One dashboard confirmation is one operation. Its UUIDv4 id is also the
5
+ * upstream `request_id`, so a retry of the same operation is the same claim to
6
+ * Anthropic. The Claude Code client reuses an unsettled claim's request id for
7
+ * ten minutes (`b5o = 600000`) and mints a new one afterwards; this journal keeps
8
+ * the same window.
9
+ *
10
+ * Rules (devlog/_plan/260923_claude_reset_grants/010_plan.md, "Ledger contract"):
11
+ * - Every read-modify-write runs synchronously inside a cross-process
12
+ * `BEGIN IMMEDIATE` lock on a sibling SQLite file; nothing awaits inside it,
13
+ * and the upstream claim always runs after the open record is on disk.
14
+ * - An open record carries a lease; a second attempt inside it is refused.
15
+ * - Settlement is never inferred. The first terminal settlement wins.
16
+ * - An unreadable journal refuses the spend instead of starting from empty.
17
+ */
18
+ import { Database } from "bun:sqlite";
19
+ import { createHash } from "node:crypto";
20
+ import { existsSync, mkdirSync, readFileSync, writeSync } from "node:fs";
21
+ import { dirname, join } from "node:path";
22
+ import { atomicWriteFileStreamed } from "../config/atomic-write";
23
+ import { getConfigDir } from "../config/paths";
24
+
25
+ export const ANTHROPIC_RESET_LEASE_MS = 90_000;
26
+ export const ANTHROPIC_RESET_RETRY_WINDOW_MS = 600_000;
27
+ const RETENTION_MS = 30 * 24 * 60 * 60_000;
28
+ const MAX_OPERATIONS = 256;
29
+
30
+ export type AnthropicResetLedgerErrorCode = "busy" | "unavailable" | "write_failed";
31
+
32
+ export class AnthropicResetLedgerError extends Error {
33
+ constructor(readonly code: AnthropicResetLedgerErrorCode) {
34
+ super(`Anthropic reset-grant journal ${code}`);
35
+ this.name = "AnthropicResetLedgerError";
36
+ }
37
+ }
38
+
39
+ interface OperationRecord {
40
+ accountId: string;
41
+ grantId: string;
42
+ orgDigest: string;
43
+ status: "open" | "settled";
44
+ code?: string;
45
+ resetsLeft?: number | null;
46
+ attempts: number;
47
+ leaseUntil: number;
48
+ createdAt: number;
49
+ updatedAt: number;
50
+ }
51
+
52
+ interface Journal {
53
+ version: 1;
54
+ operations: Record<string, OperationRecord>;
55
+ }
56
+
57
+ export interface AnthropicResetOperationIdentity {
58
+ operationId: string;
59
+ accountId: string;
60
+ grantId: string;
61
+ orgDigest: string;
62
+ }
63
+
64
+ export type AnthropicResetBegin =
65
+ | { kind: "execute"; attempt: number }
66
+ | { kind: "replay"; code: string; resetsLeft: number | null; settledAt: number }
67
+ | { kind: "identity-mismatch" }
68
+ | { kind: "in-flight" }
69
+ | { kind: "expired" }
70
+ | { kind: "unresolved-prior"; operationId: string }
71
+ | { kind: "capacity" };
72
+
73
+ export interface AnthropicResetPendingOperation {
74
+ operationId: string;
75
+ grantId: string;
76
+ createdAt: number;
77
+ retryableUntil: number;
78
+ }
79
+
80
+ /** Digest so the journal never stores the raw organization UUID. */
81
+ export function anthropicOrgDigest(organizationUuid: string): string {
82
+ return createHash("sha256").update(`opencodex-anthropic-reset-org\0${organizationUuid.toLowerCase()}`).digest("hex");
83
+ }
84
+
85
+ export function anthropicResetJournalPath(customDir?: string): string {
86
+ return join(customDir ?? getConfigDir(), "anthropic-reset-grant-ledger.json");
87
+ }
88
+
89
+ function isRecordShape(value: unknown): value is OperationRecord {
90
+ if (!value || typeof value !== "object") return false;
91
+ const record = value as Record<string, unknown>;
92
+ return typeof record.accountId === "string" && typeof record.grantId === "string"
93
+ && typeof record.orgDigest === "string" && (record.status === "open" || record.status === "settled")
94
+ && typeof record.attempts === "number" && typeof record.leaseUntil === "number"
95
+ && typeof record.createdAt === "number" && typeof record.updatedAt === "number";
96
+ }
97
+
98
+ /** Missing file → empty journal. Present but unreadable → throws; never an empty fallback. */
99
+ function readJournal(filePath: string): Journal {
100
+ if (!existsSync(filePath)) return { version: 1, operations: {} };
101
+ let parsed: unknown;
102
+ try {
103
+ parsed = JSON.parse(readFileSync(filePath, "utf-8"));
104
+ } catch {
105
+ throw new AnthropicResetLedgerError("unavailable");
106
+ }
107
+ const journal = parsed as Partial<Journal> | null;
108
+ if (!journal || journal.version !== 1 || !journal.operations || typeof journal.operations !== "object"
109
+ || !Object.values(journal.operations).every(isRecordShape)) {
110
+ throw new AnthropicResetLedgerError("unavailable");
111
+ }
112
+ return journal as Journal;
113
+ }
114
+
115
+ function writeJournal(filePath: string, journal: Journal, now: number): void {
116
+ const cutoff = now - RETENTION_MS;
117
+ journal.operations = Object.fromEntries(
118
+ Object.entries(journal.operations).filter(([, record]) => record.updatedAt > cutoff),
119
+ );
120
+ const bytes = Buffer.from(JSON.stringify(journal, null, 2));
121
+ try {
122
+ // The streamed form fsyncs the temp file and the parent directory, so an
123
+ // open record is on disk before the claim that depends on it is sent.
124
+ atomicWriteFileStreamed(filePath, descriptor => {
125
+ let offset = 0;
126
+ while (offset < bytes.length) offset += writeSync(descriptor, bytes, offset, bytes.length - offset);
127
+ });
128
+ } catch {
129
+ throw new AnthropicResetLedgerError("write_failed");
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Runs `fn` inside an OS-backed cross-process lock. `busy_timeout = 0` so a
135
+ * contended request fails immediately instead of blocking the event loop, the
136
+ * same trade-off src/config/mutation-lock.ts makes. `fn` must be synchronous.
137
+ */
138
+ function withJournalLock<T>(filePath: string, fn: () => T): T {
139
+ let database: Database | undefined;
140
+ try {
141
+ mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 });
142
+ database = new Database(`${filePath}.lock.sqlite`, { create: true });
143
+ database.exec("PRAGMA busy_timeout = 0; BEGIN IMMEDIATE");
144
+ } catch {
145
+ try { database?.close(); } catch { /* acquisition already failed */ }
146
+ throw new AnthropicResetLedgerError("busy");
147
+ }
148
+ try {
149
+ const value = fn();
150
+ database.exec("COMMIT");
151
+ return value;
152
+ } catch (error) {
153
+ try { database.exec("ROLLBACK"); } catch { /* closing releases the lock */ }
154
+ throw error;
155
+ } finally {
156
+ database.close();
157
+ }
158
+ }
159
+
160
+ export interface AnthropicResetLedgerOptions {
161
+ now?: number;
162
+ journalPath?: string;
163
+ }
164
+
165
+ /** True when the operation already has a record. Lock-free read for routing only. */
166
+ export function anthropicResetOperationExists(operationId: string, options: AnthropicResetLedgerOptions = {}): boolean {
167
+ return readJournal(options.journalPath ?? anthropicResetJournalPath()).operations[operationId] !== undefined;
168
+ }
169
+
170
+ /**
171
+ * Opens (or re-opens for a same-id retry) one claim attempt. On `execute` the
172
+ * record is durably open and leased before this returns; the caller may then
173
+ * send the claim with `operationId` as its request id.
174
+ */
175
+ export function beginAnthropicResetOperation(
176
+ identity: AnthropicResetOperationIdentity,
177
+ options: AnthropicResetLedgerOptions = {},
178
+ ): AnthropicResetBegin {
179
+ const now = options.now ?? Date.now();
180
+ const filePath = options.journalPath ?? anthropicResetJournalPath();
181
+ return withJournalLock(filePath, () => {
182
+ const journal = readJournal(filePath);
183
+ const existing = journal.operations[identity.operationId];
184
+ if (existing) {
185
+ if (existing.accountId !== identity.accountId || existing.grantId !== identity.grantId
186
+ || existing.orgDigest !== identity.orgDigest) {
187
+ return { kind: "identity-mismatch" };
188
+ }
189
+ if (existing.status === "settled") {
190
+ return { kind: "replay", code: existing.code ?? "unavailable", resetsLeft: existing.resetsLeft ?? null, settledAt: existing.updatedAt };
191
+ }
192
+ if (existing.leaseUntil > now) return { kind: "in-flight" };
193
+ if (now - existing.createdAt >= ANTHROPIC_RESET_RETRY_WINDOW_MS) return { kind: "expired" };
194
+ existing.attempts += 1;
195
+ existing.leaseUntil = now + ANTHROPIC_RESET_LEASE_MS;
196
+ existing.updatedAt = now;
197
+ writeJournal(filePath, journal, now);
198
+ return { kind: "execute", attempt: existing.attempts };
199
+ }
200
+ for (const [operationId, record] of Object.entries(journal.operations)) {
201
+ if (record.status === "open" && record.accountId === identity.accountId && record.grantId === identity.grantId
202
+ && record.orgDigest === identity.orgDigest && now - record.createdAt < ANTHROPIC_RESET_RETRY_WINDOW_MS) {
203
+ return { kind: "unresolved-prior", operationId };
204
+ }
205
+ }
206
+ if (Object.keys(journal.operations).length >= MAX_OPERATIONS) return { kind: "capacity" };
207
+ journal.operations[identity.operationId] = {
208
+ accountId: identity.accountId,
209
+ grantId: identity.grantId,
210
+ orgDigest: identity.orgDigest,
211
+ status: "open",
212
+ attempts: 1,
213
+ leaseUntil: now + ANTHROPIC_RESET_LEASE_MS,
214
+ createdAt: now,
215
+ updatedAt: now,
216
+ };
217
+ writeJournal(filePath, journal, now);
218
+ return { kind: "execute", attempt: 1 };
219
+ });
220
+ }
221
+
222
+ export interface AnthropicResetSettlement {
223
+ code: string;
224
+ resetsLeft: number | null;
225
+ }
226
+
227
+ /**
228
+ * Records a terminal answer and returns the settlement the journal now holds.
229
+ * First settlement wins: a later answer for an already-settled operation gets the
230
+ * stored one back, so the caller reports what the journal says. A missing record
231
+ * throws, because an answer with no journal entry cannot be reported as durable.
232
+ */
233
+ export function settleAnthropicResetOperation(
234
+ settlement: { operationId: string; code: string; resetsLeft: number | null },
235
+ options: AnthropicResetLedgerOptions = {},
236
+ ): AnthropicResetSettlement {
237
+ const now = options.now ?? Date.now();
238
+ const filePath = options.journalPath ?? anthropicResetJournalPath();
239
+ return withJournalLock(filePath, () => {
240
+ const journal = readJournal(filePath);
241
+ const existing = journal.operations[settlement.operationId];
242
+ if (!existing) throw new AnthropicResetLedgerError("unavailable");
243
+ if (existing.status === "settled") return { code: existing.code ?? "unavailable", resetsLeft: existing.resetsLeft ?? null };
244
+ existing.status = "settled";
245
+ existing.code = settlement.code;
246
+ existing.resetsLeft = settlement.resetsLeft;
247
+ existing.leaseUntil = 0;
248
+ existing.updatedAt = now;
249
+ writeJournal(filePath, journal, now);
250
+ return { code: settlement.code, resetsLeft: settlement.resetsLeft };
251
+ });
252
+ }
253
+
254
+ /**
255
+ * Ends this process's attempt on an unknown outcome so an explicit same-id retry
256
+ * does not wait out the whole lease. The record stays open.
257
+ */
258
+ export function releaseAnthropicResetLease(operationId: string, options: AnthropicResetLedgerOptions = {}): void {
259
+ const now = options.now ?? Date.now();
260
+ const filePath = options.journalPath ?? anthropicResetJournalPath();
261
+ withJournalLock(filePath, () => {
262
+ const journal = readJournal(filePath);
263
+ const existing = journal.operations[operationId];
264
+ if (!existing || existing.status !== "open") return;
265
+ existing.leaseUntil = now;
266
+ existing.updatedAt = now;
267
+ writeJournal(filePath, journal, now);
268
+ });
269
+ }
270
+
271
+ /** Open operation for this account still inside its retry window, if any. */
272
+ export function pendingAnthropicResetOperation(
273
+ accountId: string,
274
+ options: AnthropicResetLedgerOptions = {},
275
+ ): AnthropicResetPendingOperation | null {
276
+ const now = options.now ?? Date.now();
277
+ const journal = readJournal(options.journalPath ?? anthropicResetJournalPath());
278
+ let pending: AnthropicResetPendingOperation | null = null;
279
+ for (const [operationId, record] of Object.entries(journal.operations)) {
280
+ if (record.status !== "open" || record.accountId !== accountId) continue;
281
+ const retryableUntil = record.createdAt + ANTHROPIC_RESET_RETRY_WINDOW_MS;
282
+ if (retryableUntil <= now) continue;
283
+ if (!pending || record.createdAt > pending.createdAt) {
284
+ pending = { operationId, grantId: record.grantId, createdAt: record.createdAt, retryableUntil };
285
+ }
286
+ }
287
+ return pending;
288
+ }