@manyoars/oars 0.2.0 → 0.4.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 (320) hide show
  1. package/README.md +147 -28
  2. package/dist/api.js +1 -1
  3. package/dist/bin.js +6 -29
  4. package/dist/cli.d.ts +10 -4
  5. package/dist/cli.js +440 -91
  6. package/dist/commands/keys.d.ts +31 -0
  7. package/dist/commands/keys.js +65 -0
  8. package/dist/commands/login.d.ts +1 -1
  9. package/dist/commands/login.js +3 -3
  10. package/dist/errors.d.ts +13 -13
  11. package/dist/errors.js +28 -11
  12. package/dist/gate/checks.d.ts +68 -0
  13. package/dist/gate/checks.js +584 -0
  14. package/dist/gate/command.d.ts +14 -0
  15. package/dist/gate/command.js +61 -0
  16. package/dist/gate/hook-fill-main.d.ts +1 -0
  17. package/dist/gate/hook-fill-main.js +16 -0
  18. package/dist/gate/hook-fill.d.ts +38 -0
  19. package/dist/gate/hook-fill.js +94 -0
  20. package/dist/gate/hook-main.d.ts +1 -0
  21. package/dist/gate/hook-main.js +47 -0
  22. package/dist/gate/hook-process.d.ts +24 -0
  23. package/dist/gate/hook-process.js +112 -0
  24. package/dist/gate/hook-record.d.ts +3 -0
  25. package/dist/gate/hook-record.js +8 -0
  26. package/dist/gate/hook.d.ts +48 -0
  27. package/dist/gate/hook.js +259 -0
  28. package/dist/gate/install.d.ts +25 -0
  29. package/dist/gate/install.js +126 -0
  30. package/dist/gate/local-bin.d.ts +8 -0
  31. package/dist/gate/local-bin.js +36 -0
  32. package/dist/gate/notes.d.ts +19 -0
  33. package/dist/gate/notes.js +27 -0
  34. package/dist/gate/novelty.d.ts +35 -0
  35. package/dist/gate/novelty.js +99 -0
  36. package/dist/gate/policy.d.ts +25 -0
  37. package/dist/gate/policy.js +73 -0
  38. package/dist/gate/record.d.ts +120 -0
  39. package/dist/gate/record.js +125 -0
  40. package/dist/gate/registry-state.d.ts +10 -0
  41. package/dist/gate/registry-state.js +30 -0
  42. package/dist/gate/registry.d.ts +56 -0
  43. package/dist/gate/registry.js +94 -0
  44. package/dist/gate/replay.d.ts +20 -0
  45. package/dist/gate/replay.js +254 -0
  46. package/dist/gate/report.d.ts +50 -0
  47. package/dist/gate/report.js +89 -0
  48. package/dist/gate/scope.d.ts +7 -0
  49. package/dist/gate/scope.js +27 -0
  50. package/dist/gate/transcripts.d.ts +68 -0
  51. package/dist/gate/transcripts.js +232 -0
  52. package/dist/hook-bin.d.ts +1 -0
  53. package/dist/hook-bin.js +50 -0
  54. package/dist/issuer.d.ts +10 -6
  55. package/dist/issuer.js +19 -24
  56. package/dist/main.d.ts +1 -0
  57. package/dist/main.js +67 -0
  58. package/dist/oidc.js +1 -1
  59. package/dist/sessions/actions.d.ts +35 -0
  60. package/dist/sessions/actions.js +429 -0
  61. package/dist/sessions/allow.d.ts +23 -0
  62. package/dist/sessions/allow.js +71 -0
  63. package/dist/sessions/archive.d.ts +11 -0
  64. package/dist/sessions/archive.js +38 -0
  65. package/dist/sessions/audit.d.ts +42 -0
  66. package/dist/sessions/audit.js +104 -0
  67. package/dist/sessions/baseline.d.ts +62 -0
  68. package/dist/sessions/baseline.js +190 -0
  69. package/dist/sessions/call-shape.d.ts +19 -0
  70. package/dist/sessions/call-shape.js +85 -0
  71. package/dist/sessions/capture-runtime.d.ts +10 -0
  72. package/dist/sessions/capture-runtime.js +12 -0
  73. package/dist/sessions/cell-increments.d.ts +47 -0
  74. package/dist/sessions/cell-increments.js +243 -0
  75. package/dist/sessions/cells.d.ts +109 -0
  76. package/dist/sessions/cells.js +261 -0
  77. package/dist/sessions/codex.d.ts +53 -0
  78. package/dist/sessions/codex.js +745 -0
  79. package/dist/sessions/command.d.ts +127 -0
  80. package/dist/sessions/command.js +419 -0
  81. package/dist/sessions/copilot.d.ts +40 -0
  82. package/dist/sessions/copilot.js +432 -0
  83. package/dist/sessions/coverage.d.ts +19 -0
  84. package/dist/sessions/coverage.js +56 -0
  85. package/dist/sessions/cursor.d.ts +24 -0
  86. package/dist/sessions/cursor.js +324 -0
  87. package/dist/sessions/derive.d.ts +57 -0
  88. package/dist/sessions/derive.js +386 -0
  89. package/dist/sessions/exclusion.d.ts +87 -0
  90. package/dist/sessions/exclusion.js +361 -0
  91. package/dist/sessions/extract.d.ts +181 -0
  92. package/dist/sessions/extract.js +1806 -0
  93. package/dist/sessions/fold.d.ts +17 -0
  94. package/dist/sessions/fold.js +71 -0
  95. package/dist/sessions/frame.d.ts +24 -0
  96. package/dist/sessions/frame.js +95 -0
  97. package/dist/sessions/gemini.d.ts +37 -0
  98. package/dist/sessions/gemini.js +613 -0
  99. package/dist/sessions/gold-arms.d.ts +67 -0
  100. package/dist/sessions/gold-arms.js +172 -0
  101. package/dist/sessions/gold.d.ts +197 -0
  102. package/dist/sessions/gold.js +463 -0
  103. package/dist/sessions/graph.d.ts +12 -0
  104. package/dist/sessions/graph.js +34 -0
  105. package/dist/sessions/grok.d.ts +36 -0
  106. package/dist/sessions/grok.js +421 -0
  107. package/dist/sessions/hook-calls.d.ts +3 -0
  108. package/dist/sessions/hook-calls.js +47 -0
  109. package/dist/sessions/hook-index.d.ts +66 -0
  110. package/dist/sessions/hook-index.js +164 -0
  111. package/dist/sessions/hooks.d.ts +17 -0
  112. package/dist/sessions/hooks.js +281 -0
  113. package/dist/sessions/identity.d.ts +27 -0
  114. package/dist/sessions/identity.js +131 -0
  115. package/dist/sessions/install.d.ts +70 -0
  116. package/dist/sessions/install.js +246 -0
  117. package/dist/sessions/lineage.d.ts +19 -0
  118. package/dist/sessions/lineage.js +118 -0
  119. package/dist/sessions/mcp.d.ts +7 -0
  120. package/dist/sessions/mcp.js +157 -0
  121. package/dist/sessions/merkle.d.ts +7 -0
  122. package/dist/sessions/merkle.js +28 -0
  123. package/dist/sessions/motif.d.ts +44 -0
  124. package/dist/sessions/motif.js +59 -0
  125. package/dist/sessions/opencode.d.ts +33 -0
  126. package/dist/sessions/opencode.js +319 -0
  127. package/dist/sessions/otel.d.ts +60 -0
  128. package/dist/sessions/otel.js +494 -0
  129. package/dist/sessions/package-books.d.ts +48 -0
  130. package/dist/sessions/package-books.js +120 -0
  131. package/dist/sessions/package-resolve.d.ts +32 -0
  132. package/dist/sessions/package-resolve.js +185 -0
  133. package/dist/sessions/pending.d.ts +34 -0
  134. package/dist/sessions/pending.js +142 -0
  135. package/dist/sessions/pi.d.ts +36 -0
  136. package/dist/sessions/pi.js +353 -0
  137. package/dist/sessions/pool-books.d.ts +21 -0
  138. package/dist/sessions/pool-books.js +75 -0
  139. package/dist/sessions/pool-choices.d.ts +5 -0
  140. package/dist/sessions/pool-choices.js +11 -0
  141. package/dist/sessions/pool-command.d.ts +115 -0
  142. package/dist/sessions/pool-command.js +669 -0
  143. package/dist/sessions/pool-delta.d.ts +54 -0
  144. package/dist/sessions/pool-delta.js +190 -0
  145. package/dist/sessions/pool-nightly-deps.d.ts +3 -0
  146. package/dist/sessions/pool-nightly-deps.js +11 -0
  147. package/dist/sessions/pool-nightly.d.ts +10 -0
  148. package/dist/sessions/pool-nightly.js +85 -0
  149. package/dist/sessions/pool-outbox.d.ts +76 -0
  150. package/dist/sessions/pool-outbox.js +313 -0
  151. package/dist/sessions/pool-print.d.ts +26 -0
  152. package/dist/sessions/pool-print.js +68 -0
  153. package/dist/sessions/pool-raw.d.ts +27 -0
  154. package/dist/sessions/pool-raw.js +68 -0
  155. package/dist/sessions/pool-replica.d.ts +35 -0
  156. package/dist/sessions/pool-replica.js +91 -0
  157. package/dist/sessions/pool-send-lock.d.ts +20 -0
  158. package/dist/sessions/pool-send-lock.js +87 -0
  159. package/dist/sessions/pool-sent.d.ts +4 -0
  160. package/dist/sessions/pool-sent.js +26 -0
  161. package/dist/sessions/pool-totals.d.ts +10 -0
  162. package/dist/sessions/pool-totals.js +21 -0
  163. package/dist/sessions/pool-wire.d.ts +435 -0
  164. package/dist/sessions/pool-wire.js +1100 -0
  165. package/dist/sessions/pool.d.ts +32 -0
  166. package/dist/sessions/pool.js +164 -0
  167. package/dist/sessions/projection.d.ts +75 -0
  168. package/dist/sessions/projection.js +165 -0
  169. package/dist/sessions/public-vocabulary.d.ts +72 -0
  170. package/dist/sessions/public-vocabulary.js +295 -0
  171. package/dist/sessions/query.d.ts +123 -0
  172. package/dist/sessions/query.js +268 -0
  173. package/dist/sessions/read-adoption.d.ts +2 -0
  174. package/dist/sessions/read-adoption.js +27 -0
  175. package/dist/sessions/read-call-shape.d.ts +2 -0
  176. package/dist/sessions/read-call-shape.js +25 -0
  177. package/dist/sessions/read-change.d.ts +2 -0
  178. package/dist/sessions/read-change.js +30 -0
  179. package/dist/sessions/read-cost.d.ts +4 -0
  180. package/dist/sessions/read-cost.js +48 -0
  181. package/dist/sessions/read-day-move.d.ts +3 -0
  182. package/dist/sessions/read-day-move.js +65 -0
  183. package/dist/sessions/read-fit.d.ts +7 -0
  184. package/dist/sessions/read-fit.js +41 -0
  185. package/dist/sessions/read-interaction.d.ts +5 -0
  186. package/dist/sessions/read-interaction.js +62 -0
  187. package/dist/sessions/read-lead-lag.d.ts +15 -0
  188. package/dist/sessions/read-lead-lag.js +81 -0
  189. package/dist/sessions/read-live.d.ts +6 -0
  190. package/dist/sessions/read-live.js +38 -0
  191. package/dist/sessions/read-method.d.ts +2 -0
  192. package/dist/sessions/read-method.js +29 -0
  193. package/dist/sessions/read-next-step.d.ts +2 -0
  194. package/dist/sessions/read-next-step.js +35 -0
  195. package/dist/sessions/read-order.d.ts +54 -0
  196. package/dist/sessions/read-order.js +62 -0
  197. package/dist/sessions/read-readable.d.ts +8 -0
  198. package/dist/sessions/read-readable.js +24 -0
  199. package/dist/sessions/read-recovery-edge.d.ts +2 -0
  200. package/dist/sessions/read-recovery-edge.js +26 -0
  201. package/dist/sessions/read-recovery.d.ts +20 -0
  202. package/dist/sessions/read-recovery.js +97 -0
  203. package/dist/sessions/read-rows.d.ts +13 -0
  204. package/dist/sessions/read-rows.js +47 -0
  205. package/dist/sessions/read-substitution.d.ts +5 -0
  206. package/dist/sessions/read-substitution.js +51 -0
  207. package/dist/sessions/read-support.d.ts +48 -0
  208. package/dist/sessions/read-support.js +73 -0
  209. package/dist/sessions/read-tool-day.d.ts +2 -0
  210. package/dist/sessions/read-tool-day.js +26 -0
  211. package/dist/sessions/read-version-boundary.d.ts +31 -0
  212. package/dist/sessions/read-version-boundary.js +95 -0
  213. package/dist/sessions/read.d.ts +86 -0
  214. package/dist/sessions/read.js +87 -0
  215. package/dist/sessions/recall-bundle.d.ts +12 -0
  216. package/dist/sessions/recall-bundle.js +33 -0
  217. package/dist/sessions/recall-consequence.d.ts +45 -0
  218. package/dist/sessions/recall-consequence.js +179 -0
  219. package/dist/sessions/recall-frames.d.ts +18 -0
  220. package/dist/sessions/recall-frames.js +44 -0
  221. package/dist/sessions/recall-next.d.ts +145 -0
  222. package/dist/sessions/recall-next.js +241 -0
  223. package/dist/sessions/recall-object.d.ts +139 -0
  224. package/dist/sessions/recall-object.js +78 -0
  225. package/dist/sessions/recall-pool.d.ts +19 -0
  226. package/dist/sessions/recall-pool.js +96 -0
  227. package/dist/sessions/recall-push.d.ts +26 -0
  228. package/dist/sessions/recall-push.js +47 -0
  229. package/dist/sessions/recall-reads.d.ts +124 -0
  230. package/dist/sessions/recall-reads.js +417 -0
  231. package/dist/sessions/recall-render.d.ts +19 -0
  232. package/dist/sessions/recall-render.js +168 -0
  233. package/dist/sessions/recovery-calls.d.ts +21 -0
  234. package/dist/sessions/recovery-calls.js +25 -0
  235. package/dist/sessions/redact.d.ts +17 -0
  236. package/dist/sessions/redact.js +113 -0
  237. package/dist/sessions/repo-root.d.ts +10 -0
  238. package/dist/sessions/repo-root.js +101 -0
  239. package/dist/sessions/schema.d.ts +694 -0
  240. package/dist/sessions/schema.js +167 -0
  241. package/dist/sessions/session-shape.d.ts +76 -0
  242. package/dist/sessions/session-shape.js +355 -0
  243. package/dist/sessions/sources.d.ts +50 -0
  244. package/dist/sessions/sources.js +233 -0
  245. package/dist/sessions/steps.d.ts +105 -0
  246. package/dist/sessions/steps.js +250 -0
  247. package/dist/sessions/store.d.ts +309 -0
  248. package/dist/sessions/store.js +1498 -0
  249. package/dist/sessions/strata.d.ts +26 -0
  250. package/dist/sessions/strata.js +77 -0
  251. package/dist/sessions/survival.d.ts +27 -0
  252. package/dist/sessions/survival.js +230 -0
  253. package/dist/sessions/transcript-tail.d.ts +14 -0
  254. package/dist/sessions/transcript-tail.js +64 -0
  255. package/dist/sessions/verify.d.ts +53 -0
  256. package/dist/sessions/verify.js +286 -0
  257. package/dist/sessions/vocab-load.d.ts +26 -0
  258. package/dist/sessions/vocab-load.js +61 -0
  259. package/dist/sessions/vscode-copilot.d.ts +38 -0
  260. package/dist/sessions/vscode-copilot.js +494 -0
  261. package/dist/telemetry/command.d.ts +8 -0
  262. package/dist/telemetry/command.js +85 -0
  263. package/dist/telemetry/consent.d.ts +17 -0
  264. package/dist/telemetry/consent.js +63 -0
  265. package/dist/telemetry/crash.d.ts +8 -0
  266. package/dist/telemetry/crash.js +52 -0
  267. package/dist/telemetry/emit.d.ts +31 -0
  268. package/dist/telemetry/emit.js +42 -0
  269. package/dist/telemetry/fields.d.ts +16 -0
  270. package/dist/telemetry/fields.js +26 -0
  271. package/dist/telemetry/health.d.ts +21 -0
  272. package/dist/telemetry/health.js +108 -0
  273. package/dist/telemetry/hook-emit.d.ts +4 -0
  274. package/dist/telemetry/hook-emit.js +18 -0
  275. package/dist/telemetry/measures.d.ts +32 -0
  276. package/dist/telemetry/measures.js +128 -0
  277. package/dist/telemetry/observe.d.ts +13 -0
  278. package/dist/telemetry/observe.js +51 -0
  279. package/dist/telemetry/read-events.d.ts +9 -0
  280. package/dist/telemetry/read-events.js +17 -0
  281. package/dist/telemetry/record.d.ts +17 -0
  282. package/dist/telemetry/record.js +26 -0
  283. package/dist/telemetry/report.d.ts +77 -0
  284. package/dist/telemetry/report.js +188 -0
  285. package/dist/telemetry/schema.d.ts +40 -0
  286. package/dist/telemetry/schema.js +187 -0
  287. package/dist/telemetry/send-main.d.ts +1 -0
  288. package/dist/telemetry/send-main.js +11 -0
  289. package/dist/telemetry/send.d.ts +36 -0
  290. package/dist/telemetry/send.js +172 -0
  291. package/dist/telemetry/spool.d.ts +17 -0
  292. package/dist/telemetry/spool.js +70 -0
  293. package/dist/telemetry/wire.d.ts +25 -0
  294. package/dist/telemetry/wire.js +76 -0
  295. package/package.json +4 -5
  296. package/vocab/crates.json +1 -0
  297. package/vocab/go.json +1 -0
  298. package/vocab/harness.json +1 -0
  299. package/vocab/homebrew.json +1 -0
  300. package/vocab/manifest.json +1 -0
  301. package/vocab/maven.json +1 -0
  302. package/vocab/models.json +1 -0
  303. package/vocab/npm.json +1 -0
  304. package/vocab/programs.json +1 -0
  305. package/vocab/pypi.json +1 -0
  306. package/vocab/rubygems.json +1 -0
  307. package/dist/commands/deploy-input.d.ts +0 -38
  308. package/dist/commands/deploy-input.js +0 -150
  309. package/dist/commands/deploy.d.ts +0 -22
  310. package/dist/commands/deploy.js +0 -124
  311. package/dist/commands/status.d.ts +0 -7
  312. package/dist/commands/status.js +0 -18
  313. package/dist/commands/templates.d.ts +0 -11
  314. package/dist/commands/templates.js +0 -32
  315. package/dist/commands/test-charge.d.ts +0 -11
  316. package/dist/commands/test-charge.js +0 -34
  317. package/dist/journal.d.ts +0 -26
  318. package/dist/journal.js +0 -58
  319. package/parity/deploy-plans/paid-api.json +0 -34
  320. package/parity/deploy-plans/per-task-agent.json +0 -34
