@bitkyc08/opencodex 2.58.0 → 2.60.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 (303) hide show
  1. package/README.md +28 -10
  2. package/gui/dist/assets/index-BTuCbqQd.css +1 -0
  3. package/gui/dist/assets/index-DoBVdPHP.js +134 -0
  4. package/gui/dist/index.html +2 -2
  5. package/gui/dist/provider-icons/crusoe.svg +1 -0
  6. package/gui/dist/provider-icons/opper.svg +3 -0
  7. package/package.json +4 -1
  8. package/src/adapters/anthropic-image-codec.ts +16 -2
  9. package/src/adapters/anthropic-image-normalize.ts +49 -2
  10. package/src/adapters/anthropic.ts +4 -1
  11. package/src/adapters/base.ts +34 -1
  12. package/src/adapters/coding-agent/turn.ts +22 -2
  13. package/src/adapters/command-code.ts +50 -3
  14. package/src/adapters/cursor/catalog.ts +11 -0
  15. package/src/adapters/cursor/checkpoint-store.ts +3 -0
  16. package/src/adapters/cursor/discovery.ts +11 -8
  17. package/src/adapters/cursor/effort-map.ts +16 -2
  18. package/src/adapters/cursor/envelope-echo.ts +55 -2
  19. package/src/adapters/cursor/live-transport.ts +26 -9
  20. package/src/adapters/cursor/message-mapper.ts +3 -2
  21. package/src/adapters/cursor/protobuf-request.ts +8 -5
  22. package/src/adapters/cursor/request-builder.ts +21 -4
  23. package/src/adapters/cursor/thread-continuity.ts +105 -31
  24. package/src/adapters/cursor/tool-guidance.ts +5 -4
  25. package/src/adapters/cursor/transport.ts +19 -0
  26. package/src/adapters/cursor.ts +45 -2
  27. package/src/adapters/devin/cloud-direct/chat.ts +14 -3
  28. package/src/adapters/devin/cloud-direct/index.ts +7 -0
  29. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +140 -0
  30. package/src/adapters/devin.ts +125 -26
  31. package/src/adapters/google-antigravity-replay.ts +1 -1
  32. package/src/adapters/google-antigravity-wire.ts +55 -4
  33. package/src/adapters/google-http.ts +57 -11
  34. package/src/adapters/google-tool-schema.ts +595 -31
  35. package/src/adapters/google-wire-compiler.ts +93 -10
  36. package/src/adapters/google-wire-shape.ts +461 -0
  37. package/src/adapters/google.ts +60 -10
  38. package/src/adapters/openai-chat/response-events.ts +61 -0
  39. package/src/adapters/openai-chat-images.ts +3 -1
  40. package/src/adapters/openai-chat.ts +10 -11
  41. package/src/adapters/openai-responses/image-gen.ts +8 -6
  42. package/src/adapters/openai-responses/passthrough.ts +25 -4
  43. package/src/adapters/openai-responses/reasoning.ts +7 -0
  44. package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
  45. package/src/adapters/openai-responses/tool-schema.ts +19 -7
  46. package/src/adapters/opencode-go-additional-tools.ts +12 -2
  47. package/src/adapters/responses-tool-schema.ts +76 -46
  48. package/src/adapters/run-turn-queue.ts +17 -4
  49. package/src/bridge/response-json.ts +1 -1
  50. package/src/bridge/sse.ts +28 -35
  51. package/src/claude/context-windows.ts +22 -0
  52. package/src/claude/outbound.ts +35 -4
  53. package/src/cli/account-api.ts +4 -3
  54. package/src/cli/account-extended.ts +26 -6
  55. package/src/cli/account-orca-import.ts +63 -0
  56. package/src/cli/account.ts +32 -4
  57. package/src/cli/capabilities.ts +40 -0
  58. package/src/cli/claude.ts +29 -1
  59. package/src/cli/codex-cli-update.ts +97 -2
  60. package/src/cli/dispatch.ts +57 -3
  61. package/src/cli/doctor.ts +218 -4
  62. package/src/cli/help.ts +4 -1
  63. package/src/cli/hub.ts +3 -2
  64. package/src/cli/index.ts +95 -22
  65. package/src/cli/models-runtime.ts +33 -4
  66. package/src/cli/opencode.ts +2 -2
  67. package/src/cli/provider.ts +13 -1
  68. package/src/cli/registry.ts +11 -1
  69. package/src/cli/runtime-api.ts +44 -0
  70. package/src/cli/start-args.ts +94 -0
  71. package/src/cli/system-command.ts +2 -0
  72. package/src/client/machine-api.ts +6 -5
  73. package/src/client/machine-listener.ts +16 -3
  74. package/src/client/runtime.ts +26 -2
  75. package/src/clients/config-export/constants.ts +2 -3
  76. package/src/clients/config-export.ts +5 -5
  77. package/src/codex/account-store.ts +146 -5
  78. package/src/codex/auth-api/account-list.ts +19 -11
  79. package/src/codex/auth-api/pool-quota-probe.ts +44 -10
  80. package/src/codex/auth-api/routes.ts +17 -2
  81. package/src/codex/auth-context.ts +16 -12
  82. package/src/codex/catalog/build-entries.ts +25 -4
  83. package/src/codex/catalog/derive-entry.ts +8 -1
  84. package/src/codex/catalog/effort.ts +10 -6
  85. package/src/codex/catalog/gather-capture.ts +22 -2
  86. package/src/codex/catalog/model-hints.ts +66 -33
  87. package/src/codex/catalog/parsing.ts +90 -5
  88. package/src/codex/catalog/provider-models.ts +19 -2
  89. package/src/codex/catalog/reserve-warn.ts +96 -0
  90. package/src/codex/catalog/retained-sync.ts +41 -26
  91. package/src/codex/catalog/routed-gather.ts +61 -3
  92. package/src/codex/cli-installation-identity.ts +210 -0
  93. package/src/codex/cli-installation-targets.ts +158 -0
  94. package/src/codex/context-compat.ts +5 -2
  95. package/src/codex/convergence.ts +5 -0
  96. package/src/codex/desired-state.ts +4 -1
  97. package/src/codex/history-job.ts +6 -6
  98. package/src/codex/history-provider.ts +24 -167
  99. package/src/codex/history-rollout-read.ts +174 -0
  100. package/src/codex/history-state-open.ts +105 -0
  101. package/src/codex/inject/config-toml.ts +44 -2
  102. package/src/codex/inject.ts +3 -2
  103. package/src/codex/internal/catalog-writer.ts +33 -1
  104. package/src/codex/lineage.ts +83 -32
  105. package/src/codex/loopback-target.ts +31 -0
  106. package/src/codex/main-account-hard-lock.ts +2 -1
  107. package/src/codex/main-account.ts +10 -3
  108. package/src/codex/main-device-reauth.ts +17 -9
  109. package/src/codex/model-cache.ts +47 -0
  110. package/src/codex/model-entitlements.ts +80 -2
  111. package/src/codex/observed-model-denials.ts +230 -0
  112. package/src/codex/orca-auth-source.ts +94 -0
  113. package/src/codex/orca-import.ts +219 -0
  114. package/src/codex/prompt-text-probe.ts +289 -16
  115. package/src/codex/quota-401-recovery.ts +12 -0
  116. package/src/codex/quota-types.ts +65 -0
  117. package/src/codex/quota.ts +24 -19
  118. package/src/codex/routing/cooldown-math.ts +8 -47
  119. package/src/codex/routing/pin-drain.ts +57 -0
  120. package/src/codex/routing.ts +20 -16
  121. package/src/codex/shim.ts +1 -1
  122. package/src/codex/subagent-model-fallback.ts +114 -2
  123. package/src/codex/windows-installation-files.ts +224 -0
  124. package/src/combos/failover.ts +125 -5
  125. package/src/config/admitted-identity.ts +222 -0
  126. package/src/config/diagnostics.ts +43 -1
  127. package/src/config/feature-flags.ts +5 -0
  128. package/src/config/load-degrade.ts +33 -0
  129. package/src/config/pending-teardown.ts +8 -0
  130. package/src/config/process-state.ts +36 -3
  131. package/src/config/provider-relative-send-path.ts +16 -0
  132. package/src/config/proxy-env.ts +31 -7
  133. package/src/config/schema/compaction-triggers.ts +11 -0
  134. package/src/config/schema/config-schema.ts +25 -0
  135. package/src/config/schema/leaf-validators.ts +76 -17
  136. package/src/config.ts +2 -2
  137. package/src/generated/compatibility-version.json +410 -258
  138. package/src/generated/model-metadata.ts +1 -1
  139. package/src/grok/reset-coupons.ts +38 -19
  140. package/src/images/loop.ts +6 -1
  141. package/src/integrations/aside-profile-context.ts +37 -3
  142. package/src/integrations/aside-profile-journal.ts +68 -3
  143. package/src/integrations/aside-profiles.ts +128 -3
  144. package/src/integrations/mutation-plan.ts +815 -0
  145. package/src/integrations/writer.ts +85 -99
  146. package/src/lab/live/transport.ts +4 -0
  147. package/src/lab/live/types.ts +5 -0
  148. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  149. package/src/lib/admin-secrets.ts +9 -1
  150. package/src/lib/bounded-body.ts +4 -2
  151. package/src/lib/debug-log-buffer.ts +6 -1
  152. package/src/lib/debug.ts +23 -0
  153. package/src/lib/destination-policy.ts +48 -6
  154. package/src/lib/errors.ts +82 -15
  155. package/src/lib/http-response-semantics.ts +57 -0
  156. package/src/lib/lab-live-pinned-sender.ts +26 -12
  157. package/src/lib/local-destinations.ts +32 -5
  158. package/src/lib/pinned-http.ts +142 -2
  159. package/src/lib/plain-data.ts +103 -0
  160. package/src/lib/process-control.ts +13 -5
  161. package/src/lib/provider-outbound.ts +53 -5
  162. package/src/lib/proxy-env.ts +70 -3
  163. package/src/lib/request-execution-budget.ts +11 -3
  164. package/src/lib/response-body-inactivity.ts +193 -0
  165. package/src/lib/retry-delay.ts +69 -0
  166. package/src/lib/socks5-fetch.ts +741 -0
  167. package/src/lib/spend-ledger-owner.ts +364 -0
  168. package/src/lib/spend-reservation-ledger.ts +332 -35
  169. package/src/lib/windows-system-proxy.ts +16 -11
  170. package/src/lib/workflow-budget.ts +145 -8
  171. package/src/oauth/account-quota-rank.ts +72 -15
  172. package/src/oauth/callback-server.ts +4 -3
  173. package/src/oauth/generic-account-failover.ts +41 -27
  174. package/src/oauth/health.ts +12 -1
  175. package/src/oauth/index.ts +3 -107
  176. package/src/oauth/login-flow-state.ts +127 -0
  177. package/src/oauth/orcarouter.ts +15 -2
  178. package/src/oauth/store.ts +8 -0
  179. package/src/providers/codex-capacity.ts +9 -0
  180. package/src/providers/derive.ts +34 -17
  181. package/src/providers/devin-cli-authmode-migration.ts +14 -10
  182. package/src/providers/devin-provider-merge-migration.ts +33 -12
  183. package/src/providers/free-directory.ts +20 -2
  184. package/src/providers/key-failover.ts +327 -25
  185. package/src/providers/model-rename-migration.ts +56 -1
  186. package/src/providers/model-rename-startup.ts +7 -5
  187. package/src/providers/openai-sidecar.ts +4 -0
  188. package/src/providers/openai-virtual-models.ts +42 -2
  189. package/src/providers/opencode-go-transport.ts +14 -5
  190. package/src/providers/quota/antigravity.ts +22 -2
  191. package/src/providers/quota/report-cache.ts +3 -0
  192. package/src/providers/quota/vendor-probes-key.ts +1 -1
  193. package/src/providers/registry/entries-core.ts +39 -17
  194. package/src/providers/registry/entries-extended.ts +131 -1
  195. package/src/providers/registry/model-ids.ts +168 -0
  196. package/src/providers/registry/model-seeds.ts +87 -21
  197. package/src/providers/registry/types.ts +2 -0
  198. package/src/providers/resolved-model-policy-merge.ts +167 -0
  199. package/src/providers/resolved-model-policy.ts +406 -0
  200. package/src/providers/stale-vision-classification-migration.ts +137 -0
  201. package/src/responses/apply-patch-envelope.ts +32 -11
  202. package/src/responses/bridge-search-replay-cache.ts +152 -0
  203. package/src/responses/code-mode-helper-compat.ts +26 -16
  204. package/src/responses/custom-tool-compat.ts +1 -1
  205. package/src/responses/freeform-wrapper-scan.ts +279 -0
  206. package/src/responses/hosted-tool-policy.ts +85 -2
  207. package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
  208. package/src/responses/progressive-freeform-input.ts +130 -0
  209. package/src/responses/reasoning-envelope.ts +30 -0
  210. package/src/responses/schema.ts +9 -2
  211. package/src/responses/state.ts +5 -12
  212. package/src/responses/tool-name-aliases.ts +15 -1
  213. package/src/router.ts +91 -115
  214. package/src/routing/compatibility/behavior.ts +9 -0
  215. package/src/routing/compatibility/subject.ts +16 -1
  216. package/src/server/adapter-resolve.ts +9 -0
  217. package/src/server/auth-cors.ts +29 -0
  218. package/src/server/chat-completions.ts +13 -5
  219. package/src/server/chat-native-sse.ts +26 -9
  220. package/src/server/chat-native.ts +10 -4
  221. package/src/server/claude-messages.ts +30 -5
  222. package/src/server/effort-row.ts +11 -3
  223. package/src/server/grok-responses-control-frame.ts +160 -1
  224. package/src/server/gui-static.ts +36 -2
  225. package/src/server/inbound-body-admission.ts +187 -0
  226. package/src/server/index/serve-options.ts +86 -29
  227. package/src/server/index/spend-ledger-lifecycle.ts +66 -0
  228. package/src/server/index/websocket-handler.ts +6 -1
  229. package/src/server/index.ts +24 -25
  230. package/src/server/management/api-access.ts +3 -4
  231. package/src/server/management/aside-profile-routes.ts +266 -7
  232. package/src/server/management/config-routes.ts +48 -7
  233. package/src/server/management/context.ts +3 -0
  234. package/src/server/management/integration-routes.ts +287 -5
  235. package/src/server/management/metrics-routes.ts +20 -0
  236. package/src/server/management/model-rows.ts +224 -12
  237. package/src/server/management/provider-capability-config.ts +35 -7
  238. package/src/server/management/provider-routes.ts +70 -18
  239. package/src/server/management/route-registry.ts +14 -0
  240. package/src/server/management/shared.ts +10 -3
  241. package/src/server/management/system-restart.ts +7 -2
  242. package/src/server/management/system-routes.ts +2 -0
  243. package/src/server/management/usage-aggregate-cache.ts +4 -0
  244. package/src/server/management-api.ts +2 -0
  245. package/src/server/management-auth.ts +15 -1
  246. package/src/server/proxy-liveness.ts +97 -2
  247. package/src/server/readiness.ts +29 -10
  248. package/src/server/relay-eager.ts +24 -2
  249. package/src/server/relay.ts +136 -34
  250. package/src/server/request-log.ts +80 -3
  251. package/src/server/request-metrics.ts +236 -0
  252. package/src/server/responses/adapter-continuation.ts +74 -30
  253. package/src/server/responses/adapter-delivery.ts +39 -8
  254. package/src/server/responses/adapter-dispatch.ts +63 -30
  255. package/src/server/responses/compact.ts +89 -18
  256. package/src/server/responses/compaction-routing.ts +111 -0
  257. package/src/server/responses/core-codex-account.ts +90 -24
  258. package/src/server/responses/core-combo.ts +7 -7
  259. package/src/server/responses/core-normalize.ts +16 -15
  260. package/src/server/responses/core-opaque-recovery.ts +1 -0
  261. package/src/server/responses/core-options.ts +4 -0
  262. package/src/server/responses/encrypted-payload.ts +20 -2
  263. package/src/server/responses/fetch-helpers.ts +68 -2
  264. package/src/server/responses/passthrough-delivery.ts +41 -15
  265. package/src/server/responses/passthrough-dispatch.ts +145 -53
  266. package/src/server/responses/passthrough-execution.ts +11 -1
  267. package/src/server/responses/policy-fallback.ts +5 -13
  268. package/src/server/responses/request-prepare.ts +96 -17
  269. package/src/server/responses/request-send-budget.ts +89 -8
  270. package/src/server/responses/request-sidecar-auth.ts +17 -9
  271. package/src/server/responses/request-spend.ts +38 -9
  272. package/src/server/responses/request-transport.ts +15 -12
  273. package/src/server/responses/run-turn-execution.ts +45 -8
  274. package/src/server/responses/sidecar-execution.ts +19 -2
  275. package/src/server/responses/ws-upstream.ts +16 -28
  276. package/src/server/responses-custom-tool-repair.ts +29 -56
  277. package/src/server/responses-undeclared-tool-guard.ts +31 -1
  278. package/src/server/sse-frame-buffer.ts +12 -10
  279. package/src/server/sse-payload-rewrite.ts +37 -10
  280. package/src/server/system-env-shell.ts +5 -1
  281. package/src/server/system-env.ts +7 -1
  282. package/src/server/workflow-refusal.ts +56 -2
  283. package/src/service/cli.ts +16 -6
  284. package/src/service/guards.ts +10 -0
  285. package/src/service/health.ts +43 -0
  286. package/src/service/state.ts +7 -2
  287. package/src/tray/windows-tray.ps1 +155 -3
  288. package/src/types/accounts.ts +4 -0
  289. package/src/types/config.ts +111 -6
  290. package/src/types/provider.ts +37 -0
  291. package/src/types/request.ts +9 -1
  292. package/src/types/tools.ts +14 -0
  293. package/src/types/wire.ts +9 -1
  294. package/src/types.ts +1 -0
  295. package/src/usage/expected-prices.ts +28 -0
  296. package/src/usage/log.ts +87 -4
  297. package/src/vision/eligibility.ts +88 -9
  298. package/src/vision/plan.ts +34 -10
  299. package/src/web-search/executor.ts +41 -2
  300. package/src/web-search/loop.ts +6 -1
  301. package/src/web-search/passthrough-bridge.ts +39 -5
  302. package/gui/dist/assets/index-BbrHOIY0.js +0 -128
  303. package/gui/dist/assets/index-C5-RdDmD.css +0 -1
