oh-my-codex 0.16.0 → 0.16.1

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 (263) hide show
  1. package/Cargo.lock +5 -5
  2. package/Cargo.toml +1 -1
  3. package/crates/omx-explore/src/main.rs +434 -28
  4. package/dist/agents/__tests__/native-config.test.js +50 -0
  5. package/dist/agents/__tests__/native-config.test.js.map +1 -1
  6. package/dist/agents/native-config.d.ts.map +1 -1
  7. package/dist/agents/native-config.js +3 -2
  8. package/dist/agents/native-config.js.map +1 -1
  9. package/dist/cli/__tests__/codex-plugin-layout.test.js +1 -0
  10. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  11. package/dist/cli/__tests__/explore.test.js +118 -1
  12. package/dist/cli/__tests__/explore.test.js.map +1 -1
  13. package/dist/cli/__tests__/imagegen-continuation.test.d.ts +2 -0
  14. package/dist/cli/__tests__/imagegen-continuation.test.d.ts.map +1 -0
  15. package/dist/cli/__tests__/imagegen-continuation.test.js +135 -0
  16. package/dist/cli/__tests__/imagegen-continuation.test.js.map +1 -0
  17. package/dist/cli/__tests__/index.test.js +182 -18
  18. package/dist/cli/__tests__/index.test.js.map +1 -1
  19. package/dist/cli/__tests__/launch-fallback.test.js +88 -2
  20. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  21. package/dist/cli/__tests__/ralph.test.js +62 -0
  22. package/dist/cli/__tests__/ralph.test.js.map +1 -1
  23. package/dist/cli/__tests__/setup-install-mode.test.js +45 -0
  24. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  25. package/dist/cli/__tests__/team.test.js +465 -12
  26. package/dist/cli/__tests__/team.test.js.map +1 -1
  27. package/dist/cli/__tests__/ultragoal.test.js +40 -0
  28. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  29. package/dist/cli/explore.d.ts.map +1 -1
  30. package/dist/cli/explore.js +208 -8
  31. package/dist/cli/explore.js.map +1 -1
  32. package/dist/cli/index.d.ts +11 -3
  33. package/dist/cli/index.d.ts.map +1 -1
  34. package/dist/cli/index.js +124 -18
  35. package/dist/cli/index.js.map +1 -1
  36. package/dist/cli/ralph.d.ts.map +1 -1
  37. package/dist/cli/ralph.js +37 -3
  38. package/dist/cli/ralph.js.map +1 -1
  39. package/dist/cli/setup.d.ts.map +1 -1
  40. package/dist/cli/setup.js +93 -4
  41. package/dist/cli/setup.js.map +1 -1
  42. package/dist/cli/team.d.ts +1 -0
  43. package/dist/cli/team.d.ts.map +1 -1
  44. package/dist/cli/team.js +42 -7
  45. package/dist/cli/team.js.map +1 -1
  46. package/dist/cli/ultragoal.d.ts +1 -1
  47. package/dist/cli/ultragoal.d.ts.map +1 -1
  48. package/dist/cli/ultragoal.js +6 -3
  49. package/dist/cli/ultragoal.js.map +1 -1
  50. package/dist/config/__tests__/codex-hooks.test.js +5 -0
  51. package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
  52. package/dist/config/__tests__/models.test.js +18 -1
  53. package/dist/config/__tests__/models.test.js.map +1 -1
  54. package/dist/config/codex-hooks.d.ts.map +1 -1
  55. package/dist/config/codex-hooks.js +4 -1
  56. package/dist/config/codex-hooks.js.map +1 -1
  57. package/dist/config/models.d.ts +6 -0
  58. package/dist/config/models.d.ts.map +1 -1
  59. package/dist/config/models.js +37 -0
  60. package/dist/config/models.js.map +1 -1
  61. package/dist/exec/followup.d.ts +1 -0
  62. package/dist/exec/followup.d.ts.map +1 -1
  63. package/dist/exec/followup.js +9 -3
  64. package/dist/exec/followup.js.map +1 -1
  65. package/dist/hooks/__tests__/anti-slop-workflow.test.js +19 -0
  66. package/dist/hooks/__tests__/anti-slop-workflow.test.js.map +1 -1
  67. package/dist/hooks/__tests__/consensus-execution-handoff.test.js +19 -2
  68. package/dist/hooks/__tests__/consensus-execution-handoff.test.js.map +1 -1
  69. package/dist/hooks/__tests__/deep-interview-contract.test.js +40 -0
  70. package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
  71. package/dist/hooks/__tests__/foreground-isolation-contract.test.d.ts +2 -0
  72. package/dist/hooks/__tests__/foreground-isolation-contract.test.d.ts.map +1 -0
  73. package/dist/hooks/__tests__/foreground-isolation-contract.test.js +28 -0
  74. package/dist/hooks/__tests__/foreground-isolation-contract.test.js.map +1 -0
  75. package/dist/hooks/__tests__/session.test.js +32 -0
  76. package/dist/hooks/__tests__/session.test.js.map +1 -1
  77. package/dist/hooks/codebase-map.d.ts.map +1 -1
  78. package/dist/hooks/codebase-map.js +3 -2
  79. package/dist/hooks/codebase-map.js.map +1 -1
  80. package/dist/hooks/extensibility/dispatcher.d.ts.map +1 -1
  81. package/dist/hooks/extensibility/dispatcher.js +6 -4
  82. package/dist/hooks/extensibility/dispatcher.js.map +1 -1
  83. package/dist/hooks/extensibility/logging.d.ts.map +1 -1
  84. package/dist/hooks/extensibility/logging.js +3 -2
  85. package/dist/hooks/extensibility/logging.js.map +1 -1
  86. package/dist/hooks/extensibility/sdk/paths.d.ts.map +1 -1
  87. package/dist/hooks/extensibility/sdk/paths.js +4 -3
  88. package/dist/hooks/extensibility/sdk/paths.js.map +1 -1
  89. package/dist/hooks/session.d.ts.map +1 -1
  90. package/dist/hooks/session.js +22 -12
  91. package/dist/hooks/session.js.map +1 -1
  92. package/dist/hud/__tests__/hud-tmux-injection.test.js +8 -7
  93. package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
  94. package/dist/hud/__tests__/reconcile.test.js +1 -1
  95. package/dist/hud/__tests__/state.test.js +24 -0
  96. package/dist/hud/__tests__/state.test.js.map +1 -1
  97. package/dist/hud/index.js +1 -1
  98. package/dist/hud/index.js.map +1 -1
  99. package/dist/hud/state.d.ts.map +1 -1
  100. package/dist/hud/state.js +22 -8
  101. package/dist/hud/state.js.map +1 -1
  102. package/dist/hud/tmux.js +1 -1
  103. package/dist/hud/tmux.js.map +1 -1
  104. package/dist/imagegen/continuation.d.ts +44 -0
  105. package/dist/imagegen/continuation.d.ts.map +1 -0
  106. package/dist/imagegen/continuation.js +220 -0
  107. package/dist/imagegen/continuation.js.map +1 -0
  108. package/dist/mcp/__tests__/bootstrap.test.js +47 -2
  109. package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
  110. package/dist/mcp/__tests__/server-lifecycle.test.js +49 -1
  111. package/dist/mcp/__tests__/server-lifecycle.test.js.map +1 -1
  112. package/dist/mcp/bootstrap.d.ts +2 -0
  113. package/dist/mcp/bootstrap.d.ts.map +1 -1
  114. package/dist/mcp/bootstrap.js +95 -15
  115. package/dist/mcp/bootstrap.js.map +1 -1
  116. package/dist/mcp/lifecycle-telemetry.d.ts +16 -0
  117. package/dist/mcp/lifecycle-telemetry.d.ts.map +1 -0
  118. package/dist/mcp/lifecycle-telemetry.js +95 -0
  119. package/dist/mcp/lifecycle-telemetry.js.map +1 -0
  120. package/dist/pipeline/__tests__/stages.test.js +274 -5
  121. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  122. package/dist/pipeline/stages/team-exec.d.ts +2 -0
  123. package/dist/pipeline/stages/team-exec.d.ts.map +1 -1
  124. package/dist/pipeline/stages/team-exec.js +51 -26
  125. package/dist/pipeline/stages/team-exec.js.map +1 -1
  126. package/dist/planning/__tests__/artifacts.test.js +138 -3
  127. package/dist/planning/__tests__/artifacts.test.js.map +1 -1
  128. package/dist/planning/__tests__/context-pack-status.test.d.ts +2 -0
  129. package/dist/planning/__tests__/context-pack-status.test.d.ts.map +1 -0
  130. package/dist/planning/__tests__/context-pack-status.test.js +271 -0
  131. package/dist/planning/__tests__/context-pack-status.test.js.map +1 -0
  132. package/dist/planning/artifacts.d.ts +12 -1
  133. package/dist/planning/artifacts.d.ts.map +1 -1
  134. package/dist/planning/artifacts.js +32 -9
  135. package/dist/planning/artifacts.js.map +1 -1
  136. package/dist/planning/context-pack-status.d.ts +42 -0
  137. package/dist/planning/context-pack-status.d.ts.map +1 -0
  138. package/dist/planning/context-pack-status.js +479 -0
  139. package/dist/planning/context-pack-status.js.map +1 -0
  140. package/dist/runtime/__tests__/process-tree.test.d.ts +2 -0
  141. package/dist/runtime/__tests__/process-tree.test.d.ts.map +1 -0
  142. package/dist/runtime/__tests__/process-tree.test.js +107 -0
  143. package/dist/runtime/__tests__/process-tree.test.js.map +1 -0
  144. package/dist/runtime/process-tree.d.ts +28 -0
  145. package/dist/runtime/process-tree.d.ts.map +1 -0
  146. package/dist/runtime/process-tree.js +230 -0
  147. package/dist/runtime/process-tree.js.map +1 -0
  148. package/dist/scripts/__tests__/codex-native-hook.test.js +205 -1
  149. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  150. package/dist/scripts/__tests__/notify-state-io.test.d.ts +2 -0
  151. package/dist/scripts/__tests__/notify-state-io.test.d.ts.map +1 -0
  152. package/dist/scripts/__tests__/notify-state-io.test.js +40 -0
  153. package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -0
  154. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  155. package/dist/scripts/codex-native-hook.js +111 -7
  156. package/dist/scripts/codex-native-hook.js.map +1 -1
  157. package/dist/scripts/notify-hook/managed-tmux.d.ts.map +1 -1
  158. package/dist/scripts/notify-hook/managed-tmux.js +6 -9
  159. package/dist/scripts/notify-hook/managed-tmux.js.map +1 -1
  160. package/dist/scripts/notify-hook/process-runner.d.ts.map +1 -1
  161. package/dist/scripts/notify-hook/process-runner.js +4 -1
  162. package/dist/scripts/notify-hook/process-runner.js.map +1 -1
  163. package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
  164. package/dist/scripts/notify-hook/state-io.js +4 -7
  165. package/dist/scripts/notify-hook/state-io.js.map +1 -1
  166. package/dist/scripts/notify-hook.js +25 -3
  167. package/dist/scripts/notify-hook.js.map +1 -1
  168. package/dist/scripts/verify-native-agents.d.ts.map +1 -1
  169. package/dist/scripts/verify-native-agents.js +3 -1
  170. package/dist/scripts/verify-native-agents.js.map +1 -1
  171. package/dist/sidecar/__tests__/tmux.test.js +1 -1
  172. package/dist/sidecar/__tests__/tmux.test.js.map +1 -1
  173. package/dist/sidecar/tmux.js +1 -1
  174. package/dist/sidecar/tmux.js.map +1 -1
  175. package/dist/state/__tests__/workflow-transition.test.js +45 -1
  176. package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
  177. package/dist/state/workflow-transition-reconcile.js +2 -2
  178. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  179. package/dist/state/workflow-transition.js +2 -2
  180. package/dist/state/workflow-transition.js.map +1 -1
  181. package/dist/team/__tests__/approved-execution.test.js +96 -0
  182. package/dist/team/__tests__/approved-execution.test.js.map +1 -1
  183. package/dist/team/__tests__/followup-planner.test.js +16 -0
  184. package/dist/team/__tests__/followup-planner.test.js.map +1 -1
  185. package/dist/team/__tests__/model-contract.test.js +16 -0
  186. package/dist/team/__tests__/model-contract.test.js.map +1 -1
  187. package/dist/team/__tests__/repo-aware-decomposition.test.js +20 -0
  188. package/dist/team/__tests__/repo-aware-decomposition.test.js.map +1 -1
  189. package/dist/team/__tests__/runtime-cli.test.js +16 -0
  190. package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
  191. package/dist/team/__tests__/runtime.test.js +209 -11
  192. package/dist/team/__tests__/runtime.test.js.map +1 -1
  193. package/dist/team/__tests__/scaling.test.js +110 -0
  194. package/dist/team/__tests__/scaling.test.js.map +1 -1
  195. package/dist/team/__tests__/tmux-session.test.js +9 -0
  196. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  197. package/dist/team/__tests__/worker-runtime-identity.test.js +6 -0
  198. package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
  199. package/dist/team/approved-execution.d.ts +13 -0
  200. package/dist/team/approved-execution.d.ts.map +1 -1
  201. package/dist/team/approved-execution.js +40 -22
  202. package/dist/team/approved-execution.js.map +1 -1
  203. package/dist/team/followup-planner.d.ts +1 -0
  204. package/dist/team/followup-planner.d.ts.map +1 -1
  205. package/dist/team/followup-planner.js +9 -9
  206. package/dist/team/followup-planner.js.map +1 -1
  207. package/dist/team/model-contract.d.ts +1 -1
  208. package/dist/team/model-contract.d.ts.map +1 -1
  209. package/dist/team/model-contract.js +4 -3
  210. package/dist/team/model-contract.js.map +1 -1
  211. package/dist/team/repo-aware-decomposition.d.ts +1 -0
  212. package/dist/team/repo-aware-decomposition.d.ts.map +1 -1
  213. package/dist/team/repo-aware-decomposition.js +5 -1
  214. package/dist/team/repo-aware-decomposition.js.map +1 -1
  215. package/dist/team/runtime-cli.d.ts +4 -0
  216. package/dist/team/runtime-cli.d.ts.map +1 -1
  217. package/dist/team/runtime-cli.js +14 -1
  218. package/dist/team/runtime-cli.js.map +1 -1
  219. package/dist/team/runtime.d.ts +1 -0
  220. package/dist/team/runtime.d.ts.map +1 -1
  221. package/dist/team/runtime.js +46 -16
  222. package/dist/team/runtime.js.map +1 -1
  223. package/dist/team/scaling.d.ts.map +1 -1
  224. package/dist/team/scaling.js +13 -6
  225. package/dist/team/scaling.js.map +1 -1
  226. package/dist/team/tmux-session.d.ts.map +1 -1
  227. package/dist/team/tmux-session.js +7 -0
  228. package/dist/team/tmux-session.js.map +1 -1
  229. package/dist/ultragoal/__tests__/artifacts.test.js +51 -0
  230. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  231. package/dist/ultragoal/__tests__/docs-contract.test.d.ts +2 -0
  232. package/dist/ultragoal/__tests__/docs-contract.test.d.ts.map +1 -0
  233. package/dist/ultragoal/__tests__/docs-contract.test.js +23 -0
  234. package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -0
  235. package/dist/ultragoal/artifacts.d.ts +2 -2
  236. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  237. package/dist/ultragoal/artifacts.js +37 -1
  238. package/dist/ultragoal/artifacts.js.map +1 -1
  239. package/dist/verification/__tests__/ci-rust-gates.test.js +44 -14
  240. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  241. package/package.json +1 -1
  242. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  243. package/plugins/oh-my-codex/skills/ai-slop-cleaner/SKILL.md +9 -0
  244. package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +25 -2
  245. package/plugins/oh-my-codex/skills/plan/SKILL.md +7 -4
  246. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +13 -3
  247. package/plugins/oh-my-codex/skills/team/SKILL.md +2 -2
  248. package/plugins/oh-my-codex/skills/visual-ralph/SKILL.md +8 -0
  249. package/prompts/planner.md +1 -1
  250. package/skills/ai-slop-cleaner/SKILL.md +9 -0
  251. package/skills/deep-interview/SKILL.md +25 -2
  252. package/skills/plan/SKILL.md +7 -4
  253. package/skills/ralplan/SKILL.md +13 -3
  254. package/skills/team/SKILL.md +2 -2
  255. package/skills/visual-ralph/SKILL.md +8 -0
  256. package/src/scripts/__tests__/codex-native-hook.test.ts +231 -1
  257. package/src/scripts/__tests__/notify-state-io.test.ts +73 -0
  258. package/src/scripts/codex-native-hook.ts +129 -14
  259. package/src/scripts/notify-hook/managed-tmux.ts +6 -7
  260. package/src/scripts/notify-hook/process-runner.ts +4 -1
  261. package/src/scripts/notify-hook/state-io.ts +5 -7
  262. package/src/scripts/notify-hook.ts +26 -3
  263. package/src/scripts/verify-native-agents.ts +3 -1