@@ -0,0 +1,745 @@
1
+ // The OpenAI Codex CLI source: a Codex session log becomes the same capture records a Claude Code transcript does.
2
+ //
3
+ // FIXTURE-DERIVED: written from the open-source Codex CLI (github.com/openai/codex, Apache-2.0, commit ac558e2a, 2026-11-03), not from a live
4
+ // store. Each field read below cites the file it comes from; `docs/sessions-store.md` says what a live run would still have to prove.
5
+ //
6
+ // Where it reads from (`codexRoot`): `$CODEX_HOME`, else `~/.codex` (`%USERPROFILE%\.codex` on Windows); the same on every OS
7
+ // (codex-rs/utils/home-dir/src/lib.rs:5-18). Inside it, `sessions/YYYY/MM/DD/rollout-YYYY-MM-DDThh-mm-ss-<uuid>.jsonl` and `archived_sessions/` hold one
8
+ // file per session (a reverted thread's session spans several); Codex compresses an old one to `.jsonl.zst` and reads either (rollout/src/compression.rs), so this reader does too.
9
+ // A line is `{timestamp, type, payload}` (history/src/lib.rs `RolloutLine`, rollout_payload.rs). Like cursor.ts, the reader writes each session
10
+ // out as the transcript lines `captureSession` already reads, so redaction, the 1,000-character line cap, the salted file hashes, and the
11
+ // skeleton are the Claude Code path's own code:
12
+ // session_meta {id, cwd, cli_version, timestamp, history_mode, parent_thread_id, forked_from_id, subagent_history_start_ordinal}: the
13
+ // working directory and the harness version; only the first one in the file (recorder.rs:1157-1158). A spawned agent has `parent_thread_id` (protocol.rs:3464); records whose
14
+ // `ordinal` is below its `subagent_history_start_ordinal` (:3511-3516) are the parent's prefix (ordinal.rs:86-89), and are dropped. A Referenced fork copies none: its `history_base` points at the parent (:3508-3510; core/src/session/session.rs:1139-1140). A Copied fork, in either mode, does copy the parent's history and writes no `history_base`
15
+ // (`fork_thread_from_history` is `ForkPersistence::Copied`, core/src/thread_manager.rs:1680; session.rs:1139; app-server takes it when no prepared fork exists, app-server/src/request_processors/thread_processor.rs:5724-5728). Ordinals exist
16
+ // only in a Paginated rollout (ordinal.rs:37), the CLI's default (thread-store/src/local/mod.rs:512-515), which holds item_completed TurnItems
17
+ // (UserMessage, AgentMessage, FileChange, McpToolCall) in place of the legacy events below (rollout/src/policy.rs:142-156, `persisted_item_completed_event` :232, its Paginated arm :253); both are read.
18
+ // A Copied fork's history, in either mode, is dropped by matching the parent's lineage as of the fork's start, record for record
19
+ // turn_context {model, cwd, approval_policy}: the model, the directory when it changes, and the approval policy, which rides later lines as `permissionMode`
20
+ // session_meta also holds `dynamic_tools`, the tools the session declared (a `DynamicToolSpec`, dynamic_tools.rs), read as the offered list
21
+ // event_msg turn_aborted {reason} `interrupted` is the operator cutting a turn short (TurnAbortReason, protocol.rs:4580-4585); the others are the harness's own
22
+ // response_item additional_tools {tools} / tool_search_output {tools} tools offered later, and tools a search loaded (models.rs:1076, :1257)
23
+ // That these records are persisted into the rollout is rollout/src/policy.rs:83, :91, and :137 at ac558e2.
24
+ // event_msg user_message {message} a prompt (the `response_item` user messages also hold injected context, so they are not read)
25
+ // event_msg agent_message {message} the assistant's text
26
+ // event_msg token_count {info.last_token_usage} one model response's usage, closing the lines since the last one
27
+ // response_item function_call / local_shell_call / custom_tool_call a tool call
28
+ // response_item function_call_output {call_id, output} a shell call's outcome: the text `Exit code: N` / `Process exited with code N` then `Output:`
29
+ // (the unified-exec emitter's own text, core/src/tools/events.rs:400, :416, :423; context.rs:556), the real exit code; `execution error: ...` (core/src/tools/events.rs:438) is a
30
+ // call that failed to spawn. `write_stdin` (handlers/shell_spec.rs:113) is a shell call too: the process it polls ends only where the
31
+ // poll reports its exit code; a poll that failed (`write_stdin failed: ...`, write_stdin.rs:123) is its own error and settles nothing else
32
+ // event_msg item_completed CommandExecution {id, exit_code, status, aggregated_output} Paginated mode only (policy.rs:253 arm, kept by `_ =>` :284; Legacy
33
+ // keeps none, :238-251). `id` is the exec_command's call id (events.rs:571), written when the process exits (events.rs:561-590):
34
+ // by the exit watcher for a process the model never polled (unified_exec/async_watcher.rs:158, :224), or by the call itself when it exits in
35
+ // its first yield (process_manager.rs:827). It settles a known unresolved shell call from the real `exit_code` and the output; a
36
+ // `declined` one (exit code -1, events.rs:552-554) settles as a refusal only where its output is the user's refusal text (`exec command rejected by user`), and nothing otherwise. A `failed` one with exit code -1 is a failure with no exit status (a tool
37
+ // failure message, events.rs:538-542; a process with none, async_watcher.rs:213, :409), so its text is the error line. So a backgrounded process keeps its failure and its commit sha
38
+ // even with no `write_stdin` poll; only a Legacy rollout leaves it unsettled until a poll, and there the poll settles it
39
+ // response_item custom_tool_call `exec` / function_call `wait` Codex 0.166+ code mode (the live 0.166/0.167 store, 2026-11-06; upstream rust-v0.167.0 not yet read): `exec`'s input is a JavaScript cell, not a command; its
40
+ // output opens `Script completed`, `Script failed` (+ a `Script error:` part), or `Script running with cell ID <id>`, which is the cell's outcome and never its
41
+ // commands'. `wait {cell_id}` polls a cell with the same three headers and settles it, in Paginated mode too
42
+ // event_msg item_completed CommandExecution, id `exec-<uuid>` (no call owns it, 0.166+) the command a cell ran, as its own Bash call: exit_code, aggregated_output, duration
43
+ // event_msg mcp_tool_call_end {call_id, result} an MCP call's outcome: `result.Err`, or `result.Ok.isError` (protocol.rs:3047, :3076); Legacy mode only
44
+ // event_msg patch_apply_end {call_id, success, stderr, status} an apply_patch call's outcome (Legacy mode only); a declined one settles as a refusal only where its stderr is the user's refusal text (`patch rejected by user`), and nothing otherwise, and a
45
+ // patch with no end event stays unsettled (a rejected patch is only a `RespondToModel` output, tools/handlers/apply_patch.rs)
46
+ // event_msg exec_command_end never persisted at ac558e2 (policy.rs:165) nor at rust-v0.46.0, so it is not read: the CommandExecution item and the call's output are the two sources
47
+ // Older shapes: the JSON `{output, metadata.exit_code}` wrapper of a shell output (rust-v0.46.0 core/src/tools/mod.rs:186-210).
48
+ // What Codex has no equivalent of (cost) stays at the value capture gives an absent field; message ids and a call reply's wall time are persisted and not read (a code-mode command item's `duration` is).
49
+ import { createReadStream } from 'node:fs';
50
+ import { readdir, readFile, stat } from 'node:fs/promises';
51
+ import { homedir } from 'node:os';
52
+ import { basename, join } from 'node:path';
53
+ import { createInterface } from 'node:readline';
54
+ import { PassThrough, pipeline } from 'node:stream';
55
+ import { createZstdDecompress, zstdDecompressSync } from 'node:zlib';
56
+ import { INTERRUPT_MARKER, SHAPE_TOOLS_LINE } from './session-shape.js';
57
+ /** The Codex home for an environment; a test passes its own so it never reads the real one. */
58
+ export const codexRoot = (env, home = homedir()) => {
59
+ const set = env.CODEX_HOME;
60
+ return set === undefined || set === '' ? join(home, '.codex') : set;
61
+ };
62
+ const isObj = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
63
+ const str = (value) => (typeof value === 'string' && value !== '' ? value : undefined);
64
+ const num = (value) => (typeof value === 'number' && Number.isFinite(value) ? value : undefined);
65
+ const parse = (text) => {
66
+ if (typeof text !== 'string')
67
+ return undefined;
68
+ try {
69
+ return JSON.parse(text);
70
+ }
71
+ catch {
72
+ return undefined;
73
+ }
74
+ };
75
+ /**
76
+ * A rollout file name is `rollout-<timestamp>-<thread>.jsonl`, optionally `.zst`; a reverted thread's is `rollout-<timestamp>-<thread>_<rollout>.jsonl`
77
+ * (rollout/src/rollout_file_name.rs:12-14, parsed at :41-47, rendered at :66-73).
78
+ */
79
+ const ROLLOUT = /^rollout-.+\.jsonl(\.zst)?$/;
80
+ const UUID = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}';
81
+ const ROLLOUT_NAME = new RegExp(String.raw `^rollout-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-(${UUID})(?:_(${UUID}))?\.jsonl(?:\.zst)?$`, 'i');
82
+ const SAFE_ID = /^(?!\.+$)[A-Za-z0-9._-]+$/;
83
+ /** The key a rollout is deposited under, and the thread it belongs to when it is a reverted copy of one (folded into that thread's lineage when the thread is in the store). */
84
+ const idOf = (name) => {
85
+ const match = ROLLOUT_NAME.exec(name);
86
+ const thread = match?.[1]?.toLowerCase();
87
+ const reverted = match?.[2]?.toLowerCase();
88
+ if (thread === undefined)
89
+ return { id: name.replace(/^rollout-/, '').replace(/\.jsonl(\.zst)?$/, ''), thread: null };
90
+ return reverted === undefined || reverted === thread ? { id: thread, thread: null } : { id: `${thread}_${reverted}`, thread };
91
+ };
92
+ const textOf = async (file) => {
93
+ const bytes = await readFile(file);
94
+ return (file.endsWith('.zst') ? zstdDecompressSync(bytes) : bytes).toString('utf8');
95
+ };
96
+ /** The first line of a rollout, read without loading the rest: a compressed file is decompressed as a stream and the read stops at the line, so the cost does not grow with the file. */
97
+ const firstLine = async (file) => {
98
+ const source = createReadStream(file);
99
+ const stream = new PassThrough({ encoding: 'utf8' });
100
+ // pipeline hands a failure in either stage (a missing file, a corrupt frame) to `stream`, and the early destroy below ends it with a premature close nobody awaits.
101
+ if (file.endsWith('.zst'))
102
+ pipeline(source, createZstdDecompress(), stream, () => undefined);
103
+ else
104
+ pipeline(source, stream, () => undefined);
105
+ const failed = new Promise((_, reject) => stream.once('error', reject));
106
+ failed.catch(() => undefined);
107
+ const lines = createInterface({ input: stream, crlfDelay: Infinity });
108
+ const first = async () => {
109
+ for await (const line of lines)
110
+ return line;
111
+ return '';
112
+ };
113
+ try {
114
+ return await Promise.race([first(), failed]);
115
+ }
116
+ finally {
117
+ lines.close();
118
+ stream.destroy();
119
+ }
120
+ };
121
+ /** An error's message with `file` cut to its base name, so a reason that is logged names no folder. */
122
+ const reasonOf = (error, file) => (error instanceof Error ? error.message : String(error)).split(file).join(basename(file));
123
+ /** Lists rollout files under `dir`; only a missing folder is silent, any other failure (EACCES, ENOTDIR) is reported against `dir`. */
124
+ const walk = async (dir, found, unreadable) => {
125
+ let entries;
126
+ try {
127
+ entries = await readdir(dir, { withFileTypes: true });
128
+ }
129
+ catch (error) {
130
+ if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT'))
131
+ unreadable.push({ file: dir, reason: reasonOf(error, dir) });
132
+ return;
133
+ }
134
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
135
+ if (entry.isDirectory())
136
+ await walk(join(dir, entry.name), found, unreadable);
137
+ else if (entry.isFile() && ROLLOUT.test(entry.name))
138
+ found.push(join(dir, entry.name));
139
+ }
140
+ };
141
+ const iso = (value) => {
142
+ const text = str(value);
143
+ // An absolute time only: a missing or relative one (not a date after 2000) says nothing about when.
144
+ return text !== undefined && Date.parse(text) >= Date.parse('2000-01-01T00:00:00Z') ? text : undefined;
145
+ };
146
+ /** The tool vocabulary prompt classes and `files` are computed in; a Codex name with no counterpart keeps its own. */
147
+ const SHELL_CALLS = new Set(['shell', 'shell_command', 'exec_command', 'local_shell', 'container.exec', 'write_stdin', 'exec', 'wait']);
148
+ const PATCH_CALLS = new Set(['apply_patch']);
149
+ /** The namespace upstream fills in for a top-level function (codex-rs/protocol/src/tool_name.rs:6-7); with none and an empty one, it is the plain tool (:46-50). */
150
+ const DEFAULT_FUNCTION_NAMESPACE = 'functions';
151
+ /** One word of an argv as a POSIX shell reads it back: left alone when it holds only safe characters, else in single quotes, so `bash -lc 'git revert HEAD'` keeps its script as one word. */
152
+ const quotedWord = (word) => (/^[\w@%+=:,./-]+$/.test(word) ? word : `'${word.replaceAll("'", "'\\''")}'`);
153
+ const commandOf = (args) => {
154
+ const command = args.command ?? args.cmd;
155
+ if (Array.isArray(command))
156
+ return command.filter((part) => typeof part === 'string').map(quotedWord).join(' ');
157
+ return str(command);
158
+ };
159
+ /** The paths an `apply_patch` input touches (`*** Add|Update|Delete File: <path>`, the grammar of codex-rs/apply-patch). */
160
+ const patchFiles = (input) => [...input.matchAll(/^\*\*\* (?:Add|Update|Delete) File: (.+)$/gm)].map((match) => (match[1] ?? '').trim());
161
+ const EXIT_LINE = /^(?:Exit code: |Process exited with code )(-?\d+)\s*$/m;
162
+ const OUTPUT_MARK = /(?:^|\n)Output:\n?/;
163
+ const RUNNING_LINE = /^Process running with session ID (\S+)\s*$/m;
164
+ /**
165
+ * A code-mode cell's outcome is its output's first line (the live 0.166/0.167 store, 2026-11-06; upstream rust-v0.167.0 not yet read): `Script completed`, `Script failed` (a second part
166
+ * `Script error:\n<message>` follows), or `Script running with cell ID <id>`. It is the cell's outcome, never its commands': a completed cell can hold
167
+ * commands that exited nonzero, and those are CommandExecution items of their own.
168
+ */
169
+ const SCRIPT_HEAD = /^Script (completed|failed|running with cell ID (\S+))[ \t]*(?:\n|$)/;
170
+ const SCRIPT_ERROR = /(?:^|\n)Script error:\n?/;
171
+ /**
172
+ * The message of a call that failed before it produced an exit code, written as the whole output (no `Output:` header): `execution error: ` for a
173
+ * shell call (core/src/tools/events.rs:438), and for the default unified-exec tool (ConfigShellToolType is only `UnifiedExec | Disabled`,
174
+ * protocol/src/openai_models.rs:330-333) `exec_command failed: ` (core/src/tools/handlers/unified_exec/exec_command.rs:270), `write_stdin failed: `
175
+ * (write_stdin.rs:123), and `write_stdin approval failed: ` (:121). `wait failed: ` is assumed by analogy with `write_stdin failed: ` (write_stdin.rs:123) and has not
176
+ * been seen in the live store. `write_stdin rejected: ` (:118) stays unsettled: its reason is the approval policy's.
177
+ */
178
+ const SPAWN_ERROR = /^(?:execution error|exec_command failed|wait failed|write_stdin(?: approval)? failed): /;
179
+ /**
180
+ * A declined call is the user's refusal only where its message says so. Codex reports `ToolError::Rejected` as `declined` for a user's refusal and for some
181
+ * runtime rejections, and normalizes only the literal text `rejected by user` (events.rs:448-458): `exec command rejected by user` for a shell call (:456) and
182
+ * `patch rejected by user` for a patch (:457). So a `declined` item settles as the user's refusal when the message it carries is that text (`aggregated_output`
183
+ * for a CommandExecution item, events.rs:549-551; `stderr` for a FileChange item and `patch_apply_end`, events.rs:341, :616), and any other `declined` message
184
+ * stays unsettled. A settled one is these two sentences, which `errorCodeOf` classes `user-rejected`. `write_stdin rejected: <reason>` (:118) is not read this
185
+ * way either: its reason is the approval policy's, which the user may not have given.
186
+ */
187
+ const DECLINED_SHELL = 'exec command rejected by user';
188
+ const DECLINED_OUTPUT = /^exec command rejected by user\s*$/;
189
+ const DECLINED_PATCH = 'patch rejected by user';
190
+ const DECLINED_PATCH_TEXT = /^(?:patch )?rejected by user\s*$/;
191
+ /** The text of a `function_call_output` body: a bare string, or an array of `{type: input_text, text}` items (protocol/src/models.rs:2376). */
192
+ const bodyText = (raw) => {
193
+ if (!Array.isArray(raw))
194
+ return str(raw) ?? '';
195
+ return raw.map((item) => (isObj(item) ? (str(item.text) ?? '') : '')).filter((text) => text !== '').join('\n');
196
+ };
197
+ /**
198
+ * What a shell output says: the real exit code and the text after the `Output:` line, which is what an error line is cut from. The two header
199
+ * forms are the unified-exec emitter's own `Exit code: N` (core/src/tools/events.rs:400, :416, :423; `shell` and `shell_command` are older names for it) and
200
+ * core/src/tools/context.rs:556 (`Process exited with code N`); no code and no `Process running with session ID N` line means the call failed before it
201
+ * ran (`SPAWN_ERROR` above), was declined (`... rejected by user`, events.rs:456), or is a process with no exit status, whose reply header gets neither line
202
+ * (`None` writes none, context.rs:555-557; its -1 goes to the item only, process_manager.rs:823, :839). An older Codex wrote JSON `{output, metadata.exit_code}` (rust-v0.46.0 core/src/tools/mod.rs:186-210).
203
+ */
204
+ const outcomeOf = (raw) => {
205
+ const body = bodyText(raw);
206
+ const printed = OUTPUT_MARK.exec(body);
207
+ const script = SCRIPT_HEAD.exec(printed === null ? body : body.slice(0, printed.index));
208
+ if (script !== null) {
209
+ const error = SCRIPT_ERROR.exec(body);
210
+ const output = printed === null ? '' : body.slice(printed.index + printed[0].length, error === null || error.index < printed.index ? undefined : error.index);
211
+ if (script[1] === 'failed') {
212
+ const message = error === null ? output : body.slice(error.index + error[0].length);
213
+ return { text: message.trim() === '' ? 'Script failed' : message, exit: undefined, spawnError: true, running: undefined, script: true };
214
+ }
215
+ return { text: output, exit: script[2] === undefined ? 0 : undefined, spawnError: false, running: script[2], script: true };
216
+ }
217
+ const wrapped = parse(body);
218
+ if (isObj(wrapped) && typeof wrapped.output === 'string')
219
+ return { text: wrapped.output, exit: isObj(wrapped.metadata) ? num(wrapped.metadata.exit_code) : undefined, spawnError: false, running: undefined, script: false };
220
+ const mark = OUTPUT_MARK.exec(body);
221
+ if (mark === null)
222
+ return { text: body, exit: undefined, spawnError: SPAWN_ERROR.test(body), running: undefined, script: false };
223
+ const head = body.slice(0, mark.index);
224
+ const code = EXIT_LINE.exec(head)?.[1];
225
+ return { text: body.slice(mark.index + mark[0].length), exit: code === undefined ? undefined : Number(code), spawnError: false, running: RUNNING_LINE.exec(head)?.[1], script: false };
226
+ };
227
+ /**
228
+ * A failed call's error text. A negative code is no exit status: upstream writes -1 for a tool failure message (core/src/tools/events.rs:538-542) and for a
229
+ * process with none (unified_exec/async_watcher.rs:213, :409; process_manager.rs:823), and capture's `Exit code (\d+)` (extract.ts, `EXIT_RE`) cannot read it, so
230
+ * an `Exit code -1` line would be the error line and the message lost. The message stands alone then, and a code is the line capture skips.
231
+ */
232
+ const failureOf = (exit, text) => exit === undefined || exit === 0 ? undefined : exit < 0 && text.trim() !== '' ? text : `Exit code ${String(exit)}\n${text}`;
233
+ const usageOf = (info) => {
234
+ const last = isObj(info) && isObj(info.last_token_usage) ? info.last_token_usage : undefined;
235
+ if (last === undefined)
236
+ return undefined;
237
+ const input = num(last.input_tokens) ?? 0;
238
+ const cached = Math.min(num(last.cached_input_tokens) ?? 0, input);
239
+ const output = num(last.output_tokens) ?? 0;
240
+ // A zero total means Codex recorded none, so no usage is written rather than a measured zero.
241
+ if (input === 0 && output === 0)
242
+ return undefined;
243
+ const reasoning = num(last.reasoning_output_tokens);
244
+ const written = num(last.cache_write_input_tokens) ?? 0;
245
+ // Codex counts cached and written tokens both inside `input_tokens` (input 100 = cached 40 + write 60, codex-api/src/sse/responses.rs:889-911); capture's
246
+ // `input` excludes cache reads and creation, as the Claude Code path's does, so each token is counted once.
247
+ return {
248
+ input_tokens: Math.max(input - cached - written, 0),
249
+ cache_read_input_tokens: cached,
250
+ cache_creation_input_tokens: written,
251
+ output_tokens: output,
252
+ ...(reasoning === undefined ? {} : { output_tokens_details: { thinking_tokens: reasoning } }),
253
+ };
254
+ };
255
+ /** The non-blank lines of a rollout that are not JSON (a torn write), kept so `captureSession` counts them and the deposit line reports them. */
256
+ export const rolloutBadLines = (text) => text.split('\n').filter((raw) => raw.trim() !== '' && parse(raw) === undefined);
257
+ /**
258
+ * The names in a list of tool declarations: each entry's `name` (a `DynamicToolSpec` function, dynamic_tools.rs; an `additional_tools` or
259
+ * `tool_search_output` entry is an untyped value, models.rs:1076, :1257), and the names of a namespace's own `tools`; a namespace's own name is a grouping, not a tool. Nothing else is read: a
260
+ * declaration's description and schema stay where they are.
261
+ */
262
+ const toolNames = (list) => (Array.isArray(list) ? list : [])
263
+ .flatMap((entry) => (isObj(entry) ? [Array.isArray(entry.tools) ? undefined : str(entry.name), ...toolNames(entry.tools)] : []))
264
+ .filter((name) => name !== undefined && name !== '');
265
+ /** The approval policy as the permission mode: a string, or the tag of an externally tagged object (`{granular: {...}}`, protocol.rs:1181), which `mode()` keeps if documented and stores as `other` if not. */
266
+ const approvalMode = (policy) => (isObj(policy) ? Object.keys(policy)[0] : str(policy));
267
+ /** One rollout as the lines `captureSession` reads. */
268
+ export const rolloutLines = (text, fallbackId, parentText) => {
269
+ const nonblank = text.split('\n').filter((raw) => raw.trim() !== '');
270
+ if (nonblank.length > 0 && !nonblank.some((raw) => isObj(parse(raw))))
271
+ throw new Error('no parseable line');
272
+ const lines = [];
273
+ const base = { sessionId: fallbackId };
274
+ let at;
275
+ let model;
276
+ let sequence = 0;
277
+ let response = 0;
278
+ // The assistant lines since the last counted token_count share one response id; the first of them carries its usage.
279
+ let open;
280
+ let lastTotal;
281
+ const calls = new Map();
282
+ // The tools the session declared as dynamic (`dynamic_tools`): their output carries no failure signal, only their DynamicToolCall item does.
283
+ const dynamicNames = new Set();
284
+ // A unified-exec process still running, by its session id, to the exec_command that started it: in a Legacy rollout the `write_stdin` that sees it exit settles both.
285
+ const running = new Map();
286
+ // Records below this ordinal are the parent prefix a subagent's rollout copied (ordinal.rs:86-89), not the session's own. Only a Paginated rollout
287
+ // holds ordinals (rollout/src/ordinal.rs:29-30, :37).
288
+ let inherited;
289
+ // Only the first session_meta is the rollout's own; later ones are copied from fork history (rollout/src/recorder.rs:1157-1158, :1173).
290
+ let seenMeta = false;
291
+ // A Paginated rollout holds the exit watcher's item, which carries the whole output, so there a poll that sees the exit settles only itself.
292
+ let paginated = false;
293
+ // A Copied fork's file holds the parent's records first (no ordinal marks them: a Paginated one numbers them afresh from 0, ordinal.rs:28-33, and a Legacy
294
+ // one has none), so the leading records equal to the parent's, in order, are its copied history, in either mode. Null once it stops matching.
295
+ let copied = null;
296
+ const resolved = new Set();
297
+ // The stamp before the record being read, so an item's start (its stamp less its duration) is never earlier than the line before it.
298
+ let before;
299
+ const stamp = (value) => {
300
+ before = at;
301
+ const candidate = iso(value);
302
+ if (candidate !== undefined && (at === undefined || Date.parse(candidate) >= Date.parse(at)))
303
+ at = candidate;
304
+ return at ?? new Date(0).toISOString();
305
+ };
306
+ const assistant = (content, when) => {
307
+ sequence += 1;
308
+ const message = { role: 'assistant', content };
309
+ // The id is set here, not when a token_count closes the response: a line no token_count follows (an aborted turn, a local model) still has one.
310
+ const requestId = `${String(base.sessionId)}-resp${String(response + 1)}`;
311
+ const line = { ...base, type: 'assistant', uuid: `${fallbackId}-a${String(sequence)}`, requestId, timestamp: when, ...(model === undefined ? {} : { requestedModel: model }), message };
312
+ lines.push(line);
313
+ open ??= line;
314
+ };
315
+ // A failure is the error text; a success carries the call's own output when it has one, which capture reads transiently, as on the Claude Code
316
+ // path: a shell call's `[branch sha]` line is how a commit's sha is found. The output follows an `Exit code 0` line, the shape a Claude Code Bash
317
+ // result has (fixtures/proj/errors-a.jsonl:15): capture marks a result whose text starts `Exit code <nonzero>` as failed (`isErrorResult` in extract.ts), and a
318
+ // call that exited 0 may print such a line (`cat ci.log`). The commit patterns read every line (`COMMIT_LINE` and `HASH_LINE` in extract.ts, both `gm`), so the sha is still found.
319
+ const result = (id, failure, when, output = '', timed = false) => {
320
+ sequence += 1;
321
+ lines.push({
322
+ ...base,
323
+ type: 'user',
324
+ uuid: `${fallbackId}-r${String(sequence)}`,
325
+ timestamp: when,
326
+ // Codex stamps a result with its own time, but the call's start is not paired to it, so no duration is claimed; a CommandExecution item's own `duration` is the exception.
327
+ ...(timed ? {} : { noDuration: true }),
328
+ message: { role: 'user', content: [{ type: 'tool_result', tool_use_id: id, is_error: failure !== undefined, content: failure ?? (output === '' ? '' : `Exit code 0\n${output}`) }] },
329
+ });
330
+ };
331
+ // A neutral line for the setup, funnel, and trust shape (session-shape.ts): none of it is a Claude-shaped line, and none carries a message.
332
+ const tools = (fields) => {
333
+ sequence += 1;
334
+ lines.push({ ...base, type: SHAPE_TOOLS_LINE, uuid: `${fallbackId}-t${String(sequence)}`, ...fields });
335
+ };
336
+ const prompted = (message, when) => {
337
+ sequence += 1;
338
+ lines.push({ ...base, type: 'user', uuid: `${fallbackId}-u${String(sequence)}`, timestamp: when, message: { role: 'user', content: [{ type: 'text', text: message }] } });
339
+ };
340
+ const settlePatch = (id, failure, when) => {
341
+ const known = calls.get(id);
342
+ if (known?.kind !== 'patch' || resolved.has(id))
343
+ return;
344
+ resolved.add(id);
345
+ for (const part of known.ids)
346
+ result(part, failure, when);
347
+ };
348
+ const settleMcp = (id, failure, when) => {
349
+ if (calls.get(id)?.kind !== 'other' || resolved.has(id))
350
+ return;
351
+ resolved.add(id);
352
+ result(id, failure, when);
353
+ };
354
+ // A shell call settles on its exit code, or on a failure to spawn, whether that comes from its own output or from a CommandExecution item. A
355
+ // process still running was remembered by its session id, so in a Legacy rollout the `write_stdin` that polled it and read its exit
356
+ // code settles both calls (in a Paginated one an item settles only its own exec_command; a `wait` on a code-mode cell settles the cell in either mode, because
357
+ // the cell's outcome is its own header and no item carries it). A poll that failed instead (`write_stdin failed: ...`, `exited` false) says nothing about the process: a
358
+ // non-empty write to one with no tty is `StdinClosed` (unified_exec/process_manager.rs:1021-1026) and it runs on, so only the poll is the error.
359
+ const settleShell = (id, failure, output, when, exited = true) => {
360
+ const known = calls.get(id);
361
+ if (known?.kind !== 'shell' || resolved.has(id))
362
+ return;
363
+ resolved.add(id);
364
+ result(id, failure, when, output);
365
+ const pending = known.session === undefined ? undefined : running.get(known.session);
366
+ if (exited && (!paginated || known.cell === true) && pending !== undefined && !resolved.has(pending)) {
367
+ resolved.add(pending);
368
+ result(pending, failure, when, output);
369
+ }
370
+ };
371
+ // The text of a TurnItem's content array. Each variant tags its own parts: a UserMessage's `UserInput` is snake_case, `text` (user_input.rs:16), but an
372
+ // AgentMessage's `AgentMessageContent` has no rename_all, so it is `Text` (items.rs:131-135).
373
+ const contentText = (content, tag) => (Array.isArray(content) ? content.map((part) => (isObj(part) && part.type === tag ? (str(part.text) ?? '') : '')).filter((part) => part !== '').join('\n') : '');
374
+ // A completed TurnItem, the Paginated mode's replacement for the legacy events (policy.rs:142-156).
375
+ const completed = (item, when) => {
376
+ const kind = str(item.type);
377
+ const id = str(item.id);
378
+ if (kind === 'UserMessage') {
379
+ const message = contentText(item.content, 'text');
380
+ if (message !== '')
381
+ prompted(message, when);
382
+ }
383
+ else if (kind === 'AgentMessage') {
384
+ const message = contentText(item.content, 'Text');
385
+ if (message !== '')
386
+ assistant([{ type: 'text', text: message }], when);
387
+ }
388
+ else if (kind === 'FileChange' && id !== undefined && (item.status === 'completed' || item.status === 'failed' || (item.status === 'declined' && DECLINED_PATCH_TEXT.test(str(item.stderr) ?? '')))) {
389
+ settlePatch(id, item.status === 'declined' ? DECLINED_PATCH : item.status === 'failed' ? (str(item.stderr) ?? 'patch failed') : undefined, when);
390
+ }
391
+ else if (kind === 'CommandExecution' && id !== undefined && item.status === 'declined' && calls.has(id)) {
392
+ // A `declined` one has exit code -1 (events.rs:552-554); only the user's refusal text settles it, as a refusal.
393
+ if (DECLINED_OUTPUT.test(str(item.aggregated_output) ?? ''))
394
+ settleShell(id, DECLINED_SHELL, '', when);
395
+ }
396
+ else if (kind === 'CommandExecution' && id !== undefined && (item.status === 'completed' || item.status === 'failed' || item.status === 'declined') && !calls.has(id)) {
397
+ // A code-mode cell's `exec_command` (Codex 0.166+) is run by the cell's JavaScript, so no call carries the item's `exec-<uuid>` id: the item is the
398
+ // command's only record, and it is its own Bash call. Binding it to its cell by position would hold for 191 of 193 items in a live 0.167.0 store and fail
399
+ // for 2, so it is not attempted.
400
+ const exit = num(item.exit_code);
401
+ if (resolved.has(id))
402
+ return;
403
+ resolved.add(id);
404
+ const output = str(item.aggregated_output) ?? '';
405
+ const spent = isObj(item.duration) ? Math.round((num(item.duration.secs) ?? 0) * 1000 + (num(item.duration.nanos) ?? 0) / 1e6) : undefined;
406
+ const parsed = Array.isArray(item.parsed_cmd) && item.parsed_cmd.length === 1 && isObj(item.parsed_cmd[0]) ? str(item.parsed_cmd[0].cmd) : undefined;
407
+ const joined = commandOf(item);
408
+ // The command array is the launcher then its flags then the text (`pwsh.exe -Command <text>`); a single parsed command is that text alone.
409
+ const command = parsed !== undefined && (joined === undefined || parsed.length < joined.length) ? parsed : joined;
410
+ const began = spent === undefined ? when : new Date(Math.max(Date.parse(when) - spent, Date.parse(before ?? when))).toISOString();
411
+ assistant([{ type: 'tool_use', id, name: 'Bash', native: 'exec_command', input: command === undefined ? {} : { command } }], began);
412
+ // An item with no exit_code (an Option, skipped when None, items.rs:284-286) keeps its command and has no outcome, as an owned one does.
413
+ // A refused one (exit code -1) is the user's refusal only where its output says so; any other declined text leaves no outcome.
414
+ if (item.status === 'declined') {
415
+ if (DECLINED_OUTPUT.test(output))
416
+ result(id, DECLINED_SHELL, when, '');
417
+ }
418
+ else if (exit !== undefined)
419
+ result(id, failureOf(exit, output), when, output, spent !== undefined);
420
+ }
421
+ else if (kind === 'CommandExecution' && id !== undefined && (item.status === 'completed' || item.status === 'failed')) {
422
+ // The exec_command's own item: `id` is its call id, written when the process exits, even one the model never polled (header). It carries the
423
+ // real exit code and the output.
424
+ const exit = num(item.exit_code);
425
+ const output = str(item.aggregated_output) ?? '';
426
+ if (exit !== undefined)
427
+ settleShell(id, failureOf(exit, output), output, when);
428
+ }
429
+ else if (kind === 'McpToolCall' && id !== undefined && (item.status === 'completed' || item.status === 'failed')) {
430
+ const refused = isObj(item.result) && item.result.isError === true;
431
+ const failed = item.status === 'failed' || refused;
432
+ const text = isObj(item.error) ? (str(item.error.message) ?? '') : isObj(item.result) ? bodyText(item.result.content) : '';
433
+ settleMcp(id, failed ? (text === '' ? 'mcp tool call failed' : text) : undefined, when);
434
+ }
435
+ else if (kind === 'DynamicToolCall' && id !== undefined && (item.status === 'completed' || item.status === 'failed')) {
436
+ // The item's `success` and `error` (items.rs:301-322; dynamic.rs:246-262): a call with no item stays unsettled, since its output text says nothing of failure.
437
+ const failed = item.status === 'failed' || item.success === false;
438
+ const text = isObj(item.error) ? (str(item.error.message) ?? '') : '';
439
+ settleMcp(id, failed ? (text === '' ? 'dynamic tool call failed' : text) : undefined, when);
440
+ }
441
+ };
442
+ const parentKeys = (parent) => parent
443
+ .split('\n')
444
+ .map((raw) => parse(raw))
445
+ .filter((parsed) => isObj(parsed) && isObj(parsed.payload) && parsed.type !== 'session_meta')
446
+ .map((parsed) => JSON.stringify([parsed.type, parsed.payload]));
447
+ for (const raw of text.split('\n')) {
448
+ const entry = parse(raw);
449
+ if (!isObj(entry) || !isObj(entry.payload))
450
+ continue;
451
+ const payload = entry.payload;
452
+ const ordinal = num(entry.ordinal);
453
+ if (inherited !== undefined && ordinal !== undefined && ordinal < inherited)
454
+ continue;
455
+ const type = str(entry.type);
456
+ if (type !== 'session_meta' && copied !== null) {
457
+ if (copied.keys[copied.at] === JSON.stringify([entry.type, payload])) {
458
+ copied.at += 1;
459
+ continue;
460
+ }
461
+ copied = null;
462
+ }
463
+ if (type === 'session_meta' && seenMeta)
464
+ continue;
465
+ const when = stamp(entry.timestamp);
466
+ if (type === 'session_meta') {
467
+ seenMeta = true;
468
+ const id = str(payload.id);
469
+ if (id !== undefined)
470
+ base.sessionId = id;
471
+ const cwd = str(payload.cwd);
472
+ if (cwd !== undefined)
473
+ base.cwd = cwd;
474
+ const version = str(payload.cli_version);
475
+ if (version !== undefined)
476
+ base.version = version;
477
+ stamp(payload.timestamp);
478
+ const declared = toolNames(payload.dynamic_tools);
479
+ if (declared.length > 0)
480
+ tools({ offered: declared });
481
+ for (const name of declared)
482
+ dynamicNames.add(name);
483
+ inherited = num(payload.subagent_history_start_ordinal);
484
+ paginated = payload.history_mode === 'paginated';
485
+ if (inherited === undefined && parentText !== undefined && str(payload.forked_from_id) !== undefined && !isObj(payload.history_base))
486
+ copied = { keys: parentKeys(parentText), at: 0 };
487
+ }
488
+ else if (type === 'turn_context') {
489
+ model = str(payload.model) ?? model;
490
+ // The policy rides every later line as `permissionMode`, the key a Claude Code user line carries.
491
+ const mode = approvalMode(payload.approval_policy);
492
+ if (mode !== undefined)
493
+ base.permissionMode = mode;
494
+ const cwd = str(payload.cwd);
495
+ if (cwd !== undefined)
496
+ base.cwd = cwd;
497
+ }
498
+ else if (type === 'event_msg') {
499
+ const kind = str(payload.type);
500
+ if (kind === 'user_message') {
501
+ const message = str(payload.message);
502
+ if (message !== undefined)
503
+ prompted(message, when);
504
+ }
505
+ else if (kind === 'item_completed') {
506
+ if (isObj(payload.item))
507
+ completed(payload.item, when);
508
+ }
509
+ else if (kind === 'turn_aborted') {
510
+ // Only `interrupted` is the operator cutting a turn short: `replaced`, `review_ended`, and `budget_limited` are the harness's own (protocol.rs:4580-4585).
511
+ // It is written as the marker line Claude Code writes, so capture keeps it as a prompt record and the shape counts one interrupt (CAPTURE 23).
512
+ if (payload.reason === 'interrupted')
513
+ prompted(`${INTERRUPT_MARKER}]`, when);
514
+ }
515
+ else if (kind === 'agent_message') {
516
+ const message = str(payload.message);
517
+ if (message !== undefined)
518
+ assistant([{ type: 'text', text: message }], when);
519
+ }
520
+ else if (kind === 'token_count') {
521
+ const info = isObj(payload.info) ? payload.info : undefined;
522
+ const total = info !== undefined && isObj(info.total_token_usage) ? num(info.total_token_usage.total_tokens) : undefined;
523
+ const usage = usageOf(info);
524
+ // Codex repeats a token_count whose totals did not move; counting it twice would double the response.
525
+ // A token_count with no assistant line before it (a reasoning-only response) has nothing to attach to, so its usage is not counted.
526
+ if (usage !== undefined && (total === undefined || total !== lastTotal) && open !== undefined) {
527
+ const first = open.message;
528
+ if (isObj(first))
529
+ first.usage = usage;
530
+ lastTotal = total;
531
+ response += 1;
532
+ open = undefined;
533
+ }
534
+ }
535
+ else if (kind === 'mcp_tool_call_end') {
536
+ const id = str(payload.call_id);
537
+ const outcome = isObj(payload.result) ? payload.result : {};
538
+ const refused = isObj(outcome.Ok) && outcome.Ok.isError === true;
539
+ const text = typeof outcome.Err === 'string' ? outcome.Err : isObj(outcome.Ok) ? bodyText(outcome.Ok.content) : '';
540
+ if (id !== undefined)
541
+ settleMcp(id, typeof outcome.Err === 'string' || refused ? (text === '' ? 'mcp tool call failed' : text) : undefined, when);
542
+ }
543
+ else if (kind === 'patch_apply_end') {
544
+ const id = str(payload.call_id);
545
+ const declined = payload.status === 'declined';
546
+ // A declined patch settles only where its stderr is the user's refusal text (`DECLINED_PATCH_TEXT`); any other declined one stays unsettled.
547
+ if (id !== undefined && (!declined || DECLINED_PATCH_TEXT.test(str(payload.stderr) ?? ''))) {
548
+ settlePatch(id, declined ? DECLINED_PATCH : payload.success === false ? (str(payload.stderr) ?? 'patch failed') : undefined, when);
549
+ }
550
+ }
551
+ }
552
+ else if (type === 'response_item') {
553
+ const kind = str(payload.type);
554
+ if (kind === 'additional_tools') {
555
+ const declared = toolNames(payload.tools);
556
+ if (declared.length > 0)
557
+ tools({ offered: declared });
558
+ }
559
+ else if (kind === 'tool_search_output') {
560
+ const found = toolNames(payload.tools);
561
+ if (found.length > 0)
562
+ tools({ loaded: found });
563
+ }
564
+ else if (kind === 'web_search_call') {
565
+ // A search is its own response item (protocol/src/models.rs:1270; persisted, rollout/src/policy.rs:94), with no call_id and no output item: the item
566
+ // is the finished call (`response.output_item.done`, models.rs:1262-1269), so it carries no failure signal and is recorded ok, as another non-shell call is.
567
+ // Only a `search` action has a query (`query`, else the first of `queries`; models.rs:2125-2132); `open_page` and `find_in_page` carry a url (:2134-2145).
568
+ const action = isObj(payload.action) ? payload.action : {};
569
+ const first = Array.isArray(action.queries) ? str(action.queries[0]) : undefined;
570
+ const query = action.type === 'search' ? (str(action.query) ?? first) : undefined;
571
+ const id = str(payload.id) ?? `${fallbackId}-ws${String(sequence + 1)}`;
572
+ assistant([{ type: 'tool_use', id, name: 'WebSearch', native: 'web_search', input: query === undefined ? {} : { query } }], when);
573
+ result(id, undefined, when);
574
+ }
575
+ else if (kind === 'function_call' || kind === 'local_shell_call' || kind === 'custom_tool_call') {
576
+ const id = str(payload.call_id) ?? str(payload.id);
577
+ const bare = str(payload.name) ?? 'shell';
578
+ // A namespaced function (models.rs:1154) is not its bare name: only a plain one is Codex's own shell or patch tool. The router makes every
579
+ // function_call's ToolName with the default namespace filled in (core/src/tools/router.rs:285), and none, an empty string, and `functions`
580
+ // all flatten to the bare name (protocol/src/tool_name.rs:46-50; core/src/tools/mod.rs:51-53), so `functions` is no namespace here.
581
+ const declared = str(payload.namespace);
582
+ const namespace = declared === DEFAULT_FUNCTION_NAMESPACE ? undefined : declared;
583
+ // Upstream flattens any other namespace to namespace + name with no delimiter (core/src/tools/mod.rs:55-60; codex-mcp/src/tools.rs:296), and
584
+ // an MCP namespace carries its own trailing `__` (tools.rs:253), so that form joins a Claude Code MCP name as written. The `__` added to a
585
+ // namespace without one is this reader's own choice (models.rs:3485-3486: `collaboration` + `spawn_agent`), not upstream's flat name.
586
+ const joiner = namespace?.endsWith('__') === true ? '' : '__';
587
+ const name = namespace === undefined || bare.startsWith(namespace) ? bare : `${namespace}${joiner}${bare}`;
588
+ const args = kind === 'custom_tool_call' ? { input: str(payload.input) } : parse(payload.arguments);
589
+ const input = isObj(args) ? args : {};
590
+ const action = isObj(payload.action) ? payload.action : {};
591
+ if (id === undefined)
592
+ continue;
593
+ if (kind === 'local_shell_call' || (namespace === undefined && SHELL_CALLS.has(name))) {
594
+ // A code-mode `exec` call's input is a JavaScript cell, not a command, so it has none.
595
+ const command = name === 'exec' ? undefined : commandOf(kind === 'local_shell_call' ? action : input);
596
+ const poll = name === 'wait' ? input.cell_id : input.session_id;
597
+ const polled = name === 'write_stdin' || name === 'wait' ? (str(poll) ?? (num(poll) === undefined ? undefined : String(num(poll)))) : undefined;
598
+ calls.set(id, { kind: 'shell', ids: [id], ...(polled === undefined ? {} : { session: name === 'wait' ? `cell:${polled}` : polled }), ...(name === 'exec' || name === 'wait' ? { cell: true } : {}) });
599
+ // `native` keeps Codex's own name for the action table (the contract at extract.ts, as cursor.ts does); a `local_shell_call` has no name field.
600
+ assistant([{ type: 'tool_use', id, name: 'Bash', native: kind === 'local_shell_call' ? 'local_shell' : name, input: command === undefined ? {} : { command } }], when);
601
+ }
602
+ else if (namespace === undefined && PATCH_CALLS.has(name)) {
603
+ const files = patchFiles(str(input.input) ?? str(input.patch) ?? '');
604
+ const ids = (files.length === 0 ? [''] : files).map((_, index) => `${id}#${String(index)}`);
605
+ calls.set(id, { kind: 'patch', ids });
606
+ assistant(ids.map((part, index) => ({ type: 'tool_use', id: part, name: 'Edit', native: name, input: files[index] === undefined ? {} : { file_path: files[index] } })), when);
607
+ }
608
+ else {
609
+ calls.set(id, { kind: 'other', ids: [id], ...(dynamicNames.has(name) ? { dynamic: true } : {}) });
610
+ const file = str(input.path) ?? str(input.file_path);
611
+ const query = str(input.query);
612
+ assistant([{ type: 'tool_use', id, name, input: { ...(file === undefined ? {} : { file_path: file }), ...(query === undefined ? {} : { query }) } }], when);
613
+ }
614
+ }
615
+ else if (kind === 'function_call_output' || kind === 'custom_tool_call_output') {
616
+ const id = str(payload.call_id);
617
+ const known = id === undefined ? undefined : calls.get(id);
618
+ if (id !== undefined && known !== undefined && !resolved.has(id)) {
619
+ const out = outcomeOf(payload.output);
620
+ if (known.kind === 'shell') {
621
+ // A shell call settles on its exit code, or on a failure to spawn. A process still running stays unsettled and is remembered by its
622
+ // session id, so in a Legacy rollout the `write_stdin` that sees it exit settles it too. A call declined with the user's refusal text has neither and settles as that refusal; any other declined text stays unsettled.
623
+ if (out.exit === undefined && DECLINED_OUTPUT.test(out.text))
624
+ settleShell(id, DECLINED_SHELL, '', when, false);
625
+ else if (out.exit !== undefined || out.spawnError)
626
+ settleShell(id, out.spawnError ? out.text : failureOf(out.exit, out.text), out.text, when, out.exit !== undefined || out.script);
627
+ else if (out.running !== undefined && known.session === undefined)
628
+ running.set(known.cell === true ? `cell:${out.running}` : out.running, id);
629
+ }
630
+ else if (known.kind === 'other' && known.dynamic !== true) {
631
+ // Another call's output carries no failure signal (an MCP call's comes from its mcp_tool_call_end), and a patch settles only from its
632
+ // patch_apply_end, because a rejected patch comes back as plain output text (tools/handlers/apply_patch.rs).
633
+ resolved.add(id);
634
+ result(id, undefined, when);
635
+ }
636
+ }
637
+ }
638
+ }
639
+ }
640
+ return lines;
641
+ };
642
+ /**
643
+ * The thread a rollout is deposited under, or null for a main session. A spawned agent's `session_meta` names `parent_thread_id` (protocol.rs:3463-3464); a
644
+ * delegate Codex starts for itself, such as a review, gets its parent's thread id there too (core/src/codex_delegate.rs:138, through core/src/session/session.rs:1126
645
+ * and thread-store/src/local/create_thread.rs:35 into rollout/src/recorder.rs:965). Yet `SubAgentSource::parent_thread_id` is Some for ThreadSpawn only
646
+ * (protocol.rs:3376-3386), so the rollout's `source` decides when it is absent: upstream's `is_non_root_agent` is `Internal(_) | SubAgent(_)` (:3280-3285), and
647
+ * `SessionMeta.source` (:3477) serializes lowercase and externally tagged (:3141-3144, :3223-3226), so `{subagent: ...}` or `{internal: ...}`. Such a rollout belongs
648
+ * to `session_id`, "equal to the root thread's ID" (:3451-3452), or else names itself. A reverted file's parent is its own thread.
649
+ */
650
+ const parentOf = (head, id, thread) => {
651
+ const parent = str(head.parent_thread_id)?.toLowerCase() ?? thread;
652
+ if (parent !== null)
653
+ return parent;
654
+ const source = head.source;
655
+ const nonRoot = isObj(source) && (Object.hasOwn(source, 'subagent') || Object.hasOwn(source, 'internal'));
656
+ return nonRoot ? (str(head.session_id)?.toLowerCase() ?? id) : null;
657
+ };
658
+ /**
659
+ * A thread's records as one text: its own file, then each revert's file in the order they were written. A later file supersedes the records at or after
660
+ * its `from` ordinal in every earlier one (the prefix a revert retains is below it), so the turns a revert dropped are not read. A record with no
661
+ * ordinal (a line that is not a Paginated record) is kept, and so is a line that is not JSON, which `captureSession` counts. Only the reverts written at
662
+ * or before `before` count: a fork copied the lineage as it stood when it started, so a revert written later has not dropped anything from its copy.
663
+ */
664
+ const lineageText = async (session, before = Infinity) => {
665
+ const read = (file) => textOf(file).catch((error) => {
666
+ throw new Error(reasonOf(error, file));
667
+ });
668
+ const revisions = session.revisions.filter((revision) => revision.at <= before).sort((a, b) => a.at - b.at);
669
+ if (revisions.length === 0)
670
+ return read(session.file);
671
+ const parts = [{ file: session.file, from: Infinity }, ...revisions];
672
+ const texts = [];
673
+ for (const [index, part] of parts.entries()) {
674
+ const cut = Math.min(...parts.slice(index + 1).map((later) => later.from), Infinity);
675
+ const kept = (await read(part.file)).split('\n').filter((raw) => {
676
+ const entry = parse(raw);
677
+ const ordinal = isObj(entry) ? num(entry.ordinal) : undefined;
678
+ return ordinal === undefined || ordinal < cut;
679
+ });
680
+ texts.push(kept.join('\n'));
681
+ }
682
+ return texts.join('\n');
683
+ };
684
+ /** Opens the Codex store under `root`; undefined when it holds no `sessions` folder, so a machine without Codex is skipped silently. */
685
+ export const openCodexStore = async (root) => {
686
+ const files = [];
687
+ const unreadable = [];
688
+ await walk(join(root, 'sessions'), files, unreadable);
689
+ await walk(join(root, 'archived_sessions'), files, unreadable);
690
+ if (files.length === 0 && unreadable.length === 0)
691
+ return undefined;
692
+ const sessions = new Map();
693
+ const reverts = new Map();
694
+ let rejected = 0;
695
+ for (const file of files) {
696
+ const { id, thread } = idOf(basename(file));
697
+ if (!SAFE_ID.test(id)) {
698
+ rejected += 1;
699
+ continue;
700
+ }
701
+ // The active copy wins over an archived one of the same thread.
702
+ if (sessions.has(id))
703
+ continue;
704
+ try {
705
+ const info = await stat(file);
706
+ const meta = parse(await firstLine(file));
707
+ const head = isObj(meta) && isObj(meta.payload) ? meta.payload : {};
708
+ const base = isObj(head.history_base) ? head.history_base : {};
709
+ const started = Date.parse(str(head.timestamp) ?? '') || 0;
710
+ if (thread !== null && isObj(meta))
711
+ reverts.set(id, { thread, from: num(base.end_ordinal_exclusive) ?? 0, at: started });
712
+ sessions.set(id, { id, folder: str(head.cwd) ?? null, parent: parentOf(head, id, thread), agentRole: str(head.agent_role) ?? null, forkedFrom: str(head.forked_from_id)?.toLowerCase() ?? null, revisions: [], started, file, size: info.size, mtimeMs: Math.floor(info.mtimeMs) });
713
+ }
714
+ catch (error) {
715
+ unreadable.push({ file, reason: reasonOf(error, file) });
716
+ }
717
+ }
718
+ // A reverted thread is one lineage: the new file holds only the records written after the revert, so it is folded into the thread's own session, whose
719
+ // retained prefix and these records read as one. A reverted file whose thread is not in the store stays a session of its own, a subagent of that thread.
720
+ for (const [id, revert] of reverts) {
721
+ const origin = sessions.get(revert.thread);
722
+ const own = sessions.get(id);
723
+ if (origin === undefined || own === undefined)
724
+ continue;
725
+ origin.revisions.push({ file: own.file, from: revert.from, at: revert.at });
726
+ origin.size += own.size;
727
+ origin.mtimeMs = Math.max(origin.mtimeMs, own.mtimeMs);
728
+ sessions.delete(id);
729
+ }
730
+ const ordered = [...sessions.values()].sort((a, b) => a.mtimeMs - b.mtimeMs || a.id.localeCompare(b.id));
731
+ return {
732
+ sessions: ordered,
733
+ rejected,
734
+ unreadable,
735
+ transcript: async (session) => {
736
+ // A rollout that vanished after it was listed (Codex compresses an old one) fails with its reason, which names no path.
737
+ const text = await lineageText(session);
738
+ // A Copied fork's history is found by matching it against the parent's lineage as of the fork's start; a parent outside the store (or unreadable) leaves it whole.
739
+ const parent = session.forkedFrom === null ? undefined : sessions.get(session.forkedFrom);
740
+ const parentText = parent === undefined ? undefined : await lineageText(parent, session.started).catch(() => undefined);
741
+ const lines = [...rolloutLines(text, session.id, parentText).map((line) => JSON.stringify(line)), ...rolloutBadLines(text)];
742
+ return lines.join('\n') + (lines.length === 0 ? '' : '\n');
743
+ },
744
+ };
745
+ };