@pushpalsdev/cli 1.1.8 → 1.1.10

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.
@@ -67,6 +67,7 @@ const DEFAULT_LLM_MODEL = "local-model";
67
67
  const CODEX_UNAVAILABLE_WORKER_EXIT_CODE = 86;
68
68
  const CODEX_UNAVAILABLE_DOCKER_SHUTDOWN_GRACE_MS = 5_000;
69
69
  const CODEX_UNAVAILABLE_WORKER_FORCE_EXIT_MS = 4_000;
70
+ const DEFAULT_JOB_PROGRESS_LOG_EVERY_MS = 60_000;
70
71
  const CONFIG = loadPushPalsConfig();
71
72
  const LOG = new Logger("WorkerPals");
72
73
 
@@ -197,7 +198,12 @@ async function reportToolRunForUnsuccessfulJob(args: {
197
198
  if (record.failureClass === "unknown" && record.tool === "shell") return;
198
199
 
199
200
  try {
200
- const response = await postJsonWithTimeout(`${args.opts.server}/tool-runs`, args.headers, record, 5_000);
201
+ const response = await postJsonWithTimeout(
202
+ `${args.opts.server}/tool-runs`,
203
+ args.headers,
204
+ record,
205
+ 5_000,
206
+ );
201
207
  if (!response.ok) {
202
208
  const detail = await response.text().catch(() => "");
203
209
  console.warn(
@@ -315,6 +321,13 @@ function formatDurationMs(durationMs: number): string {
315
321
  return `${minutes}m ${seconds}s`;
316
322
  }
317
323
 
324
+ function resolveJobProgressLogEveryMs(): number {
325
+ const raw = Number.parseInt(process.env.PUSHPALS_WORKERPAL_PROGRESS_LOG_MS ?? "", 10);
326
+ if (Number.isFinite(raw) && raw === 0) return 0;
327
+ if (Number.isFinite(raw) && raw >= 10_000) return raw;
328
+ return DEFAULT_JOB_PROGRESS_LOG_EVERY_MS;
329
+ }
330
+
318
331
  function sanitizeJobLogLine(line: string): string {
319
332
  // Strip ANSI escape/control sequences and collapse whitespace.
320
333
  const cleaned = line
@@ -985,8 +998,7 @@ function failNoChangeReviewFixJob(jobId: string, result: WorkerJobResult): Worke
985
998
  return {
986
999
  ...result,
987
1000
  ok: false,
988
- summary:
989
- `Rejected review-fix job ${jobId} produced no code changes; refusing unchanged branch re-review.`,
1001
+ summary: `Rejected review-fix job ${jobId} produced no code changes; refusing unchanged branch re-review.`,
990
1002
  stderr: [
991
1003
  result.stderr,
992
1004
  "Review-fix jobs must make at least one concrete code/test/docs change before requesting another review.",
@@ -1002,9 +1014,7 @@ function taskExecuteOrigin(params: Record<string, unknown> | undefined): "user"
1002
1014
  if (!params) return "user";
1003
1015
  if (params.origin === "autonomy") return "autonomy";
1004
1016
  const autonomy = params.autonomy;
1005
- return autonomy && typeof autonomy === "object" && !Array.isArray(autonomy)
1006
- ? "autonomy"
1007
- : "user";
1017
+ return autonomy && typeof autonomy === "object" && !Array.isArray(autonomy) ? "autonomy" : "user";
1008
1018
  }
1009
1019
 
1010
1020
  async function enqueueCompletion(
@@ -1109,15 +1119,19 @@ async function failActiveJobOnShutdown(
1109
1119
  runtimeState.currentSessionId &&
1110
1120
  shouldEmitDirectSessionJobEvent({ ok: false, statusPersistedToServer })
1111
1121
  ) {
1112
- await transport.queueSessionCommand(runtimeState.currentSessionId, {
1113
- type: "job_failed",
1114
- payload: {
1115
- jobId: activeJobId,
1116
- message,
1117
- detail,
1122
+ await transport.queueSessionCommand(
1123
+ runtimeState.currentSessionId,
1124
+ {
1125
+ type: "job_failed",
1126
+ payload: {
1127
+ jobId: activeJobId,
1128
+ message,
1129
+ detail,
1130
+ },
1131
+ from: `worker:${opts.workerId}`,
1118
1132
  },
1119
- from: `worker:${opts.workerId}`,
1120
- }, { priority: "high" });
1133
+ { priority: "high" },
1134
+ );
1121
1135
  }
1122
1136
  }
1123
1137
 
@@ -1224,10 +1238,7 @@ async function workerLoop(
1224
1238
  const job = data.job;
1225
1239
 
1226
1240
  if (job) {
1227
- if (
1228
- dockerExecutor &&
1229
- dockerExecutor.shouldPrepareMergeConflictJobBeforeExecution(job)
1230
- ) {
1241
+ if (dockerExecutor && dockerExecutor.shouldPrepareMergeConflictJobBeforeExecution(job)) {
1231
1242
  const deferMs = dockerExecutor.recommendedMergeConflictDeferMs();
1232
1243
  const deferred = await deferClaimedJobForMaintenance(opts, headers, job.id, deferMs);
1233
1244
  if (!deferred.ok) {
@@ -1325,50 +1336,86 @@ async function workerLoop(
1325
1336
  }, heartbeatEveryMs);
1326
1337
 
1327
1338
  if (job.sessionId) {
1328
- await transport.queueSessionCommand(job.sessionId, {
1329
- type: "job_claimed",
1330
- payload: { jobId: job.id, workerId: opts.workerId },
1331
- from: `worker:${opts.workerId}`,
1332
- }, { priority: "high" });
1339
+ await transport.queueSessionCommand(
1340
+ job.sessionId,
1341
+ {
1342
+ type: "job_claimed",
1343
+ payload: { jobId: job.id, workerId: opts.workerId },
1344
+ from: `worker:${opts.workerId}`,
1345
+ },
1346
+ { priority: "high" },
1347
+ );
1333
1348
  }
1334
1349
 
1335
1350
  let stdoutSeq = 0;
1336
1351
  let stderrSeq = 0;
1337
1352
  let lastCleanLog = "";
1338
1353
  let lastCleanLogAt = 0;
1354
+ let lastForwardedJobLogAt = Date.now();
1339
1355
 
1340
- const onLog = job.sessionId
1341
- ? (stream: "stdout" | "stderr", line: string) => {
1356
+ const emitJobLog = job.sessionId
1357
+ ? (stream: "stdout" | "stderr", line: string): boolean => {
1342
1358
  const cleaned = sanitizeJobLogLine(line);
1343
- if (!cleaned) return;
1344
- // Print executor logs locally only in debug mode.
1345
- if (LOG.isDebugEnabled()) LOG.debug(`[${stream}] ${cleaned}`);
1359
+ if (!cleaned) return false;
1346
1360
 
1347
1361
  // Drop high-frequency terminal progress redraw spam; keep meaningful lines.
1348
- if (isNoisyProgressLine(cleaned)) return;
1362
+ if (isNoisyProgressLine(cleaned)) return false;
1349
1363
 
1350
1364
  // Collapse very noisy duplicate lines emitted in tight loops.
1351
1365
  const now = Date.now();
1352
- if (cleaned === lastCleanLog && now - lastCleanLogAt < 1_000) return;
1366
+ if (cleaned === lastCleanLog && now - lastCleanLogAt < 1_000) return false;
1353
1367
  lastCleanLog = cleaned;
1354
1368
  lastCleanLogAt = now;
1369
+ lastForwardedJobLogAt = now;
1355
1370
  const logTs = new Date(now).toISOString();
1356
1371
 
1357
1372
  const seq = stream === "stdout" ? ++stdoutSeq : ++stderrSeq;
1358
- void transport.queueSessionCommand(job.sessionId, {
1359
- type: "job_log",
1360
- payload: { jobId: job.id, stream, seq, line: cleaned, ts: logTs },
1361
- from: `worker:${opts.workerId}`,
1362
- }, { droppable: true });
1373
+ void transport.queueSessionCommand(
1374
+ job.sessionId,
1375
+ {
1376
+ type: "job_log",
1377
+ payload: { jobId: job.id, stream, seq, line: cleaned, ts: logTs },
1378
+ from: `worker:${opts.workerId}`,
1379
+ },
1380
+ { droppable: true },
1381
+ );
1363
1382
  void transport.queueJobLog(job.id, {
1364
1383
  stream,
1365
1384
  seq,
1366
1385
  message: cleaned,
1367
1386
  ts: logTs,
1368
1387
  });
1388
+ return true;
1389
+ }
1390
+ : undefined;
1391
+
1392
+ const onLog = emitJobLog
1393
+ ? (stream: "stdout" | "stderr", line: string) => {
1394
+ const cleaned = sanitizeJobLogLine(line);
1395
+ if (LOG.isDebugEnabled() && cleaned) LOG.debug(`[${stream}] ${cleaned}`);
1396
+ emitJobLog(stream, line);
1369
1397
  }
1370
1398
  : undefined;
1371
1399
 
1400
+ const jobClaimedAtMs = Date.now();
1401
+ const jobProgressLogEveryMs = resolveJobProgressLogEveryMs();
1402
+ const jobProgressTimer =
1403
+ emitJobLog && jobProgressLogEveryMs > 0
1404
+ ? setInterval(() => {
1405
+ const now = Date.now();
1406
+ const quietForMs = Math.max(0, now - lastForwardedJobLogAt);
1407
+ if (quietForMs < jobProgressLogEveryMs) return;
1408
+ emitJobLog(
1409
+ "stdout",
1410
+ `[WorkerPals] Job ${job.id} still running after ${formatDurationMs(
1411
+ now - jobClaimedAtMs,
1412
+ )} (kind=${job.kind}, worker=${opts.workerId}, quiet_for=${formatDurationMs(
1413
+ quietForMs,
1414
+ )}).`,
1415
+ );
1416
+ }, jobProgressLogEveryMs)
1417
+ : null;
1418
+
1372
1419
  let directWorktreePath: string | null = null;
1373
1420
  let executionRepo = opts.repo;
1374
1421
  let result: WorkerJobResult | null = null;
@@ -1611,11 +1658,15 @@ async function workerLoop(
1611
1658
  durationMs: jobDurationMs,
1612
1659
  phase: job.kind,
1613
1660
  });
1614
- const response = await postJsonWithTimeout(`${opts.server}/jobs/${job.id}/fail`, headers, {
1615
- message: result.summary,
1616
- detail: redactSensitiveText(result.stderr ?? ""),
1617
- durationMs: jobDurationMs,
1618
- });
1661
+ const response = await postJsonWithTimeout(
1662
+ `${opts.server}/jobs/${job.id}/fail`,
1663
+ headers,
1664
+ {
1665
+ message: result.summary,
1666
+ detail: redactSensitiveText(result.stderr ?? ""),
1667
+ durationMs: jobDurationMs,
1668
+ },
1669
+ );
1619
1670
  statusPersistedToServer = response.ok;
1620
1671
  console.log(
1621
1672
  `[WorkerPals] Job ${job.id} failed in ${formatDurationMs(jobDurationMs)}: ${result.summary}`,
@@ -1703,6 +1754,7 @@ async function workerLoop(
1703
1754
  }
1704
1755
  } finally {
1705
1756
  clearInterval(busyHeartbeat);
1757
+ if (jobProgressTimer) clearInterval(jobProgressTimer);
1706
1758
  if (recycleWorkerAfterJob) {
1707
1759
  runtimeState.shutdownRequested = true;
1708
1760
  const forceExitTimer = setTimeout(() => {
@@ -1895,7 +1947,9 @@ async function main(): Promise<void> {
1895
1947
  },
1896
1948
  }),
1897
1949
  );
1898
- await withTimeout(failActiveJobOnShutdown(opts, headers, runtimeState, transport, signalName));
1950
+ await withTimeout(
1951
+ failActiveJobOnShutdown(opts, headers, runtimeState, transport, signalName),
1952
+ );
1899
1953
  await withTimeout(transport.flush());
1900
1954
  if (dockerExecutor) {
1901
1955
  await withTimeout(