@bitkyc08/opencodex 2.55.0-preview.20260914 → 2.56.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 (167) hide show
  1. package/gui/dist/assets/{index-DH2PUHqr.js → index-D4zuyIxQ.js} +1 -1
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +2 -1
  4. package/src/adapters/base.ts +21 -0
  5. package/src/adapters/cursor/transport-retry.ts +46 -1
  6. package/src/adapters/cursor.ts +4 -0
  7. package/src/adapters/kiro/adapter.ts +42 -1
  8. package/src/adapters/kiro-retry.ts +23 -4
  9. package/src/adapters/openai-chat/errors.ts +116 -0
  10. package/src/adapters/openai-chat/messages.ts +346 -0
  11. package/src/adapters/openai-chat/passthrough.ts +146 -0
  12. package/src/adapters/openai-chat/response-events.ts +117 -0
  13. package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
  14. package/src/adapters/openai-chat/tool-schema.ts +477 -0
  15. package/src/adapters/openai-chat/wire.ts +50 -0
  16. package/src/adapters/openai-chat.ts +33 -1445
  17. package/src/adapters/openai-responses/canonical-forward.ts +202 -0
  18. package/src/adapters/openai-responses/image-gen.ts +406 -0
  19. package/src/adapters/openai-responses/internal.ts +3 -0
  20. package/src/adapters/openai-responses/passthrough.ts +611 -0
  21. package/src/adapters/openai-responses/prompt-cache.ts +83 -0
  22. package/src/adapters/openai-responses/reasoning.ts +220 -0
  23. package/src/adapters/openai-responses/request-strips.ts +185 -0
  24. package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
  25. package/src/adapters/openai-responses/tool-schema.ts +293 -0
  26. package/src/adapters/openai-responses/web-search.ts +156 -0
  27. package/src/adapters/openai-responses.ts +4 -2625
  28. package/src/bridge/errors.ts +34 -0
  29. package/src/bridge/internal.ts +174 -0
  30. package/src/bridge/response-json.ts +624 -0
  31. package/src/bridge/sse.ts +1444 -0
  32. package/src/bridge.ts +5 -2204
  33. package/src/chat/inbound.ts +12 -1
  34. package/src/codex/account-lifecycle.ts +3 -0
  35. package/src/codex/account-store.ts +71 -9
  36. package/src/codex/auth-api/account-list.ts +507 -0
  37. package/src/codex/auth-api/http.ts +32 -0
  38. package/src/codex/auth-api/login-flow.ts +554 -0
  39. package/src/codex/auth-api/login-state.ts +64 -0
  40. package/src/codex/auth-api/main-account-probe.ts +331 -0
  41. package/src/codex/auth-api/pool-mode-gate.ts +274 -0
  42. package/src/codex/auth-api/pool-quota-probe.ts +512 -0
  43. package/src/codex/auth-api/reset-credit-service.ts +422 -0
  44. package/src/codex/auth-api/routes.ts +425 -0
  45. package/src/codex/auth-api/runtime-config.ts +48 -0
  46. package/src/codex/auth-api.ts +27 -3118
  47. package/src/codex/auth-context.ts +95 -28
  48. package/src/codex/catalog/auto-review.ts +507 -0
  49. package/src/codex/catalog/build-entries.ts +981 -0
  50. package/src/codex/catalog/combo-member.ts +375 -0
  51. package/src/codex/catalog/derive-entry.ts +229 -0
  52. package/src/codex/catalog/effort.ts +0 -1
  53. package/src/codex/catalog/gated-native-warn.ts +63 -0
  54. package/src/codex/catalog/gather-capture.ts +533 -0
  55. package/src/codex/catalog/model-hints.ts +691 -0
  56. package/src/codex/catalog/model-visibility.ts +304 -0
  57. package/src/codex/catalog/provider-fetch.ts +52 -2942
  58. package/src/codex/catalog/provider-models.ts +685 -0
  59. package/src/codex/catalog/restore.ts +132 -0
  60. package/src/codex/catalog/retained-sync.ts +706 -0
  61. package/src/codex/catalog/routed-gather.ts +858 -0
  62. package/src/codex/catalog/subagent-roster.ts +176 -0
  63. package/src/codex/catalog/sync.ts +52 -2698
  64. package/src/codex/inject/config-toml.ts +563 -0
  65. package/src/codex/inject/remove.ts +192 -0
  66. package/src/codex/inject/restore.ts +540 -0
  67. package/src/codex/inject/routing-classify.ts +109 -0
  68. package/src/codex/inject/routing-target.ts +125 -0
  69. package/src/codex/inject.ts +81 -1436
  70. package/src/codex/lineage.ts +458 -0
  71. package/src/codex/pool-refresh-backoff.ts +152 -0
  72. package/src/codex/routing/active-account.ts +194 -0
  73. package/src/codex/routing/cooldown-math.ts +275 -0
  74. package/src/codex/routing/health-store.ts +402 -0
  75. package/src/codex/routing/probe-lease.ts +358 -0
  76. package/src/codex/routing/selection.ts +703 -0
  77. package/src/codex/routing/thread-affinity.ts +538 -0
  78. package/src/codex/routing.ts +353 -2234
  79. package/src/codex/shim-fingerprint.ts +223 -0
  80. package/src/codex/shim-inspect.ts +175 -0
  81. package/src/codex/shim-probe.ts +367 -0
  82. package/src/codex/shim-restore-lock.ts +169 -0
  83. package/src/codex/shim-state-file.ts +151 -0
  84. package/src/codex/shim-templates.ts +265 -0
  85. package/src/codex/shim.ts +48 -1268
  86. package/src/config/diagnostics.ts +705 -0
  87. package/src/config/feature-flags.ts +55 -0
  88. package/src/config/live-reconcile.ts +403 -0
  89. package/src/config/load-degrade.ts +880 -0
  90. package/src/config/mutation-lock.ts +244 -0
  91. package/src/config/openai-tier-backup.ts +268 -0
  92. package/src/config/persist-unlocked.ts +92 -0
  93. package/src/config/proxy-env.ts +188 -0
  94. package/src/config/salvage.ts +244 -0
  95. package/src/config/schema/config-schema.ts +640 -0
  96. package/src/config/schema/leaf-validators.ts +855 -0
  97. package/src/config/warn-memo.ts +28 -0
  98. package/src/config.ts +234 -4481
  99. package/src/generated/compatibility-version.json +539 -39
  100. package/src/lib/request-execution-budget.ts +69 -20
  101. package/src/lib/spend-reservation-ledger.ts +940 -0
  102. package/src/lib/upstream-retry.ts +55 -11
  103. package/src/lib/workflow-budget.ts +553 -30
  104. package/src/providers/quota/account-cache.ts +441 -0
  105. package/src/providers/quota/antigravity.ts +295 -0
  106. package/src/providers/quota/report-cache.ts +320 -0
  107. package/src/providers/quota/vendor-probes-key.ts +1243 -0
  108. package/src/providers/quota/vendor-probes-oauth.ts +590 -0
  109. package/src/providers/quota.ts +324 -3079
  110. package/src/providers/registry/entries-core.ts +1221 -0
  111. package/src/providers/registry/entries-extended.ts +1204 -0
  112. package/src/providers/registry/model-seeds.ts +908 -0
  113. package/src/providers/registry/types.ts +352 -0
  114. package/src/providers/registry.ts +24 -3536
  115. package/src/responses/continuation-ownership.ts +29 -0
  116. package/src/responses/state/replay-fingerprint.ts +80 -0
  117. package/src/responses/state/snapshot-codec.ts +104 -0
  118. package/src/responses/state/spill-failure.ts +118 -0
  119. package/src/responses/state/spill-queue.ts +665 -0
  120. package/src/responses/state/temp-recovery.ts +257 -0
  121. package/src/responses/state.ts +82 -1143
  122. package/src/routing/identity-domains.ts +449 -0
  123. package/src/routing/probe-lease.ts +511 -0
  124. package/src/server/index/bounded-request.ts +88 -0
  125. package/src/server/index/live-sideband.ts +565 -0
  126. package/src/server/index/serve-options.ts +1766 -0
  127. package/src/server/index/startup-warnings.ts +213 -0
  128. package/src/server/index/websocket-handler.ts +335 -0
  129. package/src/server/index.ts +40 -2547
  130. package/src/server/management/route-registry.ts +26 -23
  131. package/src/server/management/shared.ts +8 -5
  132. package/src/server/management/workflow-budget-routes.ts +133 -0
  133. package/src/server/management-api.ts +12 -0
  134. package/src/server/request-log-conversation.ts +9 -7
  135. package/src/server/request-log.ts +245 -1
  136. package/src/server/responses/account-change-state.ts +233 -0
  137. package/src/server/responses/adapter-continuation.ts +514 -0
  138. package/src/server/responses/adapter-delivery.ts +214 -0
  139. package/src/server/responses/adapter-dispatch.ts +971 -0
  140. package/src/server/responses/compact.ts +59 -4
  141. package/src/server/responses/completion-policy.ts +33 -0
  142. package/src/server/responses/core-auth.ts +527 -0
  143. package/src/server/responses/core-codex-account.ts +859 -0
  144. package/src/server/responses/core-combo-failure.ts +210 -0
  145. package/src/server/responses/core-combo.ts +707 -0
  146. package/src/server/responses/core-errors.ts +152 -0
  147. package/src/server/responses/core-lifetime.ts +95 -0
  148. package/src/server/responses/core-normalize.ts +350 -0
  149. package/src/server/responses/core-opaque-recovery.ts +380 -0
  150. package/src/server/responses/core-options.ts +159 -0
  151. package/src/server/responses/core-replay.ts +225 -0
  152. package/src/server/responses/core.ts +192 -8893
  153. package/src/server/responses/passthrough-delivery.ts +856 -0
  154. package/src/server/responses/passthrough-dispatch.ts +1476 -0
  155. package/src/server/responses/passthrough-execution.ts +54 -0
  156. package/src/server/responses/request-prepare.ts +970 -0
  157. package/src/server/responses/request-send-budget.ts +164 -0
  158. package/src/server/responses/request-sidecar-auth.ts +149 -0
  159. package/src/server/responses/request-transport.ts +744 -0
  160. package/src/server/responses/response-effects.ts +157 -0
  161. package/src/server/responses/run-turn-execution.ts +448 -0
  162. package/src/server/responses/sidecar-execution.ts +469 -0
  163. package/src/server/responses-image-gen-repair.ts +1 -1
  164. package/src/server/workflow-refusal.ts +84 -0
  165. package/src/types/config.ts +30 -0
  166. package/src/usage/log.ts +146 -0
  167. package/src/usage/summary.ts +171 -21