package/Cargo.lock CHANGED
@@ -32,14 +32,14 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
32
32
 
33
33
  [[package]]
34
34
  name = "omx-explore-harness"
35
- version = "0.16.0"
35
+ version = "0.16.1"
36
36
  dependencies = [
37
37
  "libc",
38
38
  ]
39
39
 
40
40
  [[package]]
41
41
  name = "omx-mux"
42
- version = "0.16.0"
42
+ version = "0.16.1"
43
43
  dependencies = [
44
44
  "serde",
45
45
  "serde_json",
@@ -47,7 +47,7 @@ dependencies = [
47
47
 
48
48
  [[package]]
49
49
  name = "omx-runtime"
50
- version = "0.16.0"
50
+ version = "0.16.1"
51
51
  dependencies = [
52
52
  "omx-mux",
53
53
  "omx-runtime-core",
@@ -56,7 +56,7 @@ dependencies = [
56
56
 
57
57
  [[package]]
58
58
  name = "omx-runtime-core"
59
- version = "0.16.0"
59
+ version = "0.16.1"
60
60
  dependencies = [
61
61
  "fs2",
62
62
  "serde",
@@ -65,7 +65,7 @@ dependencies = [
65
65
 
66
66
  [[package]]
67
67
  name = "omx-sparkshell"
68
- version = "0.16.0"
68
+ version = "0.16.1"
69
69
  dependencies = [
70
70
  "omx-mux",
71
71
  ]
package/Cargo.toml CHANGED
@@ -10,7 +10,7 @@ resolver = "2"
10
10
 
11
11
  [workspace.package]
12
12
 
13
- version = "0.16.0"
13
+ version = "0.16.1"
14
14
 
15
15
  edition = "2021"
16
16
  rust-version = "1.73"
@@ -6,22 +6,35 @@ use std::fs::{
6
6
  use std::io::{self, BufRead, BufReader, Read};
7
7
  use std::path::{Path, PathBuf};
8
8
  use std::process::{Child, Command, Output, Stdio};
9
- use std::sync::mpsc::{self, Receiver, RecvTimeoutError};
9
+ use std::sync::mpsc::{self, Receiver, RecvTimeoutError, TryRecvError};
10
10
  use std::thread;
11
11
  use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
12
12
 
13
13
  const CODEX_BIN_ENV: &str = "OMX_EXPLORE_CODEX_BIN";
14
14
  const HARNESS_ROOT_ENV: &str = "OMX_EXPLORE_ROOT";
15
15
  const CODEX_TIMEOUT_MS_ENV: &str = "OMX_EXPLORE_CODEX_TIMEOUT_MS";
16
+ const PROCESS_LIMIT_ENV: &str = "OMX_EXPLORE_PROCESS_LIMIT";
17
+ const CODEX_OUTPUT_LIMIT_BYTES_ENV: &str = "OMX_EXPLORE_CODEX_OUTPUT_LIMIT_BYTES";
16
18
  const INTERNAL_DIRECT_WRAPPER_FLAG: &str = "--internal-allowlist-direct";
17
19
  const INTERNAL_SHELL_WRAPPER_FLAG: &str = "--internal-allowlist-shell";
18
20
  const TEMP_ALLOWLIST_DIR_PREFIX: &str = "omx-explore-allowlist-";
19
21
  const DEFAULT_CODEX_TIMEOUT_MS: u64 = 180_000;
22
+ const DEFAULT_PROCESS_LIMIT: usize = 96;
23
+ const DEFAULT_CODEX_OUTPUT_LIMIT_BYTES: usize = 8 * 1024 * 1024;
24
+ const PROCESS_LIMIT_POLL_MS: u64 = 100;
20
25
  const PROCESS_TERMINATION_GRACE_MS: u64 = 500;
21
26
  const PIPE_READER_READY_GRACE_MS: u64 = 25;
22
27
  const PIPE_READER_JOIN_GRACE_MS: u64 = 500;
23
- const EXPLORE_SUBPROCESS_ENV_VARS_TO_SCRUB: &[&str] =
24
- &["BASH_ENV", "ENV", "PROMPT_COMMAND", "NODE_OPTIONS"];
28
+ const EXPLORE_SUBPROCESS_ENV_VARS_TO_SCRUB: &[&str] = &[
29
+ "BASH_ENV",
30
+ "ENV",
31
+ "PROMPT_COMMAND",
32
+ "NODE_OPTIONS",
33
+ "SHELLOPTS",
34
+ "BASHOPTS",
35
+ "GREP_OPTIONS",
36
+ "GREP_COLORS",
37
+ ];
25
38
  const WINDOWS_UNSUPPORTED_ALLOWLIST_MESSAGE: &str =
26
39
  "omx explore built-in harness is not ready on Windows because its allowlist runtime relies on POSIX sh/bash wrappers. Set OMX_EXPLORE_BIN to a compatible custom harness, prefer `omx sparkshell` for shell-native read-only lookups, or run `omx doctor` for readiness details.";
27
40
 
@@ -334,13 +347,59 @@ fn invoke_codex(args: &Args, model: &str, prompt_contract: &str) -> io::Result<A
334
347
  ),
335
348
  output_markdown: None,
336
349
  }),
350
+ TimedCommandOutput::ProcessLimitExceeded {
351
+ stderr,
352
+ process_count,
353
+ process_limit,
354
+ } => Ok(AttemptResult {
355
+ status_code: 125,
356
+ stderr: format!(
357
+ "[omx explore] codex exec exceeded per-run process limit ({process_count}>{process_limit}); terminated process tree to avoid runaway shell storms{}{}",
358
+ if stderr.trim().is_empty() {
359
+ ""
360
+ } else {
361
+ ". stderr before termination: "
362
+ },
363
+ stderr.trim()
364
+ ),
365
+ output_markdown: None,
366
+ }),
367
+ TimedCommandOutput::OutputLimitExceeded {
368
+ stderr,
369
+ output_limit,
370
+ stream,
371
+ } => Ok(AttemptResult {
372
+ status_code: 126,
373
+ stderr: format!(
374
+ "[omx explore] codex exec exceeded subprocess {stream} output limit ({output_limit} bytes); terminated process tree to avoid unbounded memory growth{}{}",
375
+ if stderr.trim().is_empty() {
376
+ ""
377
+ } else {
378
+ ". stderr before termination: "
379
+ },
380
+ stderr.trim()
381
+ ),
382
+ output_markdown: None,
383
+ }),
337
384
  }
338
385
  }
339
386
 
340
387
  #[derive(Debug)]
341
388
  enum TimedCommandOutput {
342
389
  Completed(Output),
343
- TimedOut { stderr: String },
390
+ TimedOut {
391
+ stderr: String,
392
+ },
393
+ ProcessLimitExceeded {
394
+ stderr: String,
395
+ process_count: usize,
396
+ process_limit: usize,
397
+ },
398
+ OutputLimitExceeded {
399
+ stderr: String,
400
+ output_limit: usize,
401
+ stream: &'static str,
402
+ },
344
403
  }
345
404
 
346
405
  fn codex_timeout() -> Duration {
@@ -352,6 +411,22 @@ fn codex_timeout() -> Duration {
352
411
  Duration::from_millis(timeout_ms)
353
412
  }
354
413
 
414
+ fn codex_output_limit_bytes() -> usize {
415
+ env::var(CODEX_OUTPUT_LIMIT_BYTES_ENV)
416
+ .ok()
417
+ .and_then(|value| value.trim().parse::<usize>().ok())
418
+ .filter(|value| *value > 0)
419
+ .unwrap_or(DEFAULT_CODEX_OUTPUT_LIMIT_BYTES)
420
+ }
421
+
422
+ fn process_limit() -> usize {
423
+ env::var(PROCESS_LIMIT_ENV)
424
+ .ok()
425
+ .and_then(|value| value.trim().parse::<usize>().ok())
426
+ .filter(|value| *value > 0)
427
+ .unwrap_or(DEFAULT_PROCESS_LIMIT)
428
+ }
429
+
355
430
  fn run_command_with_timeout(
356
431
  mut command: Command,
357
432
  timeout: Duration,
@@ -360,19 +435,37 @@ fn run_command_with_timeout(
360
435
  configure_process_group(&mut command);
361
436
  let mut child = command.spawn()?;
362
437
 
363
- let stdout_reader = spawn_pipe_reader(child.stdout.take());
364
- let stderr_reader = spawn_pipe_reader(child.stderr.take());
438
+ let output_limit = codex_output_limit_bytes();
439
+ let mut stdout_reader = spawn_pipe_reader("stdout", child.stdout.take(), output_limit);
440
+ let mut stderr_reader = spawn_pipe_reader("stderr", child.stderr.take(), output_limit);
365
441
 
366
442
  let deadline = Instant::now() + timeout;
443
+ let process_limit = process_limit();
444
+ let mut next_process_limit_poll = Instant::now() + Duration::from_millis(PROCESS_LIMIT_POLL_MS);
367
445
  loop {
368
446
  if let Some(status) = child.try_wait()? {
369
- let (stdout, stderr) = collect_completed_output(
447
+ // The wrapper may exit while grandchildren keep the process group
448
+ // alive. Sweep it before collecting pipes so completed harness
449
+ // runs cannot leave detached shells behind.
450
+ terminate_child_process_tree(&mut child);
451
+ let output = collect_completed_output(
370
452
  &mut child,
371
- &stdout_reader,
372
- &stderr_reader,
453
+ &mut stdout_reader,
454
+ &mut stderr_reader,
373
455
  Duration::from_millis(PIPE_READER_READY_GRACE_MS),
374
456
  Duration::from_millis(PIPE_READER_JOIN_GRACE_MS),
375
- )?;
457
+ );
458
+ let (stdout, stderr) = match output {
459
+ Ok(output) => output,
460
+ Err(err) if is_output_limit_error(&err) => {
461
+ return Ok(TimedCommandOutput::OutputLimitExceeded {
462
+ stderr: String::new(),
463
+ output_limit,
464
+ stream: output_limit_stream(&err),
465
+ });
466
+ }
467
+ Err(err) => return Err(err),
468
+ };
376
469
  return Ok(TimedCommandOutput::Completed(Output {
377
470
  status,
378
471
  stdout,
@@ -384,37 +477,192 @@ fn run_command_with_timeout(
384
477
  terminate_child_process_tree(&mut child);
385
478
  let _ = child.wait();
386
479
  let reader_timeout = Duration::from_millis(PIPE_READER_JOIN_GRACE_MS);
387
- let _ = receive_pipe_reader(&stdout_reader, reader_timeout);
388
- let stderr = receive_pipe_reader(&stderr_reader, reader_timeout).unwrap_or_default();
480
+ let _ = receive_pipe_reader(&mut stdout_reader, reader_timeout);
481
+ let stderr =
482
+ receive_pipe_reader(&mut stderr_reader, reader_timeout).unwrap_or_default();
389
483
  return Ok(TimedCommandOutput::TimedOut {
390
484
  stderr: String::from_utf8_lossy(&stderr).into_owned(),
391
485
  });
392
486
  }
393
487
 
488
+ if Instant::now() >= next_process_limit_poll {
489
+ next_process_limit_poll = Instant::now() + Duration::from_millis(PROCESS_LIMIT_POLL_MS);
490
+ if let Some(process_count) = count_process_tree(child.id()) {
491
+ if process_count > process_limit {
492
+ terminate_child_process_tree(&mut child);
493
+ let _ = child.wait();
494
+ let reader_timeout = Duration::from_millis(PIPE_READER_JOIN_GRACE_MS);
495
+ let _ = receive_pipe_reader(&mut stdout_reader, reader_timeout);
496
+ let stderr =
497
+ receive_pipe_reader(&mut stderr_reader, reader_timeout).unwrap_or_default();
498
+ return Ok(TimedCommandOutput::ProcessLimitExceeded {
499
+ stderr: String::from_utf8_lossy(&stderr).into_owned(),
500
+ process_count,
501
+ process_limit,
502
+ });
503
+ }
504
+ }
505
+ }
506
+
507
+ if let Some(stream) = poll_output_limit(&mut stdout_reader, &mut stderr_reader)? {
508
+ terminate_child_process_tree(&mut child);
509
+ let _ = child.wait();
510
+ let reader_timeout = Duration::from_millis(PIPE_READER_JOIN_GRACE_MS);
511
+ let stderr = if stream == "stderr" {
512
+ Vec::new()
513
+ } else {
514
+ receive_pipe_reader(&mut stderr_reader, reader_timeout).unwrap_or_default()
515
+ };
516
+ return Ok(TimedCommandOutput::OutputLimitExceeded {
517
+ stderr: String::from_utf8_lossy(&stderr).into_owned(),
518
+ output_limit,
519
+ stream,
520
+ });
521
+ }
522
+
394
523
  thread::sleep(Duration::from_millis(25));
395
524
  }
396
525
  }
397
526
 
398
- fn spawn_pipe_reader<R: Read + Send + 'static>(pipe: Option<R>) -> Receiver<io::Result<Vec<u8>>> {
527
+ #[cfg(target_os = "linux")]
528
+ fn count_process_tree(root_pid: u32) -> Option<usize> {
529
+ use std::collections::HashMap;
530
+ let entries = std::fs::read_dir("/proc").ok()?;
531
+ let mut children: HashMap<u32, Vec<u32>> = HashMap::new();
532
+ for entry in entries.flatten() {
533
+ let name = entry.file_name();
534
+ let Some(name) = name.to_str() else {
535
+ continue;
536
+ };
537
+ let Ok(pid) = name.parse::<u32>() else {
538
+ continue;
539
+ };
540
+ let Ok(stat) = std::fs::read_to_string(format!("/proc/{pid}/stat")) else {
541
+ continue;
542
+ };
543
+ let Some(close_paren) = stat.rfind(')') else {
544
+ continue;
545
+ };
546
+ let fields: Vec<&str> = stat[close_paren + 2..].split(' ').collect();
547
+ let Some(ppid) = fields.get(1).and_then(|field| field.parse::<u32>().ok()) else {
548
+ continue;
549
+ };
550
+ children.entry(ppid).or_default().push(pid);
551
+ }
552
+ let mut count = 1;
553
+ let mut stack = children.remove(&root_pid).unwrap_or_default();
554
+ while let Some(pid) = stack.pop() {
555
+ count += 1;
556
+ if let Some(mut nested) = children.remove(&pid) {
557
+ stack.append(&mut nested);
558
+ }
559
+ }
560
+ Some(count)
561
+ }
562
+
563
+ #[cfg(not(target_os = "linux"))]
564
+ fn count_process_tree(_root_pid: u32) -> Option<usize> {
565
+ None
566
+ }
567
+
568
+ struct PipeReader {
569
+ receiver: Receiver<io::Result<Vec<u8>>>,
570
+ cached: Option<io::Result<Vec<u8>>>,
571
+ }
572
+
573
+ fn spawn_pipe_reader<R: Read + Send + 'static>(
574
+ stream: &'static str,
575
+ pipe: Option<R>,
576
+ output_limit: usize,
577
+ ) -> PipeReader {
399
578
  let (sender, receiver) = mpsc::channel();
400
579
  thread::spawn(move || {
401
- let _ = sender.send(read_pipe_to_end(pipe));
580
+ let _ = sender.send(read_pipe_bounded(pipe, stream, output_limit));
402
581
  });
403
- receiver
582
+ PipeReader {
583
+ receiver,
584
+ cached: None,
585
+ }
404
586
  }
405
587
 
406
- fn read_pipe_to_end<R: Read + Send + 'static>(pipe: Option<R>) -> io::Result<Vec<u8>> {
588
+ fn read_pipe_bounded<R: Read + Send + 'static>(
589
+ pipe: Option<R>,
590
+ stream: &'static str,
591
+ output_limit: usize,
592
+ ) -> io::Result<Vec<u8>> {
407
593
  let mut bytes = Vec::new();
408
- if let Some(mut pipe) = pipe {
409
- pipe.read_to_end(&mut bytes)?;
594
+ let Some(pipe) = pipe else {
595
+ return Ok(bytes);
596
+ };
597
+ let mut reader = BufReader::new(pipe);
598
+ let mut chunk = [0_u8; 8192];
599
+ loop {
600
+ let read = reader.read(&mut chunk)?;
601
+ if read == 0 {
602
+ return Ok(bytes);
603
+ }
604
+ if bytes.len().saturating_add(read) > output_limit {
605
+ return Err(output_limit_error(stream, output_limit));
606
+ }
607
+ bytes.extend_from_slice(&chunk[..read]);
608
+ }
609
+ }
610
+
611
+ fn output_limit_error(stream: &'static str, output_limit: usize) -> io::Error {
612
+ io::Error::new(
613
+ io::ErrorKind::Other,
614
+ format!("subprocess {stream} exceeded output limit of {output_limit} bytes"),
615
+ )
616
+ }
617
+
618
+ fn is_output_limit_error(err: &io::Error) -> bool {
619
+ err.to_string().contains("exceeded output limit")
620
+ }
621
+
622
+ fn output_limit_stream(err: &io::Error) -> &'static str {
623
+ if err.to_string().contains("stderr") {
624
+ "stderr"
625
+ } else {
626
+ "stdout"
627
+ }
628
+ }
629
+
630
+ fn poll_output_limit(
631
+ stdout_reader: &mut PipeReader,
632
+ stderr_reader: &mut PipeReader,
633
+ ) -> io::Result<Option<&'static str>> {
634
+ if let Some(stream) = poll_one_output_limit("stdout", stdout_reader)? {
635
+ return Ok(Some(stream));
636
+ }
637
+ poll_one_output_limit("stderr", stderr_reader)
638
+ }
639
+
640
+ fn poll_one_output_limit(
641
+ stream: &'static str,
642
+ reader: &mut PipeReader,
643
+ ) -> io::Result<Option<&'static str>> {
644
+ if reader.cached.is_some() {
645
+ return Ok(None);
646
+ }
647
+ match reader.receiver.try_recv() {
648
+ Ok(Ok(bytes)) => {
649
+ reader.cached = Some(Ok(bytes));
650
+ Ok(None)
651
+ }
652
+ Ok(Err(err)) if is_output_limit_error(&err) => Ok(Some(stream)),
653
+ Ok(Err(err)) => Err(err),
654
+ Err(TryRecvError::Empty) => Ok(None),
655
+ Err(TryRecvError::Disconnected) => Err(io::Error::new(
656
+ io::ErrorKind::Other,
657
+ "subprocess output reader disconnected",
658
+ )),
410
659
  }
411
- Ok(bytes)
412
660
  }
413
661
 
414
662
  fn collect_completed_output(
415
663
  child: &mut Child,
416
- stdout_reader: &Receiver<io::Result<Vec<u8>>>,
417
- stderr_reader: &Receiver<io::Result<Vec<u8>>>,
664
+ stdout_reader: &mut PipeReader,
665
+ stderr_reader: &mut PipeReader,
418
666
  ready_timeout: Duration,
419
667
  cleanup_timeout: Duration,
420
668
  ) -> io::Result<(Vec<u8>, Vec<u8>)> {
@@ -438,21 +686,21 @@ fn collect_completed_output(
438
686
  }
439
687
 
440
688
  fn receive_pipe_reader_if_ready(
441
- receiver: &Receiver<io::Result<Vec<u8>>>,
689
+ reader: &mut PipeReader,
442
690
  timeout: Duration,
443
691
  ) -> io::Result<Option<Vec<u8>>> {
444
- match receive_pipe_reader(receiver, timeout) {
692
+ match receive_pipe_reader(reader, timeout) {
445
693
  Ok(bytes) => Ok(Some(bytes)),
446
694
  Err(err) if err.kind() == io::ErrorKind::TimedOut => Ok(None),
447
695
  Err(err) => Err(err),
448
696
  }
449
697
  }
450
698
 
451
- fn receive_pipe_reader(
452
- receiver: &Receiver<io::Result<Vec<u8>>>,
453
- timeout: Duration,
454
- ) -> io::Result<Vec<u8>> {
455
- match receiver.recv_timeout(timeout) {
699
+ fn receive_pipe_reader(reader: &mut PipeReader, timeout: Duration) -> io::Result<Vec<u8>> {
700
+ if let Some(result) = reader.cached.take() {
701
+ return result;
702
+ }
703
+ match reader.receiver.recv_timeout(timeout) {
456
704
  Ok(result) => result,
457
705
  Err(RecvTimeoutError::Timeout) => Err(io::Error::new(
458
706
  io::ErrorKind::TimedOut,
@@ -2476,6 +2724,126 @@ sleep 30
2476
2724
  assert_eq!(read_to_string(&term_file).unwrap_or_default(), "term");
2477
2725
  }
2478
2726
 
2727
+ #[cfg(target_os = "linux")]
2728
+ #[test]
2729
+ fn run_command_with_timeout_aborts_suspicious_process_storm() {
2730
+ let _env_guard = env_lock();
2731
+ let _process_guard = process_tree_lock();
2732
+ let root = temp_allowlist_dir().expect("temp root");
2733
+ let script = root.path.join("storm.sh");
2734
+ write_executable(
2735
+ &script,
2736
+ r#"#!/bin/sh
2737
+ while :; do
2738
+ sleep 30 &
2739
+ sleep 0.01
2740
+ done
2741
+ "#,
2742
+ )
2743
+ .expect("write script");
2744
+
2745
+ unsafe {
2746
+ env::set_var(PROCESS_LIMIT_ENV, "12");
2747
+ }
2748
+ let started = Instant::now();
2749
+ let result = run_command_with_timeout(Command::new(&script), Duration::from_secs(10))
2750
+ .expect("run with process storm");
2751
+ unsafe {
2752
+ env::remove_var(PROCESS_LIMIT_ENV);
2753
+ }
2754
+
2755
+ let TimedCommandOutput::ProcessLimitExceeded {
2756
+ process_count,
2757
+ process_limit,
2758
+ ..
2759
+ } = result
2760
+ else {
2761
+ panic!("expected process limit failure");
2762
+ };
2763
+ assert!(process_count > process_limit);
2764
+ assert!(started.elapsed() < Duration::from_secs(5));
2765
+ }
2766
+
2767
+ #[cfg(unix)]
2768
+ #[test]
2769
+ fn run_command_with_timeout_fails_closed_on_large_stdout() {
2770
+ let _env_guard = env_lock();
2771
+ let _process_guard = process_tree_lock();
2772
+ let root = temp_allowlist_dir().expect("temp root");
2773
+ let script = root.path.join("large-stdout.sh");
2774
+ write_executable(
2775
+ &script,
2776
+ r#"#!/bin/sh
2777
+ while :; do
2778
+ printf 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
2779
+ done
2780
+ "#,
2781
+ )
2782
+ .expect("write script");
2783
+
2784
+ unsafe {
2785
+ env::set_var(CODEX_OUTPUT_LIMIT_BYTES_ENV, "4096");
2786
+ }
2787
+ let started = Instant::now();
2788
+ let result = run_command_with_timeout(Command::new(&script), Duration::from_secs(10))
2789
+ .expect("run with large stdout");
2790
+ unsafe {
2791
+ env::remove_var(CODEX_OUTPUT_LIMIT_BYTES_ENV);
2792
+ }
2793
+
2794
+ let TimedCommandOutput::OutputLimitExceeded {
2795
+ stream,
2796
+ output_limit,
2797
+ ..
2798
+ } = result
2799
+ else {
2800
+ panic!("expected stdout output limit failure");
2801
+ };
2802
+ assert_eq!(stream, "stdout");
2803
+ assert_eq!(output_limit, 4096);
2804
+ assert!(started.elapsed() < Duration::from_secs(3));
2805
+ }
2806
+
2807
+ #[cfg(unix)]
2808
+ #[test]
2809
+ fn run_command_with_timeout_fails_closed_on_large_stderr() {
2810
+ let _env_guard = env_lock();
2811
+ let _process_guard = process_tree_lock();
2812
+ let root = temp_allowlist_dir().expect("temp root");
2813
+ let script = root.path.join("large-stderr.sh");
2814
+ write_executable(
2815
+ &script,
2816
+ r#"#!/bin/sh
2817
+ while :; do
2818
+ printf 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' >&2
2819
+ done
2820
+ "#,
2821
+ )
2822
+ .expect("write script");
2823
+
2824
+ unsafe {
2825
+ env::set_var(CODEX_OUTPUT_LIMIT_BYTES_ENV, "4096");
2826
+ }
2827
+ let started = Instant::now();
2828
+ let result = run_command_with_timeout(Command::new(&script), Duration::from_secs(10))
2829
+ .expect("run with large stderr");
2830
+ unsafe {
2831
+ env::remove_var(CODEX_OUTPUT_LIMIT_BYTES_ENV);
2832
+ }
2833
+
2834
+ let TimedCommandOutput::OutputLimitExceeded {
2835
+ stream,
2836
+ output_limit,
2837
+ ..
2838
+ } = result
2839
+ else {
2840
+ panic!("expected stderr output limit failure");
2841
+ };
2842
+ assert_eq!(stream, "stderr");
2843
+ assert_eq!(output_limit, 4096);
2844
+ assert!(started.elapsed() < Duration::from_secs(3));
2845
+ }
2846
+
2479
2847
  #[cfg(unix)]
2480
2848
  #[test]
2481
2849
  fn run_command_with_timeout_closes_inherited_stdio_after_parent_exit() {
@@ -2510,6 +2878,44 @@ exit 0
2510
2878
  assert_eq!(String::from_utf8_lossy(&output.stderr), "parent stderr\n");
2511
2879
  }
2512
2880
 
2881
+ #[cfg(unix)]
2882
+ #[test]
2883
+ fn run_command_with_timeout_sweeps_detached_grandchildren_after_parent_exit() {
2884
+ let _env_guard = env_lock();
2885
+ let _process_guard = process_tree_lock();
2886
+ let root = temp_allowlist_dir().expect("temp root");
2887
+ let term_file = root.path.join("orphan.term");
2888
+ let ready_file = root.path.join("orphan.ready");
2889
+ let script = root.path.join("spawn-detached-grandchild.sh");
2890
+ write_executable(
2891
+ &script,
2892
+ &format!(
2893
+ r#"#!/bin/sh
2894
+ (trap 'printf term > {}; exit 0' TERM; printf ready > {}; sleep 30) >/dev/null 2>&1 &
2895
+ while [ ! -f {} ]; do
2896
+ sleep 0.01
2897
+ done
2898
+ printf 'parent done\n'
2899
+ exit 0
2900
+ "#,
2901
+ shell_quote(&term_file.display().to_string()),
2902
+ shell_quote(&ready_file.display().to_string()),
2903
+ shell_quote(&ready_file.display().to_string()),
2904
+ ),
2905
+ )
2906
+ .expect("write script");
2907
+
2908
+ let result = run_command_with_timeout(Command::new(&script), Duration::from_secs(10))
2909
+ .expect("run with detached grandchild");
2910
+
2911
+ let TimedCommandOutput::Completed(output) = result else {
2912
+ panic!("expected parent completion");
2913
+ };
2914
+ assert!(output.status.success());
2915
+ assert_eq!(String::from_utf8_lossy(&output.stdout), "parent done\n");
2916
+ assert_eq!(read_to_string(&term_file).unwrap_or_default(), "term");
2917
+ }
2918
+
2513
2919
  fn fallback_test_event() -> FallbackEvent {
2514
2920
  FallbackEvent {
2515
2921
  from_model: "spark-model".to_string(),
@@ -88,6 +88,31 @@ describe("agents/native-config", () => {
88
88
  const tripleQuoteBlocks = toml.match(/"""/g) || [];
89
89
  assert.equal(tripleQuoteBlocks.length, 2, "only TOML delimiters should remain as raw triple quotes");
90
90
  });
91
+ it("applies per-agent reasoning overrides when generating native TOML", async () => {
92
+ const codexHome = await mkdtemp(join(tmpdir(), "omx-native-config-reasoning-"));
93
+ try {
94
+ await writeFile(join(codexHome, ".omx-config.json"), JSON.stringify({
95
+ agentReasoning: {
96
+ architect: "xhigh",
97
+ },
98
+ }));
99
+ const agent = {
100
+ name: "architect",
101
+ description: "System design",
102
+ reasoningEffort: "high",
103
+ posture: "frontier-orchestrator",
104
+ modelClass: "frontier",
105
+ routingRole: "leader",
106
+ tools: "read-only",
107
+ category: "build",
108
+ };
109
+ const toml = generateAgentToml(agent, "Architect prompt", { codexHomeOverride: codexHome });
110
+ assert.match(toml, /model_reasoning_effort = "xhigh"/);
111
+ }
112
+ finally {
113
+ await rm(codexHome, { recursive: true, force: true });
114
+ }
115
+ });
91
116
  it("applies exact-model mini guidance only for resolved gpt-5.4-mini standard roles", () => {
92
117
  const agent = {
93
118
  name: "debugger",
@@ -145,6 +170,31 @@ describe("agents/native-config", () => {
145
170
  await rm(root, { recursive: true, force: true });
146
171
  }
147
172
  });
173
+ it("installs native agent TOML with configured per-agent reasoning overrides", async () => {
174
+ const root = await mkdtemp(join(tmpdir(), "omx-native-config-install-reasoning-"));
175
+ const codexHome = join(root, ".codex");
176
+ const promptsDir = join(root, "prompts");
177
+ const outDir = join(codexHome, "agents");
178
+ try {
179
+ await mkdir(promptsDir, { recursive: true });
180
+ await mkdir(codexHome, { recursive: true });
181
+ await writeFile(join(codexHome, ".omx-config.json"), JSON.stringify({
182
+ agentReasoning: {
183
+ architect: "xhigh",
184
+ },
185
+ }));
186
+ await writeFile(join(promptsDir, "architect.md"), "architect prompt");
187
+ await installNativeAgentConfigs(root, {
188
+ agentsDir: outDir,
189
+ catalogManifest: manifestWithAgents(["architect"]),
190
+ });
191
+ const architectToml = await readFile(join(outDir, "architect.toml"), "utf8");
192
+ assert.match(architectToml, /model_reasoning_effort = "xhigh"/);
193
+ }
194
+ finally {
195
+ await rm(root, { recursive: true, force: true });
196
+ }
197
+ });
148
198
  it("preserves active provider on native agents so websocket-capable Responses providers are inherited", async () => {
149
199
  const root = await mkdtemp(join(tmpdir(), "omx-native-config-provider-"));
150
200
  const codexHome = join(root, ".codex");