@pushpalsdev/cli 1.1.32 → 1.1.35

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.32",
3
+ "version": "1.1.35",
4
4
  "description": "PushPals terminal CLI for LocalBuddy -> RemoteBuddy orchestration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -116,6 +116,8 @@ _WEB_REVIEW_NO_EDIT_WATCHDOG_S = 240
116
116
  _BACKGROUND_NO_EDIT_WATCHDOG_S = 120
117
117
  _NO_EDIT_RECOVERY_WATCHDOG_S = 90
118
118
  _DEFAULT_NO_EDIT_RECHECK_S = 120
119
+ _DEFAULT_STARTUP_STALL_WATCHDOG_S = 210
120
+ _RECOVERY_STARTUP_STALL_WATCHDOG_S = 150
119
121
  _DEFAULT_ROLLOUT_WATCHDOG_S = 300
120
122
  _SMALL_TASK_ROLLOUT_WATCHDOG_S = 240
121
123
  _NARROW_TEST_TASK_ROLLOUT_WATCHDOG_S = 150
@@ -755,6 +757,44 @@ def _resolve_no_edit_recheck_seconds(communicate_timeout_s: Optional[int]) -> in
755
757
  return max(1, min(_DEFAULT_NO_EDIT_RECHECK_S, upper))
756
758
 
757
759
 
760
+ def _resolve_startup_stall_watchdog_seconds(
761
+ communicate_timeout_s: Optional[int],
762
+ recovery_attempt: int = 0,
763
+ ) -> Optional[int]:
764
+ if not communicate_timeout_s:
765
+ return None
766
+
767
+ raw = os.environ.get("WORKERPALS_OPENAI_CODEX_STARTUP_STALL_WATCHDOG_S", "").strip()
768
+ if raw:
769
+ if raw == "0":
770
+ return None
771
+ parsed = _to_positive_int(raw)
772
+ if parsed is None:
773
+ log.info(
774
+ "Invalid WORKERPALS_OPENAI_CODEX_STARTUP_STALL_WATCHDOG_S="
775
+ f"{raw!r}; using default startup-stall watchdog."
776
+ )
777
+ else:
778
+ return max(1, min(parsed, max(1, communicate_timeout_s - 1)))
779
+
780
+ default_s = (
781
+ _RECOVERY_STARTUP_STALL_WATCHDOG_S
782
+ if recovery_attempt > 0
783
+ else _DEFAULT_STARTUP_STALL_WATCHDOG_S
784
+ )
785
+ floor_s = 60
786
+ return max(floor_s, min(default_s, max(floor_s, communicate_timeout_s - 1)))
787
+
788
+
789
+ def _startup_stall_recovery_model(current_model: str) -> str:
790
+ normalized = str(current_model or "").strip()
791
+ if not normalized:
792
+ return LEGACY_CODEX_MODEL_FALLBACK
793
+ if normalized.lower() == LEGACY_CODEX_MODEL_FALLBACK.lower():
794
+ return normalized
795
+ return LEGACY_CODEX_MODEL_FALLBACK
796
+
797
+
758
798
  def _looks_like_web_review_prompt(prompt: str) -> bool:
759
799
  text = str(prompt or "").lower()
760
800
  return "repo-native web review" in text or "web review path" in text
@@ -2337,6 +2377,15 @@ def _run_codex_task(
2337
2377
  else None
2338
2378
  )
2339
2379
  no_edit_recheck_s = _resolve_no_edit_recheck_seconds(communicate_timeout_s)
