@pushpalsdev/cli 1.1.43 → 1.1.44

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.43",
3
+ "version": "1.1.44",
4
4
  "description": "PushPals terminal CLI for LocalBuddy -> RemoteBuddy orchestration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -107,6 +107,7 @@ _MAX_WRAPPER_BOOTSTRAP_TOTAL_CHARS = 5_000
107
107
  _MAX_CREDIBLE_WRAPPER_LOOP_CHANGED_PATHS = 8
108
108
  _MAX_CREDIBLE_WRAPPER_LOOP_TOP_LEVELS = 4
109
109
  _MAX_STARTUP_STALL_RECOVERY_ATTEMPTS = 1
110
+ _MAX_STARTUP_STALL_DURING_NO_EDIT_RECOVERY_ATTEMPTS = 2
110
111
  _MAX_NO_EDIT_RECOVERY_ATTEMPTS = 1
111
112
  _MAX_ROLLOUT_RECOVERY_ATTEMPTS = 1
112
113
  _DEFAULT_NO_EDIT_WATCHDOG_S = 480
@@ -3137,21 +3138,36 @@ def _run_codex_task(
3137
3138
 
3138
3139
  if no_edit_watchdog_fired:
3139
3140
  startup_stall = _codex_trace_is_startup_stall(stdout_trace)
3140
- if startup_stall and startup_stall_recovery_attempt < _MAX_STARTUP_STALL_RECOVERY_ATTEMPTS:
3141
+ startup_stall_recovery_limit = _MAX_STARTUP_STALL_RECOVERY_ATTEMPTS
3142
+ if no_edit_recovery_attempt > 0:
3143
+ startup_stall_recovery_limit = max(
3144
+ startup_stall_recovery_limit,
3145
+ _MAX_STARTUP_STALL_DURING_NO_EDIT_RECOVERY_ATTEMPTS,
3146
+ )
3147
+ if startup_stall and startup_stall_recovery_attempt < startup_stall_recovery_limit:
3141
3148
  retry_guidance = [
3142
3149
  *supplemental_guidance,
3143
3150
  _build_startup_stall_recovery_guidance(trace_excerpt),
3144
3151
  ]
3145
- recovery_model = _startup_stall_recovery_model(model)
3152
+ prefer_same_model = (
3153
+ no_edit_recovery_attempt > 0
3154
+ and startup_stall_recovery_attempt < _MAX_STARTUP_STALL_RECOVERY_ATTEMPTS
3155
+ )
3156
+ recovery_model = model if prefer_same_model else _startup_stall_recovery_model(model)
3146
3157
  recovery_detail = (
3147
- f" using fallback model {recovery_model!r}"
3148
- if recovery_model and recovery_model != model
3149
- else ""
3158
+ f" using same model {recovery_model!r} because an earlier attempt made tool progress"
3159
+ if prefer_same_model
3160
+ else (
3161
+ f" using fallback model {recovery_model!r}"
3162
+ if recovery_model and recovery_model != model
3163
+ else ""
3164
+ )
3150
3165
  )
3151
3166
  log.warning(
3152
3167
  "Codex emitted only startup events before the no-edit watchdog; "
3153
- f"restarting Codex once{recovery_detail} before classifying the job terminally."
3168
+ f"restarting Codex{recovery_detail} before classifying the job terminally."
3154
3169
  )
3170
+ retry_model_override = model_override if prefer_same_model else recovery_model or model_override
3155
3171
  retry_result = _run_codex_task(
3156
3172
  repo,
3157
3173
  instruction,
@@ -3161,7 +3177,7 @@ def _run_codex_task(
3161
3177
  startup_stall_recovery_attempt=startup_stall_recovery_attempt + 1,
3162
3178
  no_edit_recovery_attempt=no_edit_recovery_attempt,
3163
3179
  rollout_recovery_attempt=rollout_recovery_attempt,
3164
- model_override=recovery_model or model_override,
3180
+ model_override=retry_model_override,
3165
3181
  baseline_changes=baseline_snapshot,
3166
3182
  execution_deadline_monotonic=overall_deadline,
3167
3183
  )
@@ -1307,6 +1307,101 @@ class OpenAICodexRuntimeConfigTests(unittest.TestCase):
1307
1307
  self.assertNotIn("no publishable", str(result.get("summary") or "").lower())
1308
1308
  self.assertEqual(result.get("cooldownMs"), 600000)
1309
1309
 
1310
+ def test_run_codex_task_no_edit_recovery_retries_same_model_after_startup_stall(self) -> None:
1311
+ with tempfile.TemporaryDirectory(prefix="pushpals-codex-no-edit-startup-stall-") as temp_dir:
1312
+ repo = Path(temp_dir) / "repo"
1313
+ repo.mkdir(parents=True, exist_ok=True)
1314
+ (repo / "README.md").write_text("# no edit startup stall repo\n", encoding="utf-8")
1315
+ subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True, text=True)
1316
+ subprocess.run(
1317
+ ["git", "config", "user.name", "PushPals Test"],
1318
+ cwd=repo,
1319
+ check=True,
1320
+ capture_output=True,
1321
+ text=True,
1322
+ )
1323
+ subprocess.run(
1324
+ ["git", "config", "user.email", "pushpals-tests@example.com"],
1325
+ cwd=repo,
1326
+ check=True,
1327
+ capture_output=True,
1328
+ text=True,
1329
+ )
1330
+ subprocess.run(["git", "add", "README.md"], cwd=repo, check=True, capture_output=True, text=True)
1331
+ subprocess.run(
1332
+ ["git", "commit", "-m", "chore: seed no-edit startup stall repo"],
1333
+ cwd=repo,
1334
+ check=True,
1335
+ capture_output=True,
1336
+ text=True,
1337
+ )
1338
+
1339
+ stub_path = Path(temp_dir) / "fake_codex_no_edit_startup_stall.py"
1340
+ stub_path.write_text(
1341
+ "\n".join(
1342
+ [
1343
+ "from pathlib import Path",
1344
+ "import json",
1345
+ "import sys",
1346
+ "import time",
1347
+ "",
1348
+ "argv = sys.argv[1:]",
1349
+ "last_message_path = None",
1350
+ "model = ''",
1351
+ "for index, arg in enumerate(argv):",
1352
+ " if arg == '--output-last-message' and index + 1 < len(argv):",
1353
+ " last_message_path = argv[index + 1]",
1354
+ " if arg == '-m' and index + 1 < len(argv):",
1355
+ " model = argv[index + 1]",
1356
+ "",
1357
+ "prompt = sys.stdin.read()",
1358
+ "has_no_edit_recovery = 'No-edit watchdog recovery' in prompt",
1359
+ "has_startup_recovery = 'Codex startup-stall recovery' in prompt",
1360
+ "if has_no_edit_recovery and has_startup_recovery and model != 'gpt-5.4':",
1361
+ " Path('src').mkdir(exist_ok=True)",
1362
+ " Path('src/no-edit-startup-stall-recovered.txt').write_text('patched after same-model restart\\n', encoding='utf-8')",
1363
+ " if last_message_path:",
1364
+ " Path(last_message_path).write_text('Patched after same-model startup-stall recovery.', encoding='utf-8')",
1365
+ " print(json.dumps({'type': 'item.completed', 'item': {'type': 'message', 'text': 'Patched after same-model startup-stall recovery.'}}), flush=True)",
1366
+ " raise SystemExit(0)",
1367
+ "",
1368
+ "print(json.dumps({'type': 'thread.started'}), flush=True)",
1369
+ "print(json.dumps({'type': 'turn.started'}), flush=True)",
1370
+ "if not has_no_edit_recovery:",
1371
+ " print(json.dumps({'type': 'item.started', 'item': {'id': 'cmd-read', 'type': 'command_execution', 'command': 'cat README.md', 'status': 'in_progress'}}), flush=True)",
1372
+ " time.sleep(0.2)",
1373
+ " print(json.dumps({'type': 'item.completed', 'item': {'id': 'cmd-read', 'type': 'command_execution', 'command': 'cat README.md', 'status': 'completed', 'exit_code': 0}}), flush=True)",
1374
+ "time.sleep(10)",
1375
+ ]
1376
+ ),
1377
+ encoding="utf-8",
1378
+ )
1379
+
1380
+ env_overrides = {
1381
+ "PUSHPALS_OPENAI_CODEX_BIN_JSON": json.dumps([sys.executable, str(stub_path)]),
1382
+ "PUSHPALS_OPENAI_CODEX_AUTH_MODE": "api_key",
1383
+ "OPENAI_API_KEY": "pushpals-no-edit-startup-stall-test-key",
1384
+ "WORKERPALS_OPENAI_CODEX_JSON": "true",
1385
+ "WORKERPALS_OPENAI_CODEX_TIMEOUT_S": "30",
1386
+ "WORKERPALS_OPENAI_CODEX_NO_EDIT_WATCHDOG_S": "1",
1387
+ "WORKERPALS_OPENAI_CODEX_NO_EDIT_COMMAND_GRACE_S": "1",
1388
+ "WORKERPALS_OPENAI_CODEX_NO_EDIT_COMMAND_PROGRESS_CAP_S": "1",
1389
+ "WORKERPALS_OPENAI_CODEX_STARTUP_STALL_WATCHDOG_S": "1",
1390
+ "WORKERPALS_OPENAI_CODEX_PROGRESS_LOG_INTERVAL_S": "1",
1391
+ }
1392
+ with mock.patch.dict(os.environ, env_overrides, clear=False):
1393
+ result = _run_codex_task(
1394
+ str(repo),
1395
+ "Add one focused regression assertion after reading the hinted test.",
1396
+ [],
1397
+ )
1398
+
1399
+ self.assertTrue(result.get("ok"), result)
1400
+ self.assertEqual(result.get("exitCode"), 0)
1401
+ stdout = str(result.get("stdout") or "")
1402
+ self.assertIn("same-model startup-stall recovery", stdout)
1403
+ self.assertIn("src/", stdout)
1404
+
1310
1405
  def test_run_codex_task_retries_once_when_no_edit_watchdog_fires(self) -> None:
1311
1406
  with tempfile.TemporaryDirectory(prefix="pushpals-codex-no-edit-watchdog-") as temp_dir:
1312
1407
  repo = Path(temp_dir) / "repo"