@pushpalsdev/cli 1.1.40 → 1.1.42

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushpalsdev/cli",
3
- "version": "1.1.40",
3
+ "version": "1.1.42",
4
4
  "description": "PushPals terminal CLI for LocalBuddy -> RemoteBuddy orchestration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -119,6 +119,7 @@ _DEFAULT_NO_EDIT_RECHECK_S = 120
119
119
  _NO_EDIT_RECOVERY_RECHECK_S = 30
120
120
  _DEFAULT_NO_EDIT_COMMAND_GRACE_S = 240
121
121
  _DEFAULT_NO_EDIT_COMMAND_PROGRESS_CAP_S = 360
122
+ _BACKGROUND_NO_EDIT_COMMAND_PROGRESS_CAP_S = 120
122
123
  _NO_EDIT_RECOVERY_COMMAND_PROGRESS_CAP_S = 120
123
124
  _DEFAULT_STARTUP_STALL_WATCHDOG_S = 210
124
125
  _RECOVERY_STARTUP_STALL_WATCHDOG_S = 150
@@ -817,6 +818,7 @@ def _resolve_no_edit_command_progress_cap_seconds(
817
818
  communicate_timeout_s: Optional[int],
818
819
  no_edit_command_grace_s: Optional[int],
819
820
  recovery_attempt: int = 0,
821
+ prompt: str = "",
820
822
  ) -> Optional[int]:
821
823
  if not communicate_timeout_s or no_edit_command_grace_s is None:
822
824
  return None
