@pushpalsdev/cli 1.1.13 → 1.1.14

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.
@@ -5315,8 +5315,9 @@ function shouldSuppressCliSessionJobLogLine(line) {
5315
5315
  return true;
5316
5316
  if (/^\[QualityGate\]\s+(?:Policy:|Gates:)/i.test(text))
5317
5317
  return true;
5318
- if (/^\[Openai_codexExecutor\]\s+Spawning openai_codex executor/i.test(text))
5318
+ if (/^\[(?:Openai_codex|OpenHands|Miniswe)Executor\]\s+(?:Spawning\b|Timeout reached\b|Still running\b|Process did not exit after graceful timeout termination\b)/i.test(text)) {
5319
5319
  return true;
5320
+ }
5320
5321
  if (/^\[OpenAICodexExecutor\]\s+(?:Planner guidance|Codex auth mode|ChatGPT auth mode|Starting codex exec|codex exec finished|Codex JSON stream captured|Codex stdout captured|No reasoning-like|Reasoning-like event|Usage observed|Temporarily masked repo-local|Timeout reached after|Process did not exit after graceful timeout termination)/i.test(text)) {
5321
5322
  return true;
5322
5323
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushpalsdev/cli",
3
- "version": "1.1.13",
3
+ "version": "1.1.14",
4
4
  "description": "PushPals terminal CLI for LocalBuddy -> RemoteBuddy orchestration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1892,7 +1892,7 @@ def _run_codex_task(
1892
1892
  command_policy_rejection_loop = False
1893
1893
  no_edit_watchdog_s = (
1894
1894
  _resolve_no_edit_watchdog_seconds(prompt, communicate_timeout_s)
1895
- if no_edit_recovery_attempt < _MAX_NO_EDIT_RECOVERY_ATTEMPTS
1895
+ if no_edit_recovery_attempt <= _MAX_NO_EDIT_RECOVERY_ATTEMPTS
1896
1896
  else None
1897
1897
  )
1898
1898
  no_edit_deadline = (
@@ -823,6 +823,69 @@ class OpenAICodexRuntimeConfigTests(unittest.TestCase):
823
823
  self.assertIn("Patched immediately after no-edit recovery", str(result.get("stdout") or ""))
824
824
  self.assertIn("src/", str(result.get("stdout") or ""))
825
825
 
826
+ def test_run_codex_task_recovery_attempt_is_still_guarded_by_no_edit_watchdog(self) -> None:
827
+ with tempfile.TemporaryDirectory(prefix="pushpals-codex-no-edit-watchdog-fail-") as temp_dir:
828
+ repo = Path(temp_dir) / "repo"
829
+ repo.mkdir(parents=True, exist_ok=True)
830
+ (repo / "README.md").write_text("# no edit watchdog failure repo\n", encoding="utf-8")
831
+ subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True, text=True)
832
+ subprocess.run(
833
+ ["git", "config", "user.name", "PushPals Test"],
834
+ cwd=repo,
835
+ check=True,
836
+ capture_output=True,
837
+ text=True,
838
+ )
839
+ subprocess.run(
840
+ ["git", "config", "user.email", "pushpals-tests@example.com"],
841
+ cwd=repo,
842
+ check=True,
843
+ capture_output=True,
844
+ text=True,
845
+ )
846
+ subprocess.run(["git", "add", "README.md"], cwd=repo, check=True, capture_output=True, text=True)
847
+ subprocess.run(
848
+ ["git", "commit", "-m", "chore: seed no-edit watchdog failure repo"],
849
+ cwd=repo,
850
+ check=True,
851
+ capture_output=True,
852
+ text=True,
853
+ )
854
+
855
+ stub_path = Path(temp_dir) / "fake_codex_no_edit_watchdog_fail.py"
856
+ stub_path.write_text(
857
+ "\n".join(
858
+ [
859
+ "import sys",
860
+ "import time",
861
+ "",
862
+ "sys.stdin.read()",
863
+ "print('item.completed | Still inspecting, no patch yet.', flush=True)",
864
+ "time.sleep(10)",
865
+ ]
866
+ ),
867
+ encoding="utf-8",
868
+ )
869
+
870
+ env_overrides = {
871
+ "PUSHPALS_OPENAI_CODEX_BIN_JSON": json.dumps([sys.executable, str(stub_path)]),
872
+ "PUSHPALS_OPENAI_CODEX_AUTH_MODE": "api_key",
873
+ "OPENAI_API_KEY": "pushpals-no-edit-watchdog-fail-test-key",
874
+ "WORKERPALS_OPENAI_CODEX_TIMEOUT_S": "20",
875
+ "WORKERPALS_OPENAI_CODEX_NO_EDIT_WATCHDOG_S": "1",
876
+ "WORKERPALS_OPENAI_CODEX_PROGRESS_LOG_INTERVAL_S": "1",
877
+ }
878
+ with mock.patch.dict(os.environ, env_overrides, clear=False):
879
+ result = _run_codex_task(
880
+ str(repo),
881
+ "Polish the first-entry home shell with a compact visual patch.",
882
+ [],
883
+ )
884
+
885
+ self.assertFalse(result.get("ok"), result)
886
+ self.assertEqual(result.get("exitCode"), 124)
887
+ self.assertIn("no publishable changes", str(result.get("summary") or ""))
888
+
826
889
  def test_codex_changed_paths_filters_dependency_artifacts_from_publishable_delta(self) -> None:
827
890
  with tempfile.TemporaryDirectory(prefix="pushpals-codex-artifact-delta-") as temp_dir:
828
891
  repo = Path(temp_dir) / "repo"
@@ -28,6 +28,8 @@ interface GenericPythonExecutorConfig {
28
28
  capTimeoutToExecutionBudget?: boolean;
29
29
  }
30
30
 
31
+ const BACKEND_TIMEOUT_RESULT_GRACE_MS = 30_000;
32
+
31
33
  function estimateTokensFromText(text: string): number {
32
34
  return Math.max(0, Math.ceil(String(text ?? "").length / 3));
33
35
  }
@@ -123,6 +125,7 @@ function resolveRuntimeSettings(
123
125
  export function resolveGenericPythonExecutorTimeoutMs(params: {
124
126
  configuredTimeoutMs: number;
125
127
  executionBudgetMs?: number | null;
128
+ finalizationBudgetMs?: number | null;
126
129
  capTimeoutToExecutionBudget?: boolean;
127
130
  }): number {
128
131
  const configuredTimeoutMs = Math.max(10_000, Math.floor(params.configuredTimeoutMs));
@@ -130,12 +133,49 @@ export function resolveGenericPythonExecutorTimeoutMs(params: {
130
133
  typeof params.executionBudgetMs === "number" && Number.isFinite(params.executionBudgetMs)
131
134
  ? Math.max(10_000, Math.floor(params.executionBudgetMs))
132
135
  : null;
136
+ const finalizationBudgetMs =
137
+ typeof params.finalizationBudgetMs === "number" && Number.isFinite(params.finalizationBudgetMs)
138
+ ? Math.max(0, Math.floor(params.finalizationBudgetMs))
139
+ : 0;
133
140
  if (executionBudgetMs != null && params.capTimeoutToExecutionBudget !== false) {
134
- return Math.min(configuredTimeoutMs, executionBudgetMs);
141
+ return Math.min(configuredTimeoutMs, executionBudgetMs + finalizationBudgetMs);
135
142
  }
136
143
  return configuredTimeoutMs;
137
144
  }
138
145
 
146
+ export function resolveGenericPythonExecutorChildTimeoutMs(params: {
147
+ backendName: string;
148
+ hostTimeoutMs: number;
149
+ executionBudgetMs?: number | null;
150
+ }): number | null {
151
+ const hostTimeoutMs = Math.max(10_000, Math.floor(params.hostTimeoutMs));
152
+ if (params.backendName !== "openai_codex") return null;
153
+ const executionBudgetMs =
154
+ typeof params.executionBudgetMs === "number" && Number.isFinite(params.executionBudgetMs)
155
+ ? Math.max(10_000, Math.floor(params.executionBudgetMs))
156
+ : null;
157
+ const childBudgetMs =
158
+ executionBudgetMs == null ? hostTimeoutMs : Math.min(hostTimeoutMs, executionBudgetMs);
159
+ const graceMs = Math.min(
160
+ BACKEND_TIMEOUT_RESULT_GRACE_MS,
161
+ Math.max(2_000, Math.floor(childBudgetMs / 10)),
162
+ );
163
+ return Math.max(1_000, childBudgetMs - graceMs);
164
+ }
165
+
166
+ export function resolveGenericPythonExecutorChildTimeoutEnv(params: {
167
+ backendName: string;
168
+ hostTimeoutMs: number;
169
+ executionBudgetMs?: number | null;
170
+ }): Record<string, string> {
171
+ const childTimeoutMs = resolveGenericPythonExecutorChildTimeoutMs(params);
172
+ if (childTimeoutMs == null) return {};
173
+ return {
174
+ WORKERPALS_OPENAI_CODEX_TIMEOUT_MS: String(childTimeoutMs),
175
+ WORKERPALS_OPENAI_CODEX_TIMEOUT_S: String(Math.max(1, Math.floor(childTimeoutMs / 1000))),
176
+ };
177
+ }
178
+
139
179
  function toSnakeConfigKey(key: string): string {
140
180
  return key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
141
181
  }
@@ -144,6 +184,7 @@ function formatGenericPythonExecutorTimeoutDetail(
144
184
  config: GenericPythonExecutorConfig,
145
185
  configuredTimeoutMs: number,
146
186
  executionBudgetMs: number | null,
187
+ finalizationBudgetMs: number | null,
147
188
  timeoutMs: number,
148
189
  ): string {
149
190
  const configPath = `workerpals.${toSnakeConfigKey(config.timeoutConfigKey)}`;
@@ -154,7 +195,11 @@ function formatGenericPythonExecutorTimeoutDetail(
154
195
  return `${configPath}=${configuredTimeoutMs}ms; planning executionBudgetMs=${executionBudgetMs}ms ignored by backend opt-out`;
155
196
  }
156
197
  if (timeoutMs < configuredTimeoutMs) {
157
- return `${configPath}=${configuredTimeoutMs}ms capped by planning executionBudgetMs=${executionBudgetMs}ms`;
198
+ const finalizationDetail =
199
+ finalizationBudgetMs && finalizationBudgetMs > 0
200
+ ? ` + finalizationBudgetMs=${finalizationBudgetMs}ms`
201
+ : "";
202
+ return `${configPath}=${configuredTimeoutMs}ms capped by planning executionBudgetMs=${executionBudgetMs}ms${finalizationDetail}`;
158
203
  }
159
204
  return `${configPath}=${configuredTimeoutMs}ms within planning executionBudgetMs=${executionBudgetMs}ms`;
160
205
  }
@@ -190,15 +235,21 @@ export function createGenericPythonExecutor(
190
235
  typeof budgets?.executionBudgetMs === "number" && Number.isFinite(budgets.executionBudgetMs)
191
236
  ? Math.max(10_000, Math.floor(budgets.executionBudgetMs))
192
237
  : null;
238
+ const finalizationBudgetMs =
239
+ typeof budgets?.finalizationBudgetMs === "number" && Number.isFinite(budgets.finalizationBudgetMs)
240
+ ? Math.max(0, Math.floor(budgets.finalizationBudgetMs))
241
+ : null;
193
242
  const timeoutMs = resolveGenericPythonExecutorTimeoutMs({
194
243
  configuredTimeoutMs,
195
244
  executionBudgetMs,
245
+ finalizationBudgetMs,
196
246
  capTimeoutToExecutionBudget: config.capTimeoutToExecutionBudget,
197
247
  });
198
248
  const timeoutDetail = formatGenericPythonExecutorTimeoutDetail(
199
249
  config,
200
250
  configuredTimeoutMs,
201
251
  executionBudgetMs,
252
+ finalizationBudgetMs,
202
253
  timeoutMs,
203
254
  );
204
255
  const payloadBase64 = Buffer.from(
@@ -210,6 +261,11 @@ export function createGenericPythonExecutor(
210
261
  "utf-8",
211
262
  ).toString("base64");
212
263
  const args = [pythonBin, scriptPath, payloadBase64];
264
+ const childTimeoutEnv = resolveGenericPythonExecutorChildTimeoutEnv({
265
+ backendName,
266
+ hostTimeoutMs: timeoutMs,
267
+ executionBudgetMs,
268
+ });
213
269
 
214
270
  onLog?.(
215
271
  "stdout",
@@ -229,6 +285,7 @@ export function createGenericPythonExecutor(
229
285
  stderr: "pipe",
230
286
  env: {
231
287
  ...buildWorkerSandboxWritableEnv(repo),
288
+ ...childTimeoutEnv,
232
289
  PUSHPALS_REPO_PATH: repo,
233
290
  PUSHPALS_ASSIGNED_REPO_ROOT: repo,
234
291
  PYTHONIOENCODING: "utf-8",