@pushpalsdev/cli 1.1.13 → 1.1.15

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.
@@ -1648,6 +1648,7 @@ var DEFAULT_EMBEDDED_SERVICE_LAUNCH_WARN_MS = 5000;
1648
1648
  var EMBEDDED_SERVICE_RESTART_MAX_ATTEMPTS = 4;
1649
1649
  var WORKERPAL_STARTUP_READINESS_PROBE_MAX_MS = 15000;
1650
1650
  var CLI_SESSION_JOB_LOG_MAX_CHARS = 700;
1651
+ var CLI_SESSION_SHOW_JOB_EVENTS_ENV = "PUSHPALS_CLI_SHOW_JOB_EVENTS";
1651
1652
  var EMBEDDED_RUNTIME_SAFETY_CAP_DISABLE_ENV = "PUSHPALS_DISABLE_EMBEDDED_SAFETY_CAPS";
1652
1653
  var EMBEDDED_RUNTIME_WINDOWS_SAFETY_CAPS = {
1653
1654
  REMOTEBUDDY_WORKERPAL_STARTUP_TIMEOUT_MS: "120000",
@@ -1674,6 +1675,12 @@ function formatTimestampedCliLine(line, at = new Date) {
1674
1675
  }
1675
1676
  return `[${at.toISOString()}]${text}`;
1676
1677
  }