@@ -834,11 +836,12 @@ def _resolve_no_edit_command_progress_cap_seconds(
834
836
  else:
835
837
  return max(1, min(parsed, max(1, communicate_timeout_s - 1)))
836
838
 
837
- default_s = (
838
- _NO_EDIT_RECOVERY_COMMAND_PROGRESS_CAP_S
839
- if recovery_attempt > 0
840
- else _DEFAULT_NO_EDIT_COMMAND_PROGRESS_CAP_S
841
- )
839
+ if recovery_attempt > 0:
840
+ default_s = _NO_EDIT_RECOVERY_COMMAND_PROGRESS_CAP_S
841
+ elif _looks_like_background_autonomy_prompt(prompt):
842
+ default_s = _BACKGROUND_NO_EDIT_COMMAND_PROGRESS_CAP_S
843
+ else:
844
+ default_s = _DEFAULT_NO_EDIT_COMMAND_PROGRESS_CAP_S
842
845
  upper = max(1, communicate_timeout_s - 1)
843
846
  return max(1, min(default_s, upper))
844
847
 
@@ -2713,10 +2716,11 @@ def _run_codex_task(
2713
2716
  communicate_timeout_s,
2714
2717
  no_edit_command_grace_s,
2715
2718
  recovery_attempt=recovery_depth,
2719
+ prompt=prompt,
2716
2720
  )
2717
2721
  startup_stall_watchdog_s = _resolve_startup_stall_watchdog_seconds(
2718
2722
  communicate_timeout_s,
2719
- recovery_attempt=startup_stall_recovery_attempt,
2723
+ recovery_attempt=recovery_depth,
2720
2724
  )
2721
2725
  startup_stall_deadline = (
2722
2726
  started_at + float(startup_stall_watchdog_s)
@@ -2849,6 +2853,11 @@ def _run_codex_task(
2849
2853
  first_no_edit_command_progress_at
2850
2854
  + float(no_edit_command_progress_cap_s),
2851
2855
  )
2856
+ if deadline is not None and command_grace_deadline > 0:
2857
+ command_grace_deadline = min(
2858
+ command_grace_deadline,
2859
+ max(now, deadline - 1.0),
2860
+ )
2852
2861
  if command_progress_cap_reached:
2853
2862
  log.info(
2854
2863
  "No-edit watchdog observed Codex tool progress for "
@@ -45,6 +45,7 @@ from openai_codex_executor import (
45
45
  _has_credible_shell_wrapper_progress,
46
46
  _load_prompt_template,
47
47
  _mask_repo_local_codex_files,
48
+ _minimum_recovery_attempt_seconds,
48
49
  _repo_root_for_prompt_loading,
49
50
  _restore_repo_local_codex_files,
50
51
  _resolve_codex_command_prefix,
@@ -249,6 +250,41 @@ class OpenAICodexRuntimeConfigTests(unittest.TestCase):
249
250
  )
250
251
  self.assertEqual(_resolve_rollout_watchdog_seconds(prompt, 1200, no_edit), 90)
251
252
 
253
+ def test_background_autonomy_caps_patchless_command_progress_before_recovery_reserve(self) -> None:
254
+ prompt = (
255
+ "Task planning contract from PushPals:\n"
256
+ "- Planning summary: intent=code_change, risk=low, priority=background\n"
257
+ "- Origin=autonomy targetPaths=[app/__tests__/opportunity-graph.contract.test.ts]\n"
258
+ "Add focused contract coverage without broad discovery.\n"
259
+ )
260
+ child_budget_s = 570
261
+
262
+ with mock.patch.dict(
263
+ os.environ,
264
+ {
265
+ "WORKERPALS_OPENAI_CODEX_NO_EDIT_COMMAND_GRACE_S": "",
266
+ "WORKERPALS_OPENAI_CODEX_NO_EDIT_COMMAND_PROGRESS_CAP_S": "",
267
+ "WORKERPALS_OPENAI_CODEX_STARTUP_STALL_WATCHDOG_S": "",
268
+ },
269
+ clear=False,
270
+ ):
271
+ command_grace_s = _resolve_no_edit_command_grace_seconds(child_budget_s)
272
+ command_cap_s = _resolve_no_edit_command_progress_cap_seconds(
273
+ child_budget_s,
274
+ command_grace_s,
275
+ prompt=prompt,
276
+ )
277
+ startup_stall_s = _resolve_startup_stall_watchdog_seconds(child_budget_s)
278
+
279
+ self.assertEqual(command_grace_s, 240)
280
+ self.assertEqual(command_cap_s, 120)
281
+ self.assertEqual(startup_stall_s, 210)
282
+ first_attempt_patchless_ceiling_s = startup_stall_s + command_cap_s
283
+ self.assertGreaterEqual(
284
+ child_budget_s - first_attempt_patchless_ceiling_s,
285
+ 2 * _minimum_recovery_attempt_seconds(child_budget_s),
286
+ )
287
+
252
288
  def test_runtime_config_prefers_explicit_config_dir_override(self) -> None:
253
289
  import executor_base
254
290
 
@@ -1758,6 +1794,86 @@ class OpenAICodexRuntimeConfigTests(unittest.TestCase):
1758
1794
  self.assertIn("no publishable changes", str(result.get("summary") or ""))
1759
1795
  self.assertEqual(result.get("cooldownMs"), 600000)
1760
1796
 
1797
+ def test_run_codex_task_recovery_command_grace_stops_before_child_timeout(self) -> None:
1798
+ with tempfile.TemporaryDirectory(prefix="pushpals-codex-no-edit-before-timeout-") as temp_dir:
1799
+ repo = Path(temp_dir) / "repo"
1800
+ repo.mkdir(parents=True, exist_ok=True)
1801
+ (repo / "README.md").write_text("# no edit before timeout repo\n", encoding="utf-8")
1802
+ subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True, text=True)
1803
+ subprocess.run(
1804
+ ["git", "config", "user.name", "PushPals Test"],
1805
+ cwd=repo,
1806
+ check=True,
1807
+ capture_output=True,
1808
+ text=True,
1809
+ )
1810
+ subprocess.run(
1811
+ ["git", "config", "user.email", "pushpals-tests@example.com"],
1812
+ cwd=repo,
1813
+ check=True,
1814
+ capture_output=True,
1815
+ text=True,
1816
+ )
1817
+ subprocess.run(["git", "add", "README.md"], cwd=repo, check=True, capture_output=True, text=True)
1818
+ subprocess.run(
1819
+ ["git", "commit", "-m", "chore: seed no-edit before timeout repo"],
1820
+ cwd=repo,
1821
+ check=True,
1822
+ capture_output=True,
1823
+ text=True,
1824
+ )
1825
+
1826
+ stub_path = Path(temp_dir) / "fake_codex_no_edit_before_timeout.py"
1827
+ stub_path.write_text(
1828
+ "\n".join(
1829
+ [
1830
+ "import json",
1831
+ "import sys",
1832
+ "import time",
1833
+ "",
1834
+ "prompt = sys.stdin.read()",
1835
+ "print(json.dumps({'type': 'thread.started'}), flush=True)",
1836
+ "print(json.dumps({'type': 'turn.started'}), flush=True)",
1837
+ "if 'No-edit watchdog recovery' in prompt:",
1838
+ " print(json.dumps({'type': 'item.started', 'item': {'id': 'cmd-read', 'type': 'command_execution', 'command': 'cat README.md', 'status': 'in_progress'}}), flush=True)",
1839
+ " time.sleep(0.1)",
1840
+ " print(json.dumps({'type': 'item.completed', 'item': {'id': 'cmd-read', 'type': 'command_execution', 'command': 'cat README.md', 'status': 'completed', 'exit_code': 0, 'aggregated_output': '# no edit before timeout repo'}}), flush=True)",
1841
+ " time.sleep(6)",
1842
+ " raise SystemExit(0)",
1843
+ "",
1844
+ "print(json.dumps({'type': 'item.completed', 'item': {'type': 'message', 'text': 'Still inspecting without a patch.'}}), flush=True)",
1845
+ "time.sleep(6)",
1846
+ ]
1847
+ ),
1848
+ encoding="utf-8",
1849
+ )
1850
+
1851
+ env_overrides = {
1852
+ "PUSHPALS_OPENAI_CODEX_BIN_JSON": json.dumps([sys.executable, str(stub_path)]),
1853
+ "PUSHPALS_OPENAI_CODEX_AUTH_MODE": "api_key",
1854
+ "OPENAI_API_KEY": "pushpals-no-edit-before-timeout-test-key",
1855
+ "WORKERPALS_OPENAI_CODEX_JSON": "true",
1856
+ "WORKERPALS_OPENAI_CODEX_TIMEOUT_S": "6",
1857
+ "WORKERPALS_OPENAI_CODEX_NO_EDIT_WATCHDOG_S": "1",
1858
+ "WORKERPALS_OPENAI_CODEX_NO_EDIT_COMMAND_GRACE_S": "5",
1859
+ "WORKERPALS_OPENAI_CODEX_PROGRESS_LOG_INTERVAL_S": "1",
1860
+ }
1861
+ with mock.patch.dict(os.environ, env_overrides, clear=False):
1862
+ result = _run_codex_task(
1863
+ str(repo),
1864
+ "Make one focused test edit after the hinted file read.",
1865
+ [],
1866
+ )
1867
+
1868
+ self.assertFalse(result.get("ok"), result)
1869
+ self.assertEqual(result.get("exitCode"), 124)
1870
+ self.assertEqual(
1871
+ result.get("summary"),
1872
+ "openai_codex made no publishable changes before the no-edit watchdog",
1873
+ )
1874
+ self.assertNotIn("execution timed out", str(result.get("summary") or ""))
1875
+ self.assertEqual(result.get("cooldownMs"), 600000)
1876
+
1761
1877
  def test_run_codex_task_no_edit_watchdog_rechecks_transient_publishable_progress(self) -> None:
1762
1878
  with tempfile.TemporaryDirectory(prefix="pushpals-codex-no-edit-recheck-") as temp_dir:
1763
1879
  repo = Path(temp_dir) / "repo"