2380
+ startup_stall_watchdog_s = _resolve_startup_stall_watchdog_seconds(
2381
+ communicate_timeout_s,
2382
+ recovery_attempt=startup_stall_recovery_attempt,
2383
+ )
2384
+ startup_stall_deadline = (
2385
+ started_at + float(startup_stall_watchdog_s)
2386
+ if startup_stall_watchdog_s is not None
2387
+ else None
2388
+ )
2340
2389
  rollout_watchdog_s = (
2341
2390
  _resolve_rollout_watchdog_seconds(
2342
2391
  prompt,
@@ -2364,9 +2413,50 @@ def _run_codex_task(
2364
2413
  _terminate_active_child()
2365
2414
  break
2366
2415
 
2416
+ if startup_stall_deadline is not None and now >= startup_stall_deadline:
2417
+ with trace_lock:
2418
+ live_trace = dict(stdout_trace_state)
2419
+ summaries = stdout_trace_state.get("summaries")
2420
+ if isinstance(summaries, list):
2421
+ live_trace["summaries"] = list(summaries)
2422
+ if _codex_trace_is_startup_stall(live_trace):
2423
+ changed_paths, _, effective_paths = _codex_changed_paths(repo, baseline_snapshot)
2424
+ if not effective_paths:
2425
+ no_edit_artifact_only_paths = _describe_non_publishable_paths(
2426
+ changed_paths,
2427
+ baseline_snapshot,
2428
+ )
2429
+ no_edit_watchdog_fired = True
2430
+ elapsed_s = int(max(0.0, now - started_at))
2431
+ log.info(
2432
+ f"Startup-stall watchdog fired after {elapsed_s}s with no assistant/tool progress."
2433
+ )
2434
+ _terminate_active_child()
2435
+ break
2436
+ startup_stall_deadline = None
2437
+
2367
2438
  if no_edit_deadline is not None and now >= no_edit_deadline:
2368
2439
  changed_paths, _, effective_paths = _codex_changed_paths(repo, baseline_snapshot)
2369
2440
  if not effective_paths:
2441
+ with trace_lock:
2442
+ live_trace = dict(stdout_trace_state)
2443
+ summaries = stdout_trace_state.get("summaries")
2444
+ if isinstance(summaries, list):
2445
+ live_trace["summaries"] = list(summaries)
2446
+ startup_only = _codex_trace_is_startup_stall(live_trace)
2447
+ if (
2448
+ startup_only
2449
+ and startup_stall_deadline is not None
2450
+ and now < startup_stall_deadline
2451
+ ):
2452
+ no_edit_deadline = startup_stall_deadline
2453
+ remaining_s = int(max(1.0, startup_stall_deadline - now))
2454
+ log.info(
2455
+ "No-edit watchdog observed only Codex startup events; "
2456
+ f"allowing {remaining_s}s for first assistant/tool progress "
2457
+ "before startup-stall recovery."
2458
+ )
2459
+ continue
2370
2460
  no_edit_artifact_only_paths = _describe_non_publishable_paths(
2371
2461
  changed_paths,
2372
2462
  baseline_snapshot,
@@ -2377,9 +2467,15 @@ def _run_codex_task(
2377
2467
  if no_edit_artifact_only_paths
2378
2468
  else ""
2379
2469
  )
2380
- log.info(
2381
- f"No-edit watchdog fired after {int(no_edit_watchdog_s or 0)}s with no publishable file changes.{artifact_detail} Retrying with patch-first guidance."
2382
- )
2470
+ if startup_only:
2471
+ elapsed_s = int(max(0.0, now - started_at))
2472
+ log.info(
2473
+ f"Startup-stall watchdog fired after {elapsed_s}s with no assistant/tool progress."
2474
+ )
2475
+ else:
2476
+ log.info(
2477
+ f"No-edit watchdog fired after {int(no_edit_watchdog_s or 0)}s with no publishable file changes.{artifact_detail} Retrying with patch-first guidance."
2478
+ )
2383
2479
  _terminate_active_child()
2384
2480
  break
2385
2481
  no_edit_deadline = now + float(no_edit_recheck_s)
@@ -2550,9 +2646,15 @@ def _run_codex_task(
2550
2646
  *supplemental_guidance,
2551
2647
  _build_startup_stall_recovery_guidance(trace_excerpt),
2552
2648
  ]
2649
+ recovery_model = _startup_stall_recovery_model(model)
2650
+ recovery_detail = (
2651
+ f" using fallback model {recovery_model!r}"
2652
+ if recovery_model and recovery_model != model
2653
+ else ""
2654
+ )
2553
2655
  log.warning(
2554
2656
  "Codex emitted only startup events before the no-edit watchdog; "
2555
- "restarting Codex once before classifying the job terminally."
2657
+ f"restarting Codex once{recovery_detail} before classifying the job terminally."
2556
2658
  )
2557
2659
  retry_result = _run_codex_task(
2558
2660
  repo,
@@ -2563,7 +2665,7 @@ def _run_codex_task(
2563
2665
  startup_stall_recovery_attempt=startup_stall_recovery_attempt + 1,
2564
2666
  no_edit_recovery_attempt=no_edit_recovery_attempt,
2565
2667
  rollout_recovery_attempt=rollout_recovery_attempt,
2566
- model_override=model_override,
2668
+ model_override=recovery_model or model_override,
2567
2669
  baseline_changes=baseline_snapshot,
2568
2670
  )
2569
2671
  retry_result["usage"] = _merge_usage_records(usage, retry_result.get("usage"))
@@ -49,6 +49,7 @@ from openai_codex_executor import (
49
49
  _resolve_codex_command_prefix,
50
50
  _resolve_no_edit_watchdog_seconds,
51
51
  _resolve_rollout_watchdog_seconds,
52
+ _resolve_startup_stall_watchdog_seconds,
52
53
  _unwrap_shell_wrapper_command,
53
54
  _usage_from_trace_or_estimate,
54
55
  )
@@ -350,6 +351,85 @@ class OpenAICodexRuntimeConfigTests(unittest.TestCase):
350
351
  self.assertIn("Home shell startup is assertable", guidance)
351
352
  self.assertIn("bun run web:e2e", guidance)
352
353
 
354
+ def test_parse_payload_accepts_file_backed_payload_transport(self) -> None:
355
+ with tempfile.TemporaryDirectory(prefix="pushpals-payload-file-") as temp_dir:
356
+ repo = Path(temp_dir) / "repo"
357
+ repo.mkdir(parents=True, exist_ok=True)
358
+ payload = {
359
+ "kind": "task.execute",
360
+ "repo": str(repo),
361
+ "params": {"instruction": "Make one small publishable change"},
362
+ }
363
+ encoded = base64.b64encode(json.dumps(payload).encode("utf-8")).decode("ascii")
364
+ payload_file = Path(temp_dir) / "payload.b64"
365
+ payload_file.write_text(encoded, encoding="utf-8")
366
+
367
+ task = parse_task_execute_payload(
368
+ ["executor", "--payload-file", str(payload_file)],
369
+ logger=Logger("[test]"),
370
+ )
371
+
372
+ self.assertEqual(task.kind, "task.execute")
373
+ self.assertEqual(task.repo, str(repo.resolve()))
374
+ self.assertEqual(task.instruction, "Make one small publishable change")
375
+
376
+ def test_parse_payload_accepts_positional_payload_file_path(self) -> None:
377
+ with tempfile.TemporaryDirectory(prefix="pushpals-payload-file-positional-") as temp_dir:
378
+ repo = Path(temp_dir) / "repo"
379
+ repo.mkdir(parents=True, exist_ok=True)
380
+ payload = {
381
+ "kind": "task.execute",
382
+ "repo": str(repo),
383
+ "params": {"instruction": "Recover from a direct-worker payload handoff"},
384
+ }
385
+ encoded = base64.b64encode(json.dumps(payload).encode("utf-8")).decode("ascii")
386
+ payload_file = Path(temp_dir) / "payload.b64"
387
+ payload_file.write_text(encoded, encoding="utf-8")
388
+
389
+ task = parse_task_execute_payload(
390
+ ["executor", str(payload_file)],
391
+ logger=Logger("[test]"),
392
+ )
393
+
394
+ self.assertEqual(task.kind, "task.execute")
395
+ self.assertEqual(task.repo, str(repo.resolve()))
396
+ self.assertEqual(task.instruction, "Recover from a direct-worker payload handoff")
397
+
398
+ def test_parse_payload_accepts_unpadded_base64_payload(self) -> None:
399
+ with tempfile.TemporaryDirectory(prefix="pushpals-payload-unpadded-") as temp_dir:
400
+ repo = Path(temp_dir) / "repo"
401
+ repo.mkdir(parents=True, exist_ok=True)
402
+ payload = {
403
+ "kind": "task.execute",
404
+ "repo": str(repo),
405
+ "params": {"instruction": "Accept wrapper-normalized payload padding"},
406
+ }
407
+ encoded = base64.b64encode(json.dumps(payload).encode("utf-8")).decode("ascii")
408
+ unpadded = encoded.rstrip("=")
409
+
410
+ task = parse_task_execute_payload(["executor", unpadded], logger=Logger("[test]"))
411
+
412
+ self.assertEqual(task.kind, "task.execute")
413
+ self.assertEqual(task.repo, str(repo.resolve()))
414
+ self.assertEqual(task.instruction, "Accept wrapper-normalized payload padding")
415
+
416
+ def test_parse_payload_accepts_raw_json_payload(self) -> None:
417
+ with tempfile.TemporaryDirectory(prefix="pushpals-payload-raw-json-") as temp_dir:
418
+ repo = Path(temp_dir) / "repo"
419
+ repo.mkdir(parents=True, exist_ok=True)
420
+ payload = {
421
+ "kind": "task.execute",
422
+ "repo": str(repo),
423
+ "params": {"instruction": "Accept raw JSON from a recovery wrapper"},
424
+ }
425
+ raw_json = json.dumps(payload)
426
+
427
+ task = parse_task_execute_payload(["executor", raw_json], logger=Logger("[test]"))
428
+
429
+ self.assertEqual(task.kind, "task.execute")
430
+ self.assertEqual(task.repo, str(repo.resolve()))
431
+ self.assertEqual(task.instruction, "Accept raw JSON from a recovery wrapper")
432
+
353
433
  def test_parse_payload_prefers_helper_tests_for_visual_derivation_tasks(self) -> None:
354
434
  with tempfile.TemporaryDirectory(prefix="pushpals-visual-guidance-") as temp_dir:
355
435
  repo = Path(temp_dir) / "repo"
@@ -1069,13 +1149,16 @@ class OpenAICodexRuntimeConfigTests(unittest.TestCase):
1069
1149
  "",
1070
1150
  "argv = sys.argv[1:]",
1071
1151
  "last_message_path = None",
1152
+ "model = ''",
1072
1153
  "for index, arg in enumerate(argv):",
1073
1154
  " if arg == '--output-last-message' and index + 1 < len(argv):",
1074
1155
  " last_message_path = argv[index + 1]",
1156
+ " if arg == '-m' and index + 1 < len(argv):",
1157
+ " model = argv[index + 1]",
1075
1158
  " break",
1076
1159
  "",
1077
1160
  "prompt = sys.stdin.read()",
1078
- "if 'Codex startup-stall recovery' in prompt:",
1161
+ "if 'Codex startup-stall recovery' in prompt and model == 'gpt-5.4':",
1079
1162
  " Path('src').mkdir(exist_ok=True)",
1080
1163
  " Path('src/startup-stall-recovered.txt').write_text('patched after restart\\n', encoding='utf-8')",
1081
1164
  " if last_message_path:",
@@ -1097,7 +1180,8 @@ class OpenAICodexRuntimeConfigTests(unittest.TestCase):
1097
1180
  "OPENAI_API_KEY": "pushpals-startup-stall-test-key",
1098
1181
  "WORKERPALS_OPENAI_CODEX_JSON": "true",
1099
1182
  "WORKERPALS_OPENAI_CODEX_TIMEOUT_S": "20",
1100
- "WORKERPALS_OPENAI_CODEX_NO_EDIT_WATCHDOG_S": "1",
1183
+ "WORKERPALS_OPENAI_CODEX_NO_EDIT_WATCHDOG_S": "0",
1184
+ "WORKERPALS_OPENAI_CODEX_STARTUP_STALL_WATCHDOG_S": "1",
1101
1185
  "WORKERPALS_OPENAI_CODEX_PROGRESS_LOG_INTERVAL_S": "1",
1102
1186
  }
1103
1187
  with mock.patch.dict(os.environ, env_overrides, clear=False):
@@ -1167,6 +1251,7 @@ class OpenAICodexRuntimeConfigTests(unittest.TestCase):
1167
1251
  "WORKERPALS_OPENAI_CODEX_JSON": "true",
1168
1252
  "WORKERPALS_OPENAI_CODEX_TIMEOUT_S": "20",
1169
1253
  "WORKERPALS_OPENAI_CODEX_NO_EDIT_WATCHDOG_S": "1",
1254
+ "WORKERPALS_OPENAI_CODEX_STARTUP_STALL_WATCHDOG_S": "1",
1170
1255
  "WORKERPALS_OPENAI_CODEX_PROGRESS_LOG_INTERVAL_S": "1",
1171
1256
  }
1172
1257
  with mock.patch.dict(os.environ, env_overrides, clear=False):
@@ -1565,6 +1650,31 @@ class OpenAICodexRuntimeConfigTests(unittest.TestCase):
1565
1650
 
1566
1651
  self.assertEqual(watchdog_s, 180)
1567
1652
 
1653
+ def test_startup_stall_watchdog_allows_slower_first_response_than_no_edit_watchdog(self) -> None:
1654
+ with mock.patch.dict(
1655
+ os.environ,
1656
+ {"WORKERPALS_OPENAI_CODEX_STARTUP_STALL_WATCHDOG_S": ""},
1657
+ clear=False,
1658
+ ):
1659
+ watchdog_s = _resolve_startup_stall_watchdog_seconds(1200)
1660
+ recovery_watchdog_s = _resolve_startup_stall_watchdog_seconds(
1661
+ 1200,
1662
+ recovery_attempt=1,
1663
+ )
1664
+
1665
+ self.assertEqual(watchdog_s, 210)
1666
+ self.assertEqual(recovery_watchdog_s, 150)
1667
+
1668
+ def test_explicit_startup_stall_watchdog_override_is_bounded(self) -> None:
1669
+ with mock.patch.dict(
1670
+ os.environ,
1671
+ {"WORKERPALS_OPENAI_CODEX_STARTUP_STALL_WATCHDOG_S": "500"},
1672
+ clear=False,
1673
+ ):
1674
+ watchdog_s = _resolve_startup_stall_watchdog_seconds(120)
1675
+
1676
+ self.assertEqual(watchdog_s, 119)
1677
+
1568
1678
  def test_narrow_contract_regression_with_required_e2e_uses_fast_no_edit_watchdog(self) -> None:
1569
1679
  prompt = (
1570
1680
  "Harden the opportunity graph contract around autonomous delivery-loop failure signals. "
@@ -17,6 +17,10 @@ import {
17
17
  filterResultLines,
18
18
  } from "../common/execution_utils.js";
19
19
  import { buildWorkerSandboxWritableEnv } from "../common/sandbox_env.js";
20
+ import {
21
+ createPythonPayloadTransport,
22
+ type PythonPayloadTransport,
23
+ } from "../common/python_payload_transport.js";
20
24
  import { computeTimeoutWarningWindow } from "../timeout_policy.js";
21
25
 
22
26
  // ---- Script path (resolved relative to this file) ----------------------------
@@ -274,6 +278,7 @@ export async function executeWithOpenHands(
274
278
  let timeoutTimer: ReturnType<typeof setTimeout> | null = null;
275
279
  let stuckNudgeStartTimer: ReturnType<typeof setTimeout> | null = null;
276
280
  let stuckNudgeTimer: ReturnType<typeof setInterval> | null = null;
281
+ let payloadTransport: PythonPayloadTransport | null = null;
277
282
  const outputPolicy = {
278
283
  maxOutputChars: runtimeConfig.workerpals.outputMaxChars,
279
284
  maxOutputLines: runtimeConfig.workerpals.outputMaxLines,
@@ -282,7 +287,8 @@ export async function executeWithOpenHands(
282
287
  };
283
288
 
284
289
  try {
285
- const proc = Bun.spawn([pythonBin, scriptPath, payload], {
290
+ payloadTransport = createPythonPayloadTransport(payload);
291
+ const proc = Bun.spawn([pythonBin, scriptPath, ...payloadTransport.args], {
286
292
  cwd: repo,
287
293
  stdout: "pipe",
288
294
  stderr: "pipe",
@@ -623,5 +629,6 @@ export async function executeWithOpenHands(
623
629
  if (stuckNudgeTimer) {
624
630
  clearInterval(stuckNudgeTimer);
625
631
  }
632
+ payloadTransport?.cleanup();
626
633
  }
627
634
  }
@@ -155,14 +155,60 @@ def fail(summary: str, stderr: Optional[str] = None, exit_code: int = 1) -> int:
155
155
  return exit_code
156
156
 
157
157
 
158
- def decode_payload(raw: str) -> Dict[str, Any]:
159
- decoded = base64.b64decode(raw).decode("utf-8")
160
- payload = json.loads(decoded)
158
+ def _parse_payload_json(raw: str) -> Dict[str, Any]:
159
+ payload = json.loads(raw)
161
160
  if not isinstance(payload, dict):
162
161
  raise ValueError("payload must be a JSON object")
163
162
  return payload
164
163
 
165
164
 
165
+ def decode_payload(raw: str) -> Dict[str, Any]:
166
+ stripped = str(raw or "").strip()
167
+ if not stripped:
168
+ raise ValueError("empty job payload")
169
+
170
+ # Direct workers normally receive a file-backed base64 payload, but this
171
+ # parser intentionally accepts the safe adjacent encodings too. That keeps
172
+ # executor startup resilient if an outer wrapper normalizes padding, uses
173
+ # url-safe base64, or hands through raw JSON during recovery.
174
+ if stripped.startswith("{"):
175
+ return _parse_payload_json(stripped)
176
+
177
+ compact = "".join(stripped.split())
178
+ padded = compact + ("=" * ((4 - len(compact) % 4) % 4))
179
+ decode_errors: List[str] = []
180
+ for decoder in (base64.b64decode, base64.urlsafe_b64decode):
181
+ try:
182
+ decoded = decoder(padded).decode("utf-8")
183
+ return _parse_payload_json(decoded)
184
+ except Exception as exc:
185
+ decode_errors.append(str(exc))
186
+
187
+ detail = "; ".join(error for error in decode_errors if error) or "unknown decode error"
188
+ raise ValueError(f"invalid base64/JSON job payload: {detail}")
189
+
190
+
191
+ def read_encoded_payload_arg(argv: List[str]) -> str:
192
+ if len(argv) < 2:
193
+ raise ValueError("missing base64 job payload")
194
+ mode = argv[1]
195
+ if mode == "--payload-file":
196
+ if len(argv) < 3 or not str(argv[2] or "").strip():
197
+ raise ValueError("missing payload file path")
198
+ path = Path(str(argv[2])).expanduser()
199
+ return path.read_text(encoding="utf-8").strip()
200
+ if mode == "--payload-stdin":
201
+ return sys.stdin.read().strip()
202
+ if len(mode) < 4096:
203
+ try:
204
+ path = Path(mode).expanduser()
205
+ if path.is_file():
206
+ return path.read_text(encoding="utf-8").strip()
207
+ except OSError:
208
+ pass
209
+ return mode
210
+
211
+
166
212
  def resolve_repo_within_assigned_root(repo: str) -> Tuple[Optional[str], Optional[str]]:
167
213
  raw_repo = str(repo or "").strip()
168
214
  if not raw_repo:
@@ -968,11 +1014,8 @@ def parse_task_execute_payload(
968
1014
  don't need to handle them.
969
1015
  """
970
1016
  log = logger or Logger("[Executor]")
971
- if len(argv) < 2:
972
- raise SystemExit(fail("Missing base64 job payload", exit_code=2))
973
-
974
1017
  try:
975
- payload = decode_payload(argv[1])
1018
+ payload = decode_payload(read_encoded_payload_arg(argv))
976
1019
  except Exception as exc:
977
1020
  raise SystemExit(fail(f"Failed to decode job payload: {exc}", exit_code=2))
978
1021
 
@@ -19,6 +19,10 @@ import {
19
19
  streamLines,
20
20
  } from "./execution_utils.js";
21
21
  import { buildWorkerSandboxWritableEnv } from "./sandbox_env.js";
22
+ import {
23
+ createPythonPayloadTransport,
24
+ type PythonPayloadTransport,
25
+ } from "./python_payload_transport.js";
22
26
 
23
27
  interface GenericPythonExecutorConfig {
24
28
  backendName: string;
@@ -357,7 +361,6 @@ export function createGenericPythonExecutor(
357
361
  }),
358
362
  "utf-8",
359
363
  ).toString("base64");
360
- const args = [pythonBin, scriptPath, payloadBase64];
361
364
  const childTimeoutMs = resolveGenericPythonExecutorChildTimeoutMs({
362
365
  backendName,
363
366
  hostTimeoutMs: timeoutMs,
@@ -379,12 +382,15 @@ export function createGenericPythonExecutor(
379
382
  )}ms`
380
383
  : "";
381
384
 
382
- onLog?.(
383
- "stdout",
384
- `[${backendLabel}Executor] Spawning ${backendName} executor (timeout=${timeoutMs}ms; ${timeoutDetail}${childTimeoutDetail})`,
385
- );
386
-
385
+ let payloadTransport: PythonPayloadTransport | null = null;
387
386
  try {
387
+ payloadTransport = createPythonPayloadTransport(payloadBase64);
388
+ const args = [pythonBin, scriptPath, ...payloadTransport.args];
389
+ onLog?.(
390
+ "stdout",
391
+ `[${backendLabel}Executor] Spawning ${backendName} executor (timeout=${timeoutMs}ms; ${timeoutDetail}${childTimeoutDetail})`,
392
+ );
393
+
388
394
  const outputPolicy = {
389
395
  maxOutputChars: runtimeConfig.workerpals.outputMaxChars,
390
396
  maxOutputLines: runtimeConfig.workerpals.outputMaxLines,
@@ -539,6 +545,8 @@ export function createGenericPythonExecutor(
539
545
  "",
540
546
  ),
541
547
  };
548
+ } finally {
549
+ payloadTransport?.cleanup();
542
550
  }
543
551
  };
544
552
  }
@@ -0,0 +1,26 @@
1
+ import { mkdtempSync, rmSync, writeFileSync } from "fs";
2
+ import { tmpdir } from "os";
3
+ import { join } from "path";
4
+
5
+ export interface PythonPayloadTransport {
6
+ args: string[];
7
+ filePath: string;
8
+ cleanup: () => void;
9
+ }
10
+
11
+ export function createPythonPayloadTransport(payloadBase64: string): PythonPayloadTransport {
12
+ const dir = mkdtempSync(join(tmpdir(), "pushpals-python-payload-"));
13
+ const filePath = join(dir, "payload.b64");
14
+ writeFileSync(filePath, payloadBase64, { encoding: "utf8", mode: 0o600 });
15
+
16
+ let cleaned = false;
17
+ return {
18
+ args: ["--payload-file", filePath],
19
+ filePath,
20
+ cleanup: () => {
21
+ if (cleaned) return;
22
+ cleaned = true;
23
+ rmSync(dir, { recursive: true, force: true });
24
+ },
25
+ };
26
+ }
@@ -1918,8 +1918,6 @@ export class DockerExecutor {
1918
1918
 
1919
1919
  private matchesRetryablePattern(text: string): boolean {
1920
1920
  const transientPatterns: RegExp[] = [
1921
- /\bstalled before first response\b/i,
1922
- /\bstartup stall\b/i,
1923
1921
  /warm .*runtime/i,
1924
1922
  /failed to start warm container/i,
1925
1923
  /docker execution error/i,