@@ -0,0 +1,223 @@
1
+ import {
2
+ closeSync,
3
+ existsSync,
4
+ lstatSync,
5
+ linkSync,
6
+ openSync,
7
+ readSync,
8
+ readlinkSync,
9
+ statSync,
10
+ symlinkSync,
11
+ unlinkSync,
12
+ } from "node:fs";
13
+ import { posix, win32 } from "node:path";
14
+ import { SHIM_MARKER, UNIX_SHIM_REVISION_MARKER } from "./shim-templates";
15
+ import type { ShimFileState } from "./shim-state-file";
16
+
17
+ const CODEX_SHIM_PROBE_BYTES = 16 * 1024;
18
+
19
+ interface ShimPathFingerprint {
20
+ dev: number;
21
+ ino: number;
22
+ kind: "file" | "symlink";
23
+ mode: number;
24
+ size: number;
25
+ mtimeMs: number;
26
+ ctimeMs: number;
27
+ target?: Omit<ShimPathFingerprint, "target">;
28
+ }
29
+
30
+ interface StableShimPathProbe {
31
+ fingerprint: ShimPathFingerprint;
32
+ prefix: string;
33
+ }
34
+
35
+ function readShimProbePrefix(path: string): string {
36
+ const fd = openSync(path, "r");
37
+ try {
38
+ const buffer = Buffer.allocUnsafe(CODEX_SHIM_PROBE_BYTES);
39
+ const bytesRead = readSync(fd, buffer, 0, buffer.length, 0);
40
+ return buffer.toString("utf8", 0, bytesRead);
41
+ } finally {
42
+ closeSync(fd);
43
+ }
44
+ }
45
+
46
+ function statFingerprint(path: string, follow: boolean): Omit<ShimPathFingerprint, "target"> | null {
47
+ try {
48
+ const stat = follow ? statSync(path) : lstatSync(path);
49
+ if (follow ? !stat.isFile() : (!stat.isFile() && !stat.isSymbolicLink())) return null;
50
+ return {
51
+ dev: stat.dev,
52
+ ino: stat.ino,
53
+ kind: stat.isSymbolicLink() ? "symlink" : "file",
54
+ mode: stat.mode,
55
+ size: stat.size,
56
+ mtimeMs: stat.mtimeMs,
57
+ ctimeMs: stat.ctimeMs,
58
+ };
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+
64
+ function sameFingerprint(
65
+ left: ShimPathFingerprint | Omit<ShimPathFingerprint, "target">,
66
+ right: ShimPathFingerprint | Omit<ShimPathFingerprint, "target">,
67
+ ): boolean {
68
+ return left.dev === right.dev
69
+ && left.ino === right.ino
70
+ && left.kind === right.kind
71
+ && left.mode === right.mode
72
+ && left.size === right.size
73
+ && left.mtimeMs === right.mtimeMs
74
+ && left.ctimeMs === right.ctimeMs
75
+ && (!("target" in left) || !("target" in right)
76
+ ? true
77
+ : left.target === undefined && right.target === undefined
78
+ ? true
79
+ : left.target !== undefined && right.target !== undefined
80
+ ? sameFingerprint(left.target, right.target)
81
+ : false);
82
+ }
83
+
84
+ function sameFingerprintAfterRename(left: ShimPathFingerprint, right: ShimPathFingerprint): boolean {
85
+ // rename changes the outer directory entry ctime on macOS; every other field,
86
+ // including a symlink target fingerprint, must remain identical.
87
+ return sameFingerprint({ ...left, ctimeMs: 0 }, { ...right, ctimeMs: 0 });
88
+ }
89
+
90
+ function stableShimPathProbe(path: string): StableShimPathProbe | null {
91
+ const before = statFingerprint(path, false);
92
+ if (!before) return null;
93
+ const targetBefore = before.kind === "symlink" ? statFingerprint(path, true) : undefined;
94
+ if (before.kind === "symlink" && !targetBefore) return null;
95
+ let prefix: string;
96
+ try {
97
+ prefix = readShimProbePrefix(path);
98
+ } catch {
99
+ return null;
100
+ }
101
+ const targetAfter = before.kind === "symlink" ? statFingerprint(path, true) : undefined;
102
+ const after = statFingerprint(path, false);
103
+ if (!after || !sameFingerprint(before, after)) return null;
104
+ if (before.kind === "symlink") {
105
+ if (!targetBefore || !targetAfter || !sameFingerprint(targetBefore, targetAfter)) return null;
106
+ }
107
+ const fingerprint: ShimPathFingerprint = {
108
+ ...before,
109
+ ...(targetBefore ? { target: targetBefore } : {}),
110
+ };
111
+ const contentSize = fingerprint.target?.size ?? fingerprint.size;
112
+ return contentSize > 0 ? { fingerprint, prefix } : null;
113
+ }
114
+
115
+ function sameStableShimPathProbe(left: StableShimPathProbe, right: StableShimPathProbe): boolean {
116
+ return left.prefix === right.prefix && sameFingerprint(left.fingerprint, right.fingerprint);
117
+ }
118
+
119
+ /**
120
+ * Identity of whatever sits at `path`, read from metadata alone.
121
+ *
122
+ * `stableShimPathProbe` answers a different question: it reads content to decide
123
+ * whether a launcher looks like a healthy shim, and it deliberately returns null
124
+ * for a zero-byte file. That makes it the wrong instrument for rollback
125
+ * bookkeeping. A user can legitimately own an empty `codex` launcher, and a fresh
126
+ * install moves it aside before writing our wrapper; if the move is recorded
127
+ * without a fingerprint, rollback cannot prove the backup is still the file it
128
+ * set aside and refuses to restore it — the launcher stays lost (#1625).
129
+ *
130
+ * Content is irrelevant to that proof, so this reads dev/ino/mode/size/times and
131
+ * re-reads them to reject a path that changed under us, following a symlink to
132
+ * fingerprint its target as well.
133
+ */
134
+ function shimPathFingerprint(path: string): ShimPathFingerprint | null {
135
+ const before = statFingerprint(path, false);
136
+ if (!before) return null;
137
+ if (before.kind !== "symlink") {
138
+ const after = statFingerprint(path, false);
139
+ return after && sameFingerprint(before, after) ? before : null;
140
+ }
141
+ const targetBefore = statFingerprint(path, true);
142
+ if (!targetBefore) return null;
143
+ const targetAfter = statFingerprint(path, true);
144
+ const after = statFingerprint(path, false);
145
+ if (!targetAfter || !after
146
+ || !sameFingerprint(targetBefore, targetAfter)
147
+ || !sameFingerprint(before, after)) return null;
148
+ return { ...before, target: targetBefore };
149
+ }
150
+
151
+ /**
152
+ * Move `from` onto `to` without ever replacing an existing entry.
153
+ *
154
+ * `renameSync` silently clobbers the destination on POSIX, which is wrong for a
155
+ * rollback restore: `sourceOccupied` is sampled before the fingerprint check, so
156
+ * a concurrent installer can publish its own launcher at the original path in
157
+ * between, and the restore would delete it. `link` fails EEXIST instead, which
158
+ * is the no-replace primitive we need and needs no native helper.
159
+ *
160
+ * `link` follows a symlink to its target rather than preserving the link, so a
161
+ * symlink launcher is republished with `symlink`, which is also no-replace: it
162
+ * fails EEXIST on an occupied destination. Checking existence and then renaming
163
+ * would reintroduce exactly the race this function exists to close.
164
+ */
165
+ function restoreWithoutReplacing(from: string, to: string): void {
166
+ const source = lstatSync(from);
167
+ if (source.isSymbolicLink()) {
168
+ symlinkSync(readlinkSync(from), to);
169
+ unlinkSync(from);
170
+ return;
171
+ }
172
+ linkSync(from, to);
173
+ unlinkSync(from);
174
+ }
175
+
176
+ function isHealthyShimProbe(probe: StableShimPathProbe, platform: NodeJS.Platform): boolean {
177
+ if (probe.prefix.length < 180 || !probe.prefix.includes(SHIM_MARKER) || !probe.prefix.includes("ensure")) return false;
178
+ const mode = probe.fingerprint.target?.mode ?? probe.fingerprint.mode;
179
+ return platform === "win32" || (mode & 0o111) !== 0;
180
+ }
181
+
182
+ function isCurrentUnixShimProbe(probe: StableShimPathProbe): boolean {
183
+ return probe.prefix.includes(UNIX_SHIM_REVISION_MARKER);
184
+ }
185
+
186
+ function hasUsableBackingPath(file: ShimFileState): boolean {
187
+ return [existsSync(file.backupPath) ? file.backupPath : undefined, file.realPath]
188
+ .some(path => {
189
+ if (!path) return false;
190
+ const fingerprint = statFingerprint(path, true);
191
+ return fingerprint !== null && fingerprint.size > 0;
192
+ });
193
+ }
194
+
195
+ /**
196
+ * True when a Codex binary lives inside a version manager's install tree.
197
+ *
198
+ * These trees are rewritten in place on upgrade, which destroys both the shim
199
+ * and the sibling .opencodex-real backup it restores from (#2412). The tempting
200
+ * repair — adopt the newly installed binary as a fresh original — is wrong
201
+ * twice: it records a provenance that never happened, and the next upgrade wipes
202
+ * it again, so the repair silently un-repairs on the version manager's schedule.
203
+ *
204
+ * Scope is the three managers named in the report. nvm/fnm/npm-prefix are
205
+ * deliberately excluded: a false positive here refuses a restore that would
206
+ * otherwise be correct.
207
+ */
208
+ export function isVersionManagerOwnedCodexPath(
209
+ path: string,
210
+ platform: NodeJS.Platform = process.platform,
211
+ ): boolean {
212
+ const normalized = (platform === "win32"
213
+ ? win32.normalize(path).replace(/\\/g, "/")
214
+ : posix.normalize(path)).toLowerCase();
215
+ return normalized.includes("/mise/installs/")
216
+ || normalized.includes("/mise/shims/")
217
+ || normalized.includes("/.asdf/installs/")
218
+ || normalized.includes("/.asdf/shims/")
219
+ || normalized.includes("/.volta/");
220
+ }
221
+
222
+ export type { ShimPathFingerprint, StableShimPathProbe };
223
+ export { statFingerprint, sameFingerprint, sameFingerprintAfterRename, stableShimPathProbe, sameStableShimPathProbe, shimPathFingerprint, restoreWithoutReplacing, isHealthyShimProbe, isCurrentUnixShimProbe, hasUsableBackingPath };
@@ -0,0 +1,175 @@
1
+ import { lstatSync } from "node:fs";
2
+ import { extname, join, posix, win32 } from "node:path";
3
+ import { getConfigDir } from "../config";
4
+ import { fileErrorCode, readStateResult, stateFiles } from "./shim-state-file";
5
+ import {
6
+ isHealthyShimProbe,
7
+ isVersionManagerOwnedCodexPath,
8
+ shimPathFingerprint,
9
+ stableShimPathProbe,
10
+ statFingerprint,
11
+ type ShimPathFingerprint,
12
+ } from "./shim-fingerprint";
13
+ import { gitBashPath, psString, shQuote, windowsBatchSet } from "./shim-templates";
14
+
15
+ export type CodexShimBackingForCommand =
16
+ | Readonly<{ status: "not-tracked" }>
17
+ | Readonly<{
18
+ status: "matched";
19
+ selectedRole: "wrapper" | "backing";
20
+ backingPath: string;
21
+ backingKind: "backup" | "real";
22
+ }>
23
+ | Readonly<{
24
+ status: "unknown";
25
+ reason:
26
+ | "state_invalid"
27
+ | "platform_mismatch"
28
+ | "ambiguous_match"
29
+ | "preserve_only"
30
+ | "backing_missing"
31
+ | "backing_mismatch"
32
+ | "binding_unavailable"
33
+ | "wrapper_unhealthy"
34
+ | "version_manager_refused";
35
+ }>;
36
+
37
+ export function isLocalAbsoluteInspectionPath(path: string, platform: NodeJS.Platform): boolean {
38
+ if (platform !== "win32") return posix.isAbsolute(path);
39
+ const normalized = path.replace(/\//g, "\\");
40
+ // UNC and device namespaces can initiate remote I/O while a nominally local
41
+ // inspection is resolving user-controlled paths. Root-relative paths are
42
+ // drive-context dependent, so require an explicit local drive as well.
43
+ return win32.isAbsolute(path)
44
+ && /^[a-z]:\\/i.test(normalized)
45
+ && !normalized.startsWith("\\\\");
46
+ }
47
+
48
+ function windowsShimInspectionIsDeferred(platform: NodeJS.Platform): boolean {
49
+ return platform === "win32";
50
+ }
51
+
52
+ /** Resolve one selected command through already-recorded shim state, without repair. */
53
+ export function inspectCodexShimBackingForCommand(
54
+ selectedCommand: string,
55
+ platform: NodeJS.Platform = process.platform,
56
+ configDir: string = getConfigDir(),
57
+ ): CodexShimBackingForCommand {
58
+ // Pathname prechecks cannot prevent a writable Windows ancestor from being
59
+ // replaced with a remote reparse point before the later state/fingerprint
60
+ // reads. Keep the exported read-only helper fail-closed until those reads are
61
+ // performed through a handle-bound Windows provenance layer.
62
+ if (windowsShimInspectionIsDeferred(platform)) {
63
+ return Object.freeze({ status: "unknown" as const, reason: "binding_unavailable" as const });
64
+ }
65
+ if (!isLocalAbsoluteInspectionPath(configDir, platform)) {
66
+ return Object.freeze({ status: "unknown" as const, reason: "state_invalid" as const });
67
+ }
68
+ const stateFile = join(configDir, "codex-shim.json");
69
+ try {
70
+ const stateEntry = lstatSync(stateFile);
71
+ if (stateEntry.isSymbolicLink()) {
72
+ return Object.freeze({ status: "unknown" as const, reason: "state_invalid" as const });
73
+ }
74
+ } catch (error) {
75
+ if (fileErrorCode(error) !== "ENOENT") {
76
+ return Object.freeze({ status: "unknown" as const, reason: "state_invalid" as const });
77
+ }
78
+ }
79
+ const result = readStateResult(stateFile);
80
+ if (!result.state) {
81
+ return result.present
82
+ ? Object.freeze({ status: "unknown" as const, reason: "state_invalid" as const })
83
+ : Object.freeze({ status: "not-tracked" as const });
84
+ }
85
+ const pathApi = platform === "win32" ? win32 : posix;
86
+ const samePath = (left: string, right: string): boolean => {
87
+ const normalizedLeft = pathApi.resolve(left);
88
+ const normalizedRight = pathApi.resolve(right);
89
+ return platform === "win32"
90
+ ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
91
+ : normalizedLeft === normalizedRight;
92
+ };
93
+ const files = stateFiles(result.state);
94
+ if (files.some(file => !file.wrapperPath || !file.originalPath || !file.backupPath
95
+ || ![file.wrapperPath, file.originalPath, file.backupPath, file.realPath]
96
+ .filter((path): path is string => typeof path === "string")
97
+ .every(path => isLocalAbsoluteInspectionPath(path, platform)))) {
98
+ return Object.freeze({ status: "unknown" as const, reason: "state_invalid" as const });
99
+ }
100
+ const wrapperKeys = files.map(file => platform === "win32"
101
+ ? pathApi.resolve(file.wrapperPath).toLowerCase()
102
+ : pathApi.resolve(file.wrapperPath));
103
+ if (new Set(wrapperKeys).size !== wrapperKeys.length) {
104
+ return Object.freeze({ status: "unknown" as const, reason: "state_invalid" as const });
105
+ }
106
+ const selectedFingerprint = shimPathFingerprint(selectedCommand);
107
+ if (!selectedFingerprint) {
108
+ return Object.freeze({ status: "unknown" as const, reason: "binding_unavailable" as const });
109
+ }
110
+ const selectedIdentity = selectedFingerprint.target ?? selectedFingerprint;
111
+ const sameEffectiveIdentity = (fingerprint: ShimPathFingerprint | null): boolean => {
112
+ if (!fingerprint) return false;
113
+ const identity = fingerprint.target ?? fingerprint;
114
+ return identity.dev === selectedIdentity.dev && identity.ino === selectedIdentity.ino;
115
+ };
116
+ const matches = files.flatMap(file => {
117
+ const backingPath = file.realPath ?? file.backupPath;
118
+ const roles: Array<"wrapper" | "backing"> = [];
119
+ if (samePath(file.wrapperPath, selectedCommand)
120
+ || sameEffectiveIdentity(shimPathFingerprint(file.wrapperPath))) {
121
+ roles.push("wrapper");
122
+ }
123
+ if (samePath(backingPath, selectedCommand)
124
+ || sameEffectiveIdentity(shimPathFingerprint(backingPath))) {
125
+ roles.push("backing");
126
+ }
127
+ return roles.map(selectedRole => ({ file, backingPath, selectedRole }));
128
+ });
129
+ if (matches.length === 0) return Object.freeze({ status: "not-tracked" as const });
130
+ if (result.state.platform !== platform) {
131
+ return Object.freeze({ status: "unknown" as const, reason: "platform_mismatch" as const });
132
+ }
133
+ if (matches.length !== 1) {
134
+ return Object.freeze({ status: "unknown" as const, reason: "ambiguous_match" as const });
135
+ }
136
+ const { file, backingPath, selectedRole } = matches[0]!;
137
+ if (file.preserveOnly === true) {
138
+ return Object.freeze({ status: "unknown" as const, reason: "preserve_only" as const });
139
+ }
140
+ const backing = statFingerprint(backingPath, true);
141
+ if (!backing || backing.size <= 0 || samePath(backingPath, file.wrapperPath)) {
142
+ return Object.freeze({ status: "unknown" as const, reason: "backing_missing" as const });
143
+ }
144
+ const wrapperProbe = stableShimPathProbe(file.wrapperPath);
145
+ if (!wrapperProbe || !isHealthyShimProbe(wrapperProbe, result.state.platform)) {
146
+ return Object.freeze({
147
+ status: "unknown" as const,
148
+ reason: isVersionManagerOwnedCodexPath(file.wrapperPath)
149
+ ? "version_manager_refused" as const
150
+ : "wrapper_unhealthy" as const,
151
+ });
152
+ }
153
+ const wrapperIdentity = wrapperProbe.fingerprint.target ?? wrapperProbe.fingerprint;
154
+ if (backing.dev === wrapperIdentity.dev && backing.ino === wrapperIdentity.ino) {
155
+ return Object.freeze({ status: "unknown" as const, reason: "backing_mismatch" as const });
156
+ }
157
+ const wrapperExt = extname(file.wrapperPath).toLowerCase();
158
+ const invokesBacking = platform !== "win32"
159
+ ? wrapperProbe.prefix.includes(`exec ${shQuote(backingPath)} "$@"`)
160
+ : wrapperExt === ".cmd" || wrapperExt === ".bat"
161
+ ? wrapperProbe.prefix.includes(windowsBatchSet("OCX_REAL_CODEX", backingPath))
162
+ && wrapperProbe.prefix.includes('"%OCX_REAL_CODEX%" %*')
163
+ : wrapperExt === ".ps1"
164
+ ? wrapperProbe.prefix.includes(`& ${psString(backingPath)} @args`)
165
+ : wrapperProbe.prefix.includes(`exec ${shQuote(gitBashPath(backingPath))} "$@"`);
166
+ if (!invokesBacking) {
167
+ return Object.freeze({ status: "unknown" as const, reason: "backing_mismatch" as const });
168
+ }
169
+ return Object.freeze({
170
+ status: "matched" as const,
171
+ selectedRole,
172
+ backingPath,
173
+ backingKind: file.realPath !== undefined ? "real" as const : "backup" as const,
174
+ });
175
+ }