@@ -0,0 +1,158 @@
1
+ import { closeSync, existsSync, openSync, readSync } from "node:fs";
2
+ import { win32 } from "node:path";
3
+ import { SHIM_MARKER } from "./shim-templates";
4
+ import type { CodexCliInstallationIdentityInput } from "./cli-installation-identity";
5
+
6
+ /**
7
+ * The proof-captured launcher snapshot used to identify the selected candidate.
8
+ * Every field is optional because older launchers and direct Bun/source runs
9
+ * supply no snapshot at all; derivation fails closed rather than reading the
10
+ * ambient environment.
11
+ */
12
+ export interface CodexCliInstallationSnapshot {
13
+ readonly codexCliPath?: string | null;
14
+ readonly path?: string | null;
15
+ readonly pathExt?: string | null;
16
+ }
17
+
18
+ export type CodexCliInstallationTargetRefusal =
19
+ | "unsupported_platform"
20
+ | "candidate_unavailable"
21
+ | "unsupported_layout"
22
+ | "toolchain_unresolved";
23
+
24
+ export type CodexCliInstallationTargetDerivation =
25
+ | { readonly kind: "derived"; readonly input: CodexCliInstallationIdentityInput }
26
+ | { readonly kind: "unavailable"; readonly reason: CodexCliInstallationTargetRefusal };
27
+
28
+ export interface CodexCliInstallationTargetDeps {
29
+ readonly platform?: NodeJS.Platform;
30
+ readonly exists?: (path: string) => boolean;
31
+ /** Bounded prefix read used only to recognize an OpenCodex-owned wrapper. */
32
+ readonly fileContains?: (path: string, marker: string) => boolean;
33
+ }
34
+
35
+ const DEFAULT_PATH_EXT = ".COM;.EXE;.BAT;.CMD;.PS1";
36
+ const SHIM_PROBE_BYTES = 8 * 1024;
37
+ const CODEX_PACKAGE_SUFFIX = "\\node_modules\\@openai\\codex\\bin\\codex.js";
38
+
39
+ function defaultFileContains(path: string, marker: string): boolean {
40
+ let descriptor: number | undefined;
41
+ let contains = false;
42
+ try {
43
+ descriptor = openSync(path, "r");
44
+ const bytes = Buffer.allocUnsafe(SHIM_PROBE_BYTES);
45
+ const count = readSync(descriptor, bytes, 0, bytes.length, 0);
46
+ contains = bytes.subarray(0, count).toString("utf8").includes(marker);
47
+ } catch {
48
+ contains = false;
49
+ } finally {
50
+ if (descriptor !== undefined) {
51
+ try { closeSync(descriptor); } catch { contains = false; }
52
+ }
53
+ }
54
+ return contains;
55
+ }
56
+
57
+ /**
58
+ * First match wins, mirroring PATH resolution: directories in order, and within
59
+ * each directory every PATHEXT suffix in order (or the exact name when it
60
+ * already carries an extension). Skipping a hit to keep scanning would attest
61
+ * something other than the launcher that actually resolves.
62
+ */
63
+ function scanPath(
64
+ name: string,
65
+ pathValue: string | null | undefined,
66
+ pathExt: string | null | undefined,
67
+ exists: (path: string) => boolean,
68
+ ): string | null {
69
+ const extensions = (pathExt ?? DEFAULT_PATH_EXT).split(";").map(value => value.trim()).filter(Boolean);
70
+ const names = /\.[a-z0-9]+$/i.test(name) ? [name] : extensions.map(ext => name + ext.toLowerCase());
71
+ for (const entry of (pathValue ?? "").split(";")) {
72
+ const dir = entry.trim().replace(/^"+|"+$/g, "");
73
+ if (!dir) continue;
74
+ for (const candidateName of names) {
75
+ const candidate = win32.join(dir, candidateName);
76
+ if (exists(candidate)) return candidate;
77
+ }
78
+ }
79
+ return null;
80
+ }
81
+
82
+ /**
83
+ * Derive the four attestation inputs from the proof-captured launcher snapshot.
84
+ * Discovery only proposes candidate paths; the held-handle observation remains
85
+ * the authority, so a stale or racing probe can only produce a refusal, never a
86
+ * false identity. No ambient environment is read: without a snapshot the result
87
+ * is unavailable rather than silently trusting the child's environment.
88
+ */
89
+ export function deriveCodexCliInstallationInput(
90
+ snapshot: CodexCliInstallationSnapshot,
91
+ deps: CodexCliInstallationTargetDeps = {},
92
+ ): CodexCliInstallationTargetDerivation {
93
+ if ((deps.platform ?? process.platform) !== "win32") {
94
+ return { kind: "unavailable", reason: "unsupported_platform" };
95
+ }
96
+ const exists = deps.exists ?? existsSync;
97
+ const fileContains = deps.fileContains ?? defaultFileContains;
98
+ const configured = snapshot.codexCliPath;
99
+
100
+ let candidate: string | null;
101
+ if (configured) {
102
+ if (/^[a-z]:[\\/]/i.test(configured)) {
103
+ if (!exists(configured)) return { kind: "unavailable", reason: "candidate_unavailable" };
104
+ candidate = win32.normalize(configured);
105
+ } else {
106
+ if (configured.includes("/") || configured.includes("\\")) {
107
+ return { kind: "unavailable", reason: "candidate_unavailable" };
108
+ }
109
+ candidate = scanPath(configured, snapshot.path, snapshot.pathExt, exists);
110
+ if (!candidate) return { kind: "unavailable", reason: "candidate_unavailable" };
111
+ }
112
+ } else {
113
+ candidate = scanPath("codex", snapshot.path, snapshot.pathExt, exists);
114
+ if (!candidate) return { kind: "unavailable", reason: "candidate_unavailable" };
115
+ }
116
+
117
+ // An OpenCodex wrapper at the npm prefix is our own launcher, not the npm
118
+ // artifact. The renamed original beside it is the file npm wrote.
119
+ if (/\.cmd$/i.test(candidate) && fileContains(candidate, SHIM_MARKER)) {
120
+ const backing = candidate.slice(0, -".cmd".length) + ".opencodex-real.cmd";
121
+ if (!exists(backing)) return { kind: "unavailable", reason: "unsupported_layout" };
122
+ candidate = backing;
123
+ }
124
+
125
+ const base = win32.basename(candidate).toLowerCase();
126
+ let prefix: string;
127
+ if (base === "codex.cmd" || base === "codex.opencodex-real.cmd") {
128
+ prefix = win32.dirname(candidate);
129
+ } else if (candidate.toLowerCase().endsWith(CODEX_PACKAGE_SUFFIX)) {
130
+ prefix = candidate.slice(0, candidate.length - CODEX_PACKAGE_SUFFIX.length);
131
+ } else {
132
+ return { kind: "unavailable", reason: "unsupported_layout" };
133
+ }
134
+ if (!exists(win32.join(prefix, "node_modules", "@openai", "codex", "package.json"))) {
135
+ return { kind: "unavailable", reason: "unsupported_layout" };
136
+ }
137
+
138
+ // The npm cmd-shim itself prefers %dp0%\node.exe before falling back to PATH.
139
+ let node = win32.join(prefix, "node.exe");
140
+ if (!exists(node)) {
141
+ const resolved = scanPath("node.exe", snapshot.path, null, exists);
142
+ if (!resolved) return { kind: "unavailable", reason: "toolchain_unresolved" };
143
+ node = resolved;
144
+ }
145
+ const npmCli = win32.join(win32.dirname(node), "node_modules", "npm", "bin", "npm-cli.js");
146
+ if (!exists(npmCli)) return { kind: "unavailable", reason: "toolchain_unresolved" };
147
+
148
+ return {
149
+ kind: "derived",
150
+ input: Object.freeze({
151
+ candidate,
152
+ npmPrefix: prefix,
153
+ npmCli,
154
+ node,
155
+ candidateSource: "selected",
156
+ }),
157
+ };
158
+ }
@@ -7,14 +7,17 @@ export const CONTEXT_BACKEND_PREFIX = "/backend-api/codex";
7
7
 
8
8
  /** The single definition of the opt-in, shared by injection and the runtime gate. */
9
9
  export function contextExperimentalEnabled(configContent: string): boolean {
10
- let parsed: { features?: { context_management?: { experimental_mode?: boolean } } };
10
+ let parsed: { features?: { context_management?: boolean | { experimental_mode?: boolean } } };
11
11
  try {
12
12
  parsed = Bun.TOML.parse(configContent) as typeof parsed;
13
13
  } catch {
14
14
  // Injection tolerates incomplete user config; malformed TOML is not an opt-in.
15
15
  return false;
16
16
  }
17
- return parsed.features?.context_management?.experimental_mode === true;
17
+ // Codex FeatureToml accepts either a boolean or the experimental_mode table.
18
+ // Keep strict true checks: malformed values must not activate a credentialed relay.
19
+ const setting = parsed.features?.context_management;
20
+ return setting === true || (typeof setting === "object" && setting?.experimental_mode === true);
18
21
  }
19
22
 
20
23
  let activation: { key: string; active: boolean } | undefined;
@@ -36,6 +36,7 @@ import {
36
36
  catalogHasRoutedEntries,
37
37
  findSupportedNativeTemplate,
38
38
  legacyCatalogBackupPath,
39
+ nativeMultiAgentDefaults,
39
40
  parseCatalogJson,
40
41
  type RawCatalog,
41
42
  type RawEntry,
@@ -69,6 +70,7 @@ import {
69
70
  clampCatalogModelsToObservedCodexSupport,
70
71
  supportedCodexReasoningEffortsFromObservedCatalog,
71
72
  } from "./catalog/effort";
73
+ import { suppressedSyntheticMaxCatalogSlugs } from "./catalog/model-hints";
72
74
  import { codexRuntimeStatePath, peekCodexRuntimeProcessCache } from "./runtime";
73
75
  import { codexAccountNamespaceEntries, isMainCodexAccountTarget } from "./account-namespaces";
74
76
  import { MAIN_CODEX_ACCOUNT_ID } from "./main-account";
@@ -305,6 +307,7 @@ function prepareCatalog(
305
307
  [catalog.models ?? [], ...nativeRecoverySources],
306
308
  );
307
309
  const catalogModels = nativeCatalogModels;
310
+ const suppressedSyntheticMaxSlugs = suppressedSyntheticMaxCatalogSlugs(config, ordered, catalogModels);
308
311
  const routedEntries = buildCatalogEntriesFromObservedState({
309
312
  template: template ? JSON.parse(JSON.stringify(template)) : null,
310
313
  gptSlugs: [],
@@ -371,8 +374,10 @@ function prepareCatalog(
371
374
  includeNativeOpenAi,
372
375
  accountBoundEntries,
373
376
  suppressedBareNativeSlugs,
377
+ suppressedSyntheticMaxSlugs,
374
378
  openaiContextCap,
375
379
  nativeDisplayNames: config.providers[OPENAI_CODEX_PROVIDER_ID]?.modelDisplayNames,
380
+ nativeMultiAgentDefaults: nativeMultiAgentDefaults(baselineCatalogModels),
376
381
  policy: {
377
382
  ...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY,
378
383
  nativeBackfillSlugs: [...availableBareNativeSlugs, ...observedNativeSlugs],
@@ -260,7 +260,10 @@ export async function syncCodexOnStartIfEnabled(
260
260
  // The `.catch` is deliberate and stays: a failure to APPLY must not stop the
261
261
  // proxy from coming up. A failed sync simply reports no writes. The readiness
262
262
  // gate observes the real outcome so /readyz reflects the sync state exactly as
263
- // the PR contract defines (ready only on ok=true with no warning).
263
+ // the contract defines: ready on ok=true once the sync has settled. A nonempty
264
+ // `warning` is a local-Codex-artifact degradation the sync chose to continue
265
+ // past, and #5181 is what it cost to treat it as terminal — a healthy
266
+ // multi-provider proxy reported itself permanently unready.
264
267
  const outcome = readinessGate
265
268
  ? await runStartupReadinessSync(readinessGate, async () => (await sync(port)) ?? null)
266
269
  : await sync(port).catch(() => undefined);
@@ -23,7 +23,7 @@ import type {
23
23
  CodexHistoryWorkerOperation,
24
24
  HistoryWorkerResult,
25
25
  } from "./history-worker";
26
- import { currentHistoryDbBusyTimeoutMs, historyBackupPathFor } from "./history-provider";
26
+ import { currentHistoryDbBusyTimeoutMs, resolveExistingHistoryBackupPath } from "./history-provider";
27
27
  import type { CodexHistoryFailureReason, CodexHistoryVerifiedNoopProof } from "./history-provider";
28
28
  import { getCodexHome, resolveCodexStateDbPath } from "./paths";
29
29
 
@@ -32,7 +32,7 @@ import { getCodexHome, resolveCodexStateDbPath } from "./paths";
32
32
  *
33
33
  * The SQLite root can differ from CODEX_HOME and both environment/config inputs
34
34
  * can change between invocations. The parent resolves one exact target and hands
35
- * those canonical paths to the Worker rather than asking the Worker to infer a
35
+ * those resolved paths to the Worker rather than asking the Worker to infer a
36
36
  * possibly different environment.
37
37
  */
38
38
  export function resolveCodexHistoryJobTarget(): {
@@ -45,10 +45,10 @@ export function resolveCodexHistoryJobTarget(): {
45
45
  return {
46
46
  canonicalCodexHome: home,
47
47
  canonicalStateDbPath: stateDb,
48
- // Derived by the provider's own rule rather than guessed: the manifest lives
49
- // in the config directory under a hash of the state database, so a
50
- // hand-built path would address a different file entirely.
51
- canonicalBackupPath: historyBackupPathFor(stateDb),
48
+ // Resolve through the provider's canonical-first compatibility rule. Passing
49
+ // only the newly normalized name would hide a pre-#4442 Windows manifest from
50
+ // the Worker and make an upgrade look like an empty backup.
51
+ canonicalBackupPath: resolveExistingHistoryBackupPath(stateDb),
52
52
  };
53
53
  }
54
54
 
@@ -1,9 +1,11 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { closeSync, constants, existsSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, statSync, unlinkSync, writeSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
- import { zstdDecompressSync } from "node:zlib";
4
+ import { parseSessionMetaLine, type ParsedSessionMeta } from "./history-rollout-read";
5
+ export { MAX_ROLLOUT_ZST_DECOMPRESSED_BYTES, readThreadFieldsFromRollout, type RolloutThreadFields } from "./history-rollout-read";
5
6
  import { Database } from "bun:sqlite";
6
7
  import { resolveCodexStateDbPath } from "./paths";
8
+ import { openCodexStateForPreflight } from "./history-state-open";
7
9
  import { atomicWriteFile, getConfigDir } from "../config";
8
10
  import {
9
11
  CODEX_HISTORY_RESUMABLE_SOURCES,
@@ -15,13 +17,6 @@ import {
15
17
  type CodexHistoryBackupManifest,
16
18
  } from "./history-manifest";
17
19
 
18
- /**
19
- * Cap for decompressing a lone `.jsonl.zst` rollout during quarantine restore.
20
- * Bounds peak memory while reconstructing thread rows; never write the decoded
21
- * JSONL to disk.
22
- */
23
- export const MAX_ROLLOUT_ZST_DECOMPRESSED_BYTES = 64 * 1024 * 1024;
24
-
25
20
  /**
26
21
  * The manifest that shadows one state database.
27
22
  *
@@ -433,23 +428,39 @@ export function preflightCodexHistoryInjection(
433
428
  if (!existsSync(resolvedPath)) {
434
429
  return restoreEntries.length > 0 ? "history_state_database_missing" : null;
435
430
  }
436
- db = new Database(resolvedPath, { readonly: true });
431
+ // Read-only, and narrowed so a cleanly-closed WAL store is inspected rather than refused
432
+ // (#4943). The open order is the safety property; see history-state-open.ts.
433
+ db = openCodexStateForPreflight(resolvedPath);
437
434
  const columns = db.query<{ name: string }, []>("PRAGMA table_info(threads)").all();
438
435
  const paginatedColumn = columns.some(column => column.name === "history_mode");
439
436
  if (paginatedColumn && restoreEntries.length > 0) return HISTORY_RELABEL_STANDS_DOWN;
440
437
  for (const entry of restoreEntries) assertLegacyHistoryWritable(entry.rolloutPath);
441
- const rows = db.query<{ rollout_path: string; history_mode: string | null }, []>(`
442
- SELECT rollout_path, ${paginatedColumn ? "history_mode" : "NULL AS history_mode"}
438
+ const rows = db.query<{ rollout_path: string; history_mode: string | null; model_provider: string }, []>(`
439
+ SELECT rollout_path, ${paginatedColumn ? "history_mode" : "NULL AS history_mode"}, model_provider
443
440
  FROM threads
444
441
  WHERE ${providerTableMode
445
442
  ? resumeHistory ? "model_provider IN ('openai', 'opencodex')" : "0"
446
443
  : "model_provider = 'opencodex'"}
447
444
  `).all();
445
+ // No ORDER BY: a paginated opencodex row can precede a paginated openai one, so the
446
+ // verdict waits for the full scan instead of standing down on the first paginated row.
447
+ let foundPaginatedRow = false;
448
+ let foundPaginatedOpenaiRow = false;
448
449
  for (const row of rows) {
449
- if (paginatedColumn || row.history_mode === "paginated") return HISTORY_RELABEL_STANDS_DOWN;
450
+ if (paginatedColumn || row.history_mode === "paginated") {
451
+ foundPaginatedRow = true;
452
+ // A provider-table transition removes the root openai_base_url, and a row already
453
+ // paginated cannot be relabeled: standing it down would route the openai-tagged
454
+ // thread to Codex's built-in OpenAI endpoint. A still-legacy row only stands down.
455
+ if (providerTableMode && row.history_mode === "paginated" && row.model_provider === "openai") {
456
+ foundPaginatedOpenaiRow = true;
457
+ }
458
+ continue;
459
+ }
450
460
  assertLegacyHistoryWritable(row.rollout_path);
451
461
  }
452
- return null;
462
+ if (foundPaginatedOpenaiRow) return "history_paginated_openai_requires_native_writer";
463
+ return foundPaginatedRow ? HISTORY_RELABEL_STANDS_DOWN : null;
453
464
  } catch (error) {
454
465
  return error instanceof CodexHistoryIntegrityError
455
466
  ? error.message
@@ -1018,24 +1029,6 @@ function assertRestoreReadback(
1018
1029
  }
1019
1030
  }
1020
1031
 
1021
- interface ParsedSessionMeta {
1022
- record: { type?: unknown; timestamp?: unknown; payload: { model_provider?: unknown; source?: unknown } & Record<string, unknown> };
1023
- }
1024
-
1025
- /** Parse one JSONL line into a `session_meta` record, or null if it isn't one. */
1026
- function parseSessionMetaLine(line: string): ParsedSessionMeta | null {
1027
- let parsed: unknown;
1028
- try {
1029
- parsed = JSON.parse(line);
1030
- } catch {
1031
- return null;
1032
- }
1033
- if (!parsed || typeof parsed !== "object") return null;
1034
- const record = parsed as ParsedSessionMeta["record"];
1035
- if (record.type !== "session_meta" || !record.payload || typeof record.payload !== "object") return null;
1036
- return { record };
1037
- }
1038
-
1039
1032
  /**
1040
1033
  * Find the LAST `session_meta` line in a rollout, mirroring the app's last-writer-wins fold
1041
1034
  * (codex-rs `apply_session_meta_from_item`). We base our patch on the most recent metadata so we
@@ -1104,142 +1097,6 @@ function compensateConcurrentSessionMetaAppend(
1104
1097
  }
1105
1098
  }
1106
1099
 
1107
- /**
1108
- * Fields needed to re-insert a production-shaped `threads` row from a rollout JSONL when a
1109
- * Phase-2 quarantine predates full `satellite-backup.json` thread snapshots.
1110
- *
1111
- * Uses the same last-writer-wins `session_meta` fold as {@link readLatestSessionMeta}, plus the
1112
- * first user-message preview (codex-rs `list.rs` / `EventMsg::UserMessage` path).
1113
- */
1114
- export interface RolloutThreadFields {
1115
- id: string;
1116
- modelProvider: string;
1117
- source: string;
1118
- firstUserMessage: string;
1119
- hasUserEvent: number;
1120
- cwd?: string;
1121
- historyMode?: string;
1122
- cliVersion?: string;
1123
- }
1124
-
1125
- function textFromContentParts(content: unknown): string | null {
1126
- if (typeof content === "string" && content.trim()) return content.trim();
1127
- if (!Array.isArray(content)) return null;
1128
- const parts: string[] = [];
1129
- for (const part of content) {
1130
- if (!part || typeof part !== "object") continue;
1131
- const p = part as Record<string, unknown>;
1132
- if (typeof p.text === "string" && p.text.trim()) parts.push(p.text.trim());
1133
- else if (typeof p.input_text === "string" && p.input_text.trim()) parts.push(p.input_text.trim());
1134
- }
1135
- const joined = parts.join("\n").trim();
1136
- return joined || null;
1137
- }
1138
-
1139
- /** Extract the first user-message preview from a rollout line, or null. */
1140
- function extractUserMessagePreview(line: string): string | null {
1141
- let parsed: unknown;
1142
- try {
1143
- parsed = JSON.parse(line);
1144
- } catch {
1145
- return null;
1146
- }
1147
- if (!parsed || typeof parsed !== "object") return null;
1148
- const record = parsed as { type?: unknown; payload?: unknown };
1149
- const payload = record.payload;
1150
- if (!payload || typeof payload !== "object") return null;
1151
- const p = payload as Record<string, unknown>;
1152
-
1153
- if (record.type === "event_msg") {
1154
- // codex-rs EventMsg::UserMessage — payload.type is "user_message" (or omitted in fixtures).
1155
- if (p.type === "user_message" || typeof p.message === "string") {
1156
- if (typeof p.message === "string" && p.message.trim()) return p.message.trim();
1157
- const fromContent = textFromContentParts(p.content);
1158
- if (fromContent) return fromContent;
1159
- }
1160
- return null;
1161
- }
1162
-
1163
- if (record.type === "response_item") {
1164
- if (p.type === "message" && p.role === "user") {
1165
- return textFromContentParts(p.content);
1166
- }
1167
- }
1168
- return null;
1169
- }
1170
-
1171
- /**
1172
- * Reconstruct thread identity + listing fields from a staged/restored rollout JSONL.
1173
- * Returns null when the file is missing or has no parseable `session_meta`.
1174
- *
1175
- * Accepts plain `.jsonl` or a lone `.jsonl.zst` (legacy Phase-2 quarantine). Compressed
1176
- * rollouts are decompressed in memory with {@link MAX_ROLLOUT_ZST_DECOMPRESSED_BYTES};
1177
- * no decompressed copy is written to disk.
1178
- */
1179
- export function readThreadFieldsFromRollout(path: string): RolloutThreadFields | null {
1180
- if (!path || !existsSync(path)) return null;
1181
- let raw: string;
1182
- try {
1183
- raw = path.endsWith(".zst")
1184
- ? decompressRolloutZstUtf8(path)
1185
- : readFileSync(path, "utf8");
1186
- } catch {
1187
- return null;
1188
- }
1189
- return parseThreadFieldsFromRolloutText(raw);
1190
- }
1191
-
1192
- function decompressRolloutZstUtf8(
1193
- path: string,
1194
- maxBytes: number = MAX_ROLLOUT_ZST_DECOMPRESSED_BYTES,
1195
- ): string {
1196
- const compressed = readFileSync(path);
1197
- const decoded = zstdDecompressSync(compressed as Uint8Array<ArrayBuffer>, {
1198
- maxOutputLength: maxBytes,
1199
- });
1200
- if (decoded.byteLength > maxBytes) {
1201
- throw new Error("rollout_zst_too_large");
1202
- }
1203
- return new TextDecoder().decode(decoded);
1204
- }
1205
-
1206
- function parseThreadFieldsFromRolloutText(raw: string): RolloutThreadFields | null {
1207
- const lines = raw.split("\n");
1208
- let latest: ParsedSessionMeta | null = null;
1209
- let firstUserMessage = "";
1210
- for (const line of lines) {
1211
- if (!line) continue;
1212
- if (line.includes("\"session_meta\"")) {
1213
- const meta = parseSessionMetaLine(line);
1214
- if (meta) latest = meta;
1215
- }
1216
- if (!firstUserMessage) {
1217
- const preview = extractUserMessagePreview(line);
1218
- if (preview) firstUserMessage = preview;
1219
- }
1220
- }
1221
- if (!latest) return null;
1222
- const payload = latest.record.payload;
1223
- const id = typeof payload.id === "string" ? payload.id : "";
1224
- if (!id) return null;
1225
- const modelProvider = typeof payload.model_provider === "string" && payload.model_provider
1226
- ? payload.model_provider
1227
- : "openai";
1228
- const source = typeof payload.source === "string" && payload.source
1229
- ? payload.source
1230
- : "cli";
1231
- return {
1232
- id,
1233
- modelProvider,
1234
- source,
1235
- firstUserMessage,
1236
- hasUserEvent: firstUserMessage.trim() ? 1 : 0,
1237
- ...(typeof payload.cwd === "string" ? { cwd: payload.cwd } : {}),
1238
- ...(typeof payload.history_mode === "string" ? { historyMode: payload.history_mode } : {}),
1239
- ...(typeof payload.cli_version === "string" ? { cliVersion: payload.cli_version } : {}),
1240
- };
1241
- }
1242
-
1243
1100
  /**
1244
1101
  * Make a thread's rollout reflect a provider/source change by APPENDING a new `session_meta` line,
1245
1102
  * rather than rewriting line 1. The appended line clones the latest metadata payload (so no field
@@ -0,0 +1,174 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { zstdDecompressSync } from "node:zlib";
3
+
4
+ /**
5
+ * Reading side of Codex rollout JSONL files: the `session_meta` fold and the thread fields a
6
+ * quarantine restore reconstructs from a rollout.
7
+ *
8
+ * Split out of `history-provider.ts` when that file crossed the repository file-size ratchet at
9
+ * 2009 lines. The cut follows a real seam rather than a line count: everything here reads a
10
+ * rollout file and returns plain data, while the module it left keeps the database and manifest
11
+ * writes. Nothing here opens a database or touches a backup manifest.
12
+ *
13
+ * `history-provider.ts` re-exports the public names, so existing importers are unaffected.
14
+ */
15
+ /**
16
+ * Cap for decompressing a lone `.jsonl.zst` rollout during quarantine restore.
17
+ * Bounds peak memory while reconstructing thread rows; never write the decoded
18
+ * JSONL to disk.
19
+ */
20
+ export const MAX_ROLLOUT_ZST_DECOMPRESSED_BYTES = 64 * 1024 * 1024;
21
+
22
+ export interface ParsedSessionMeta {
23
+ record: { type?: unknown; timestamp?: unknown; payload: { model_provider?: unknown; source?: unknown } & Record<string, unknown> };
24
+ }
25
+
26
+ /** Parse one JSONL line into a `session_meta` record, or null if it isn't one. */
27
+ export function parseSessionMetaLine(line: string): ParsedSessionMeta | null {
28
+ let parsed: unknown;
29
+ try {
30
+ parsed = JSON.parse(line);
31
+ } catch {
32
+ return null;
33
+ }
34
+ if (!parsed || typeof parsed !== "object") return null;
35
+ const record = parsed as ParsedSessionMeta["record"];
36
+ if (record.type !== "session_meta" || !record.payload || typeof record.payload !== "object") return null;
37
+ return { record };
38
+ }
39
+
40
+ /**
41
+ * Fields needed to re-insert a production-shaped `threads` row from a rollout JSONL when a
42
+ * Phase-2 quarantine predates full `satellite-backup.json` thread snapshots.
43
+ *
44
+ * Uses the same last-writer-wins `session_meta` fold as {@link readLatestSessionMeta}, plus the
45
+ * first user-message preview (codex-rs `list.rs` / `EventMsg::UserMessage` path).
46
+ */
47
+ export interface RolloutThreadFields {
48
+ id: string;
49
+ modelProvider: string;
50
+ source: string;
51
+ firstUserMessage: string;
52
+ hasUserEvent: number;
53
+ cwd?: string;
54
+ historyMode?: string;
55
+ cliVersion?: string;
56
+ }
57
+
58
+ function textFromContentParts(content: unknown): string | null {
59
+ if (typeof content === "string" && content.trim()) return content.trim();
60
+ if (!Array.isArray(content)) return null;
61
+ const parts: string[] = [];
62
+ for (const part of content) {
63
+ if (!part || typeof part !== "object") continue;
64
+ const p = part as Record<string, unknown>;
65
+ if (typeof p.text === "string" && p.text.trim()) parts.push(p.text.trim());
66
+ else if (typeof p.input_text === "string" && p.input_text.trim()) parts.push(p.input_text.trim());
67
+ }
68
+ const joined = parts.join("\n").trim();
69
+ return joined || null;
70
+ }
71
+
72
+ /** Extract the first user-message preview from a rollout line, or null. */
73
+ function extractUserMessagePreview(line: string): string | null {
74
+ let parsed: unknown;
75
+ try {
76
+ parsed = JSON.parse(line);
77
+ } catch {
78
+ return null;
79
+ }
80
+ if (!parsed || typeof parsed !== "object") return null;
81
+ const record = parsed as { type?: unknown; payload?: unknown };
82
+ const payload = record.payload;
83
+ if (!payload || typeof payload !== "object") return null;
84
+ const p = payload as Record<string, unknown>;
85
+
86
+ if (record.type === "event_msg") {
87
+ // codex-rs EventMsg::UserMessage — payload.type is "user_message" (or omitted in fixtures).
88
+ if (p.type === "user_message" || typeof p.message === "string") {
89
+ if (typeof p.message === "string" && p.message.trim()) return p.message.trim();
90
+ const fromContent = textFromContentParts(p.content);
91
+ if (fromContent) return fromContent;
92
+ }
93
+ return null;
94
+ }
95
+
96
+ if (record.type === "response_item") {
97
+ if (p.type === "message" && p.role === "user") {
98
+ return textFromContentParts(p.content);
99
+ }
100
+ }
101
+ return null;
102
+ }
103
+
104
+ /**
105
+ * Reconstruct thread identity + listing fields from a staged/restored rollout JSONL.
106
+ * Returns null when the file is missing or has no parseable `session_meta`.
107
+ *
108
+ * Accepts plain `.jsonl` or a lone `.jsonl.zst` (legacy Phase-2 quarantine). Compressed
109
+ * rollouts are decompressed in memory with {@link MAX_ROLLOUT_ZST_DECOMPRESSED_BYTES};
110
+ * no decompressed copy is written to disk.
111
+ */
112
+ export function readThreadFieldsFromRollout(path: string): RolloutThreadFields | null {
113
+ if (!path || !existsSync(path)) return null;
114
+ let raw: string;
115
+ try {
116
+ raw = path.endsWith(".zst")
117
+ ? decompressRolloutZstUtf8(path)
118
+ : readFileSync(path, "utf8");
119
+ } catch {
120
+ return null;
121
+ }
122
+ return parseThreadFieldsFromRolloutText(raw);
123
+ }
124
+
125
+ function decompressRolloutZstUtf8(
126
+ path: string,
127
+ maxBytes: number = MAX_ROLLOUT_ZST_DECOMPRESSED_BYTES,
128
+ ): string {
129
+ const compressed = readFileSync(path);
130
+ const decoded = zstdDecompressSync(compressed as Uint8Array<ArrayBuffer>, {
131
+ maxOutputLength: maxBytes,
132
+ });
133
+ if (decoded.byteLength > maxBytes) {
134
+ throw new Error("rollout_zst_too_large");
135
+ }
136
+ return new TextDecoder().decode(decoded);
137
+ }
138
+
139
+ function parseThreadFieldsFromRolloutText(raw: string): RolloutThreadFields | null {
140
+ const lines = raw.split("\n");
141
+ let latest: ParsedSessionMeta | null = null;
142
+ let firstUserMessage = "";
143
+ for (const line of lines) {
144
+ if (!line) continue;
145
+ if (line.includes("\"session_meta\"")) {
146
+ const meta = parseSessionMetaLine(line);
147
+ if (meta) latest = meta;
148
+ }
149
+ if (!firstUserMessage) {
150
+ const preview = extractUserMessagePreview(line);
151
+ if (preview) firstUserMessage = preview;
152
+ }
153
+ }
154
+ if (!latest) return null;
155
+ const payload = latest.record.payload;
156
+ const id = typeof payload.id === "string" ? payload.id : "";
157
+ if (!id) return null;
158
+ const modelProvider = typeof payload.model_provider === "string" && payload.model_provider
159
+ ? payload.model_provider
160
+ : "openai";
161
+ const source = typeof payload.source === "string" && payload.source
162
+ ? payload.source
163
+ : "cli";
164
+ return {
165
+ id,
166
+ modelProvider,
167
+ source,
168
+ firstUserMessage,
169
+ hasUserEvent: firstUserMessage.trim() ? 1 : 0,
170
+ ...(typeof payload.cwd === "string" ? { cwd: payload.cwd } : {}),
171
+ ...(typeof payload.history_mode === "string" ? { historyMode: payload.history_mode } : {}),
172
+ ...(typeof payload.cli_version === "string" ? { cliVersion: payload.cli_version } : {}),
173
+ };
174
+ }