1678
+ function isTruthyCliEnvValue(value) {
1679
+ return /^(1|true|yes|on)$/i.test(String(value ?? "").trim());
1680
+ }
1681
+ function shouldShowCliSessionOperationalEvents(env = process.env) {
1682
+ return isTruthyCliEnvValue(env[CLI_SESSION_SHOW_JOB_EVENTS_ENV]);
1683
+ }
1677
1684
  function formatRuntimeStartupTimingSummary(input) {
1678
1685
  const phaseSummary = input.phases.map((phase) => `${phase.name}=${Math.max(0, Math.floor(phase.durationMs))}ms(${phase.status.trim() || "unknown"})`).join(" ");
1679
1686
  const detail = typeof input.detail === "string" && input.detail.trim() ? ` detail=${input.detail.trim()}` : "";
@@ -5218,7 +5225,10 @@ function formatSessionEventLine(event) {
5218
5225
  const type = String(event.type ?? "").toLowerCase();
5219
5226
  const from = String(event.from ?? "");
5220
5227
  const payload = event.payload ?? {};
5228
+ const showOperationalEvents = shouldShowCliSessionOperationalEvents();
5221
5229
  if (type === "job_enqueued") {
5230
+ if (!showOperationalEvents)
5231
+ return null;
5222
5232
  const jobId = String(payload.jobId ?? "").slice(0, 8);
5223
5233
  const kind = String(payload.kind ?? "").trim();
5224
5234
  const taskId = String(payload.taskId ?? "").slice(0, 8);
@@ -5226,11 +5236,15 @@ function formatSessionEventLine(event) {
5226
5236
  return `[job ${jobId}] queued: ${detail}`;
5227
5237
  }
5228
5238
  if (type === "job_claimed") {
5239
+ if (!showOperationalEvents)
5240
+ return null;
5229
5241
  const jobId = String(payload.jobId ?? "").slice(0, 8);
5230
5242
  const workerId = String(payload.workerId ?? "").trim();
5231
5243
  return `[job ${jobId}] claimed${workerId ? ` by ${workerId}` : ""}`;
5232
5244
  }
5233
5245
  if (type === "job_log") {
5246
+ if (!showOperationalEvents)
5247
+ return null;
5234
5248
  const jobId = String(payload.jobId ?? "").slice(0, 8);
5235
5249
  const stream = String(payload.stream ?? "").toLowerCase() === "stderr" ? " stderr" : "";
5236
5250
  const phase = compactCliSessionJobLogLine(String(payload.phase ?? "").trim());
@@ -5239,6 +5253,8 @@ function formatSessionEventLine(event) {
5239
5253
  return line ? `[job ${jobId}${stream}${phaseLabel}] ${line}` : null;
5240
5254
  }
5241
5255
  if (type === "job_failed") {
5256
+ if (!showOperationalEvents)
5257
+ return null;
5242
5258
  const jobId = String(payload.jobId ?? "").slice(0, 8);
5243
5259
  const message = String(payload.message ?? "").trim();
5244
5260
  return `[job ${jobId}] failed: ${message || "unknown"}`;
@@ -5251,24 +5267,34 @@ function formatSessionEventLine(event) {
5251
5267
  const text = String(payload.text ?? "").trim();
5252
5268
  if (!text)
5253
5269
  return null;
5270
+ if (/^All systems online\b/i.test(text))
5271
+ return null;
5254
5272
  return `assistant> ${text}`;
5255
5273
  }
5256
5274
  if (type === "task_progress") {
5275
+ if (!showOperationalEvents)
5276
+ return null;
5257
5277
  const taskId = String(payload.taskId ?? "").slice(0, 8);
5258
5278
  const message = String(payload.message ?? "").trim();
5259
5279
  return message ? `[task ${taskId}] ${message}` : null;
5260
5280
  }
5261
5281
  if (type === "task_failed") {
5282
+ if (!showOperationalEvents)
5283
+ return null;
5262
5284
  const taskId = String(payload.taskId ?? "").slice(0, 8);
5263
5285
  const message = String(payload.message ?? "").trim();
5264
5286
  return `[task ${taskId}] failed: ${message || "unknown"}`;
5265
5287
  }
5266
5288
  if (type === "task_completed") {
5289
+ if (!showOperationalEvents)
5290
+ return null;
5267
5291
  const taskId = String(payload.taskId ?? "").slice(0, 8);
5268
5292
  const summary = String(payload.summary ?? "").trim();
5269
5293
  return `[task ${taskId}] completed${summary ? `: ${summary}` : ""}`;
5270
5294
  }
5271
5295
  if (type === "job_completed") {
5296
+ if (!showOperationalEvents)
5297
+ return null;
5272
5298
  const jobId = String(payload.jobId ?? "").slice(0, 8);
5273
5299
  const summary = String(payload.summary ?? "").trim();
5274
5300
  return `[job ${jobId}] completed${summary ? `: ${summary}` : ""}`;
@@ -5277,6 +5303,8 @@ function formatSessionEventLine(event) {
5277
5303
  return null;
5278
5304
  }
5279
5305
  if (type === "status") {
5306
+ if (!showOperationalEvents)
5307
+ return null;
5280
5308
  const state = String(payload.state ?? "").trim();
5281
5309
  const detail = String(payload.detail ?? "").trim();
5282
5310
  const source = from || String(payload.agentId ?? "status");
@@ -5315,8 +5343,9 @@ function shouldSuppressCliSessionJobLogLine(line) {
5315
5343
  return true;
5316
5344
  if (/^\[QualityGate\]\s+(?:Policy:|Gates:)/i.test(text))
5317
5345
  return true;
5318
- if (/^\[Openai_codexExecutor\]\s+Spawning openai_codex executor/i.test(text))
5346
+ 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
5347
  return true;
5348
+ }
5320
5349
  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
5350
  return true;
5322
5351
  }
@@ -6008,6 +6037,7 @@ export {
6008
6037
  startEmbeddedMonitoringHub,
6009
6038
  shutdownEmbeddedServiceManagerGracefully,
6010
6039
  shouldUseRemoteBuddySilentStartupFallback,
6040
+ shouldShowCliSessionOperationalEvents,
6011
6041
  shouldRunEmbeddedRuntimeStartupPrechecks,
6012
6042
  shouldRestartEmbeddedService,
6013
6043
  runCommandWithEnv,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushpalsdev/cli",
3
- "version": "1.1.13",
3
+ "version": "1.1.15",
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,11 +195,59 @@ 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
  }
161
206
 
207
+ export function normalizeGenericPythonExecutorParsedResultForTimeout(params: {
208
+ backendName: string;
209
+ kind: string;
210
+ timedOut: boolean;
211
+ timeoutMs: number;
212
+ timeoutDetail?: string;
213
+ summary: string;
214
+ stdout: string;
215
+ stderr: string;
216
+ exitCode: number;
217
+ }): { summary: string; stdout: string; stderr: string; exitCode: number } {
218
+ const signalTerminatedCodex =
219
+ params.timedOut &&
220
+ params.backendName === "openai_codex" &&
221
+ /\bopenai_codex interrupted by signal 15\b/i.test(params.summary);
222
+ if (!signalTerminatedCodex) {
223
+ return {
224
+ summary: params.summary,
225
+ stdout: params.stdout,
226
+ stderr: params.stderr,
227
+ exitCode: params.exitCode,
228
+ };
229
+ }
230
+
231
+ const timeoutDetail = String(params.timeoutDetail ?? "").trim();
232
+ const cleanedStderr = String(params.stderr ?? "")
233
+ .replace(/\bopenai_codex interrupted by signal 15\b/gi, "OpenAI Codex exceeded the execution budget")
234
+ .trim();
235
+ const stderr = [
236
+ `OpenAI Codex exceeded the PushPals execution budget before returning a completed result.`,
237
+ timeoutDetail ? `Timeout detail: ${timeoutDetail}.` : "",
238
+ cleanedStderr ? `Last stderr:\n${cleanedStderr}` : "",
239
+ ]
240
+ .filter(Boolean)
241
+ .join("\n");
242
+
243
+ return {
244
+ summary: `${params.backendName} execution budget expired after ${params.timeoutMs}ms for ${params.kind}`,
245
+ stdout: params.stdout,
246
+ stderr,
247
+ exitCode: 124,
248
+ };
249
+ }
250
+
162
251
  export function createGenericPythonExecutor(
163
252
  config: GenericPythonExecutorConfig,
164
253
  ): BackendTaskExecutor {
@@ -190,15 +279,21 @@ export function createGenericPythonExecutor(
190
279
  typeof budgets?.executionBudgetMs === "number" && Number.isFinite(budgets.executionBudgetMs)
191
280
  ? Math.max(10_000, Math.floor(budgets.executionBudgetMs))
192
281
  : null;
282
+ const finalizationBudgetMs =
283
+ typeof budgets?.finalizationBudgetMs === "number" && Number.isFinite(budgets.finalizationBudgetMs)
284
+ ? Math.max(0, Math.floor(budgets.finalizationBudgetMs))
285
+ : null;
193
286
  const timeoutMs = resolveGenericPythonExecutorTimeoutMs({
194
287
  configuredTimeoutMs,
195
288
  executionBudgetMs,
289
+ finalizationBudgetMs,
196
290
  capTimeoutToExecutionBudget: config.capTimeoutToExecutionBudget,
197
291
  });
198
292
  const timeoutDetail = formatGenericPythonExecutorTimeoutDetail(
199
293
  config,
200
294
  configuredTimeoutMs,
201
295
  executionBudgetMs,
296
+ finalizationBudgetMs,
202
297
  timeoutMs,
203
298
  );
204
299
  const payloadBase64 = Buffer.from(
@@ -210,6 +305,11 @@ export function createGenericPythonExecutor(
210
305
  "utf-8",
211
306
  ).toString("base64");
212
307
  const args = [pythonBin, scriptPath, payloadBase64];
308
+ const childTimeoutEnv = resolveGenericPythonExecutorChildTimeoutEnv({
309
+ backendName,
310
+ hostTimeoutMs: timeoutMs,
311
+ executionBudgetMs,
312
+ });
213
313
 
214
314
  onLog?.(
215
315
  "stdout",
@@ -229,6 +329,7 @@ export function createGenericPythonExecutor(
229
329
  stderr: "pipe",
230
330
  env: {
231
331
  ...buildWorkerSandboxWritableEnv(repo),
332
+ ...childTimeoutEnv,
232
333
  PUSHPALS_REPO_PATH: repo,
233
334
  PUSHPALS_ASSIGNED_REPO_ROOT: repo,
234
335
  PYTHONIOENCODING: "utf-8",
@@ -333,16 +434,27 @@ export function createGenericPythonExecutor(
333
434
  parsed.usage,
334
435
  estimateJobTokenUsage(backendName, modelId, params, summary, parsedStdout, parsedStderr),
335
436
  );
336
-
337
- return {
338
- ok: typeof parsed.ok === "boolean" ? parsed.ok : exitCode === 0,
437
+ const normalized = normalizeGenericPythonExecutorParsedResultForTimeout({
438
+ backendName,
439
+ kind,
440
+ timedOut,
441
+ timeoutMs,
442
+ timeoutDetail,
339
443
  summary,
340
- stdout: truncate(parsedStdout, outputPolicy),
341
- stderr: truncate(parsedStderr, outputPolicy),
444
+ stdout: parsedStdout,
445
+ stderr: parsedStderr,
342
446
  exitCode:
343
447
  typeof parsed.exitCode === "number" && Number.isFinite(parsed.exitCode)
344
448
  ? parsed.exitCode
345
449
  : exitCode,
450
+ });
451
+
452
+ return {
453
+ ok: typeof parsed.ok === "boolean" ? parsed.ok : exitCode === 0,
454
+ summary: normalized.summary,
455
+ stdout: truncate(normalized.stdout, outputPolicy),
456
+ stderr: truncate(normalized.stderr, outputPolicy),
457
+ exitCode: normalized.exitCode,
346
458
  usage,
347
459
  };
348
460
  } catch (err) {