@pushpalsdev/cli 1.1.9 → 1.1.11
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/dist/pushpals-cli.js +107 -9
- package/package.json +1 -1
- package/runtime/sandbox/apps/workerpals/src/backends/openai_codex/openai_codex_executor.py +6 -1
- package/runtime/sandbox/apps/workerpals/src/backends/openai_codex/test_openai_codex_runtime_config.py +126 -0
- package/runtime/sandbox/apps/workerpals/src/backends/shared/executor_base.py +177 -0
- package/runtime/sandbox/apps/workerpals/src/docker_executor.ts +8 -7
- package/runtime/sandbox/apps/workerpals/src/execute_job.ts +513 -7
- package/runtime/sandbox/apps/workerpals/src/workerpals_main.ts +168 -41
|
@@ -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(
|
|
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
|
|
@@ -329,6 +342,70 @@ function isNoisyProgressLine(line: string): boolean {
|
|
|
329
342
|
return /^(📦 Installing \[\d+\/\d+\]|🔍 Resolving\.\.\.|🔒 Saving lockfile\.\.\.)$/.test(line);
|
|
330
343
|
}
|
|
331
344
|
|
|
345
|
+
type WorkerJobPhase =
|
|
346
|
+
| "discovering"
|
|
347
|
+
| "editing"
|
|
348
|
+
| "test harness repair"
|
|
349
|
+
| "focused validation"
|
|
350
|
+
| "full validation"
|
|
351
|
+
| "final diff review"
|
|
352
|
+
| "publishing"
|
|
353
|
+
| "quality revision";
|
|
354
|
+
|
|
355
|
+
function inferWorkerJobPhaseFromLogLine(line: string): WorkerJobPhase | null {
|
|
356
|
+
const text = String(line ?? "").trim();
|
|
357
|
+
if (!text) return null;
|
|
358
|
+
if (/Quality gate requested revision|Quality revision required|revision guidance/i.test(text)) {
|
|
359
|
+
return "quality revision";
|
|
360
|
+
}
|
|
361
|
+
if (
|
|
362
|
+
/test harness|React Native package|reactNativeMock|mock helper|mock was missing|expo-secure-store|import error|Cannot find module|does not provide an export|no exported member|Animated\.View|SettingsContext|skin validator/i.test(
|
|
363
|
+
text,
|
|
364
|
+
)
|
|
365
|
+
) {
|
|
366
|
+
return "test harness repair";
|
|
367
|
+
}
|
|
368
|
+
if (
|
|
369
|
+
/focused validation|focused checks|targeted test|focused test|new regression|focused regression|fast checks|rerunning .*regression|node --check/i.test(
|
|
370
|
+
text,
|
|
371
|
+
)
|
|
372
|
+
) {
|
|
373
|
+
return "focused validation";
|
|
374
|
+
}
|
|
375
|
+
if (
|
|
376
|
+
/ValidationGate|required validation|full .*test suite|whole Bun test|repo-level|bun test\b|bunx? tsc|typecheck|type check|bun run lint|web:e2e|browser smoke/i.test(
|
|
377
|
+
text,
|
|
378
|
+
)
|
|
379
|
+
) {
|
|
380
|
+
return "full validation";
|
|
381
|
+
}
|
|
382
|
+
if (/creating commit|Publish blocked|publish-blocked|completion ref|enqueueCompletion/i.test(text)) {
|
|
383
|
+
return "publishing";
|
|
384
|
+
}
|
|
385
|
+
if (
|
|
386
|
+
/final diff|diff review|git diff|git status|whitespace|line-ending|line ending|pruning|remove unrelated|remaining diff|changed files/i.test(
|
|
387
|
+
text,
|
|
388
|
+
)
|
|
389
|
+
) {
|
|
390
|
+
return "final diff review";
|
|
391
|
+
}
|
|
392
|
+
if (
|
|
393
|
+
/editing|patch|implemented|adding|fixing|updating|wiring|in place|changes are in place|making .*change|tightening|restore|normalizing/i.test(
|
|
394
|
+
text,
|
|
395
|
+
)
|
|
396
|
+
) {
|
|
397
|
+
return "editing";
|
|
398
|
+
}
|
|
399
|
+
if (
|
|
400
|
+
/read|inspect|checking|locating|opening|artifact|screenshot|README|context|discover|search|rg |current checkout|worktree/i.test(
|
|
401
|
+
text,
|
|
402
|
+
)
|
|
403
|
+
) {
|
|
404
|
+
return "discovering";
|
|
405
|
+
}
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
|
|
332
409
|
export function shouldEmitDirectSessionJobEvent(options: {
|
|
333
410
|
ok: boolean;
|
|
334
411
|
statusPersistedToServer: boolean;
|
|
@@ -985,8 +1062,7 @@ function failNoChangeReviewFixJob(jobId: string, result: WorkerJobResult): Worke
|
|
|
985
1062
|
return {
|
|
986
1063
|
...result,
|
|
987
1064
|
ok: false,
|
|
988
|
-
summary:
|
|
989
|
-
`Rejected review-fix job ${jobId} produced no code changes; refusing unchanged branch re-review.`,
|
|
1065
|
+
summary: `Rejected review-fix job ${jobId} produced no code changes; refusing unchanged branch re-review.`,
|
|
990
1066
|
stderr: [
|
|
991
1067
|
result.stderr,
|
|
992
1068
|
"Review-fix jobs must make at least one concrete code/test/docs change before requesting another review.",
|
|
@@ -1002,9 +1078,7 @@ function taskExecuteOrigin(params: Record<string, unknown> | undefined): "user"
|
|
|
1002
1078
|
if (!params) return "user";
|
|
1003
1079
|
if (params.origin === "autonomy") return "autonomy";
|
|
1004
1080
|
const autonomy = params.autonomy;
|
|
1005
|
-
return autonomy && typeof autonomy === "object" && !Array.isArray(autonomy)
|
|
1006
|
-
? "autonomy"
|
|
1007
|
-
: "user";
|
|
1081
|
+
return autonomy && typeof autonomy === "object" && !Array.isArray(autonomy) ? "autonomy" : "user";
|
|
1008
1082
|
}
|
|
1009
1083
|
|
|
1010
1084
|
async function enqueueCompletion(
|
|
@@ -1109,15 +1183,19 @@ async function failActiveJobOnShutdown(
|
|
|
1109
1183
|
runtimeState.currentSessionId &&
|
|
1110
1184
|
shouldEmitDirectSessionJobEvent({ ok: false, statusPersistedToServer })
|
|
1111
1185
|
) {
|
|
1112
|
-
await transport.queueSessionCommand(
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1186
|
+
await transport.queueSessionCommand(
|
|
1187
|
+
runtimeState.currentSessionId,
|
|
1188
|
+
{
|
|
1189
|
+
type: "job_failed",
|
|
1190
|
+
payload: {
|
|
1191
|
+
jobId: activeJobId,
|
|
1192
|
+
message,
|
|
1193
|
+
detail,
|
|
1194
|
+
},
|
|
1195
|
+
from: `worker:${opts.workerId}`,
|
|
1118
1196
|
},
|
|
1119
|
-
|
|
1120
|
-
|
|
1197
|
+
{ priority: "high" },
|
|
1198
|
+
);
|
|
1121
1199
|
}
|
|
1122
1200
|
}
|
|
1123
1201
|
|
|
@@ -1224,10 +1302,7 @@ async function workerLoop(
|
|
|
1224
1302
|
const job = data.job;
|
|
1225
1303
|
|
|
1226
1304
|
if (job) {
|
|
1227
|
-
if (
|
|
1228
|
-
dockerExecutor &&
|
|
1229
|
-
dockerExecutor.shouldPrepareMergeConflictJobBeforeExecution(job)
|
|
1230
|
-
) {
|
|
1305
|
+
if (dockerExecutor && dockerExecutor.shouldPrepareMergeConflictJobBeforeExecution(job)) {
|
|
1231
1306
|
const deferMs = dockerExecutor.recommendedMergeConflictDeferMs();
|
|
1232
1307
|
const deferred = await deferClaimedJobForMaintenance(opts, headers, job.id, deferMs);
|
|
1233
1308
|
if (!deferred.ok) {
|
|
@@ -1325,50 +1400,95 @@ async function workerLoop(
|
|
|
1325
1400
|
}, heartbeatEveryMs);
|
|
1326
1401
|
|
|
1327
1402
|
if (job.sessionId) {
|
|
1328
|
-
await transport.queueSessionCommand(
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1403
|
+
await transport.queueSessionCommand(
|
|
1404
|
+
job.sessionId,
|
|
1405
|
+
{
|
|
1406
|
+
type: "job_claimed",
|
|
1407
|
+
payload: { jobId: job.id, workerId: opts.workerId },
|
|
1408
|
+
from: `worker:${opts.workerId}`,
|
|
1409
|
+
},
|
|
1410
|
+
{ priority: "high" },
|
|
1411
|
+
);
|
|
1333
1412
|
}
|
|
1334
1413
|
|
|
1335
1414
|
let stdoutSeq = 0;
|
|
1336
1415
|
let stderrSeq = 0;
|
|
1337
1416
|
let lastCleanLog = "";
|
|
1338
1417
|
let lastCleanLogAt = 0;
|
|
1418
|
+
let lastForwardedJobLogAt = Date.now();
|
|
1419
|
+
let currentJobPhase: WorkerJobPhase | null = null;
|
|
1339
1420
|
|
|
1340
|
-
const
|
|
1341
|
-
? (stream: "stdout" | "stderr", line: string) => {
|
|
1421
|
+
const emitJobLog = job.sessionId
|
|
1422
|
+
? (stream: "stdout" | "stderr", line: string): boolean => {
|
|
1342
1423
|
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}`);
|
|
1424
|
+
if (!cleaned) return false;
|
|
1346
1425
|
|
|
1347
1426
|
// Drop high-frequency terminal progress redraw spam; keep meaningful lines.
|
|
1348
|
-
if (isNoisyProgressLine(cleaned)) return;
|
|
1427
|
+
if (isNoisyProgressLine(cleaned)) return false;
|
|
1349
1428
|
|
|
1350
1429
|
// Collapse very noisy duplicate lines emitted in tight loops.
|
|
1351
1430
|
const now = Date.now();
|
|
1352
|
-
if (cleaned === lastCleanLog && now - lastCleanLogAt < 1_000) return;
|
|
1431
|
+
if (cleaned === lastCleanLog && now - lastCleanLogAt < 1_000) return false;
|
|
1353
1432
|
lastCleanLog = cleaned;
|
|
1354
1433
|
lastCleanLogAt = now;
|
|
1434
|
+
lastForwardedJobLogAt = now;
|
|
1435
|
+
currentJobPhase = inferWorkerJobPhaseFromLogLine(cleaned) ?? currentJobPhase;
|
|
1355
1436
|
const logTs = new Date(now).toISOString();
|
|
1356
1437
|
|
|
1357
1438
|
const seq = stream === "stdout" ? ++stdoutSeq : ++stderrSeq;
|
|
1358
|
-
void transport.queueSessionCommand(
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1439
|
+
void transport.queueSessionCommand(
|
|
1440
|
+
job.sessionId,
|
|
1441
|
+
{
|
|
1442
|
+
type: "job_log",
|
|
1443
|
+
payload: {
|
|
1444
|
+
jobId: job.id,
|
|
1445
|
+
stream,
|
|
1446
|
+
seq,
|
|
1447
|
+
line: cleaned,
|
|
1448
|
+
ts: logTs,
|
|
1449
|
+
phase: currentJobPhase,
|
|
1450
|
+
},
|
|
1451
|
+
from: `worker:${opts.workerId}`,
|
|
1452
|
+
},
|
|
1453
|
+
{ droppable: true },
|
|
1454
|
+
);
|
|
1363
1455
|
void transport.queueJobLog(job.id, {
|
|
1364
1456
|
stream,
|
|
1365
1457
|
seq,
|
|
1366
1458
|
message: cleaned,
|
|
1367
1459
|
ts: logTs,
|
|
1368
1460
|
});
|
|
1461
|
+
return true;
|
|
1369
1462
|
}
|
|
1370
1463
|
: undefined;
|
|
1371
1464
|
|
|
1465
|
+
const onLog = emitJobLog
|
|
1466
|
+
? (stream: "stdout" | "stderr", line: string) => {
|
|
1467
|
+
const cleaned = sanitizeJobLogLine(line);
|
|
1468
|
+
if (LOG.isDebugEnabled() && cleaned) LOG.debug(`[${stream}] ${cleaned}`);
|
|
1469
|
+
emitJobLog(stream, line);
|
|
1470
|
+
}
|
|
1471
|
+
: undefined;
|
|
1472
|
+
|
|
1473
|
+
const jobClaimedAtMs = Date.now();
|
|
1474
|
+
const jobProgressLogEveryMs = resolveJobProgressLogEveryMs();
|
|
1475
|
+
const jobProgressTimer =
|
|
1476
|
+
emitJobLog && jobProgressLogEveryMs > 0
|
|
1477
|
+
? setInterval(() => {
|
|
1478
|
+
const now = Date.now();
|
|
1479
|
+
const quietForMs = Math.max(0, now - lastForwardedJobLogAt);
|
|
1480
|
+
if (quietForMs < jobProgressLogEveryMs) return;
|
|
1481
|
+
emitJobLog(
|
|
1482
|
+
"stdout",
|
|
1483
|
+
`[WorkerPals] Job ${job.id} still running after ${formatDurationMs(
|
|
1484
|
+
now - jobClaimedAtMs,
|
|
1485
|
+
)} (kind=${job.kind}, worker=${opts.workerId}, phase=${
|
|
1486
|
+
currentJobPhase ?? "unknown"
|
|
1487
|
+
}, quiet_for=${formatDurationMs(quietForMs)}).`,
|
|
1488
|
+
);
|
|
1489
|
+
}, jobProgressLogEveryMs)
|
|
1490
|
+
: null;
|
|
1491
|
+
|
|
1372
1492
|
let directWorktreePath: string | null = null;
|
|
1373
1493
|
let executionRepo = opts.repo;
|
|
1374
1494
|
let result: WorkerJobResult | null = null;
|
|
@@ -1611,11 +1731,15 @@ async function workerLoop(
|
|
|
1611
1731
|
durationMs: jobDurationMs,
|
|
1612
1732
|
phase: job.kind,
|
|
1613
1733
|
});
|
|
1614
|
-
const response = await postJsonWithTimeout(
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1734
|
+
const response = await postJsonWithTimeout(
|
|
1735
|
+
`${opts.server}/jobs/${job.id}/fail`,
|
|
1736
|
+
headers,
|
|
1737
|
+
{
|
|
1738
|
+
message: result.summary,
|
|
1739
|
+
detail: redactSensitiveText(result.stderr ?? ""),
|
|
1740
|
+
durationMs: jobDurationMs,
|
|
1741
|
+
},
|
|
1742
|
+
);
|
|
1619
1743
|
statusPersistedToServer = response.ok;
|
|
1620
1744
|
console.log(
|
|
1621
1745
|
`[WorkerPals] Job ${job.id} failed in ${formatDurationMs(jobDurationMs)}: ${result.summary}`,
|
|
@@ -1703,6 +1827,7 @@ async function workerLoop(
|
|
|
1703
1827
|
}
|
|
1704
1828
|
} finally {
|
|
1705
1829
|
clearInterval(busyHeartbeat);
|
|
1830
|
+
if (jobProgressTimer) clearInterval(jobProgressTimer);
|
|
1706
1831
|
if (recycleWorkerAfterJob) {
|
|
1707
1832
|
runtimeState.shutdownRequested = true;
|
|
1708
1833
|
const forceExitTimer = setTimeout(() => {
|
|
@@ -1895,7 +2020,9 @@ async function main(): Promise<void> {
|
|
|
1895
2020
|
},
|
|
1896
2021
|
}),
|
|
1897
2022
|
);
|
|
1898
|
-
await withTimeout(
|
|
2023
|
+
await withTimeout(
|
|
2024
|
+
failActiveJobOnShutdown(opts, headers, runtimeState, transport, signalName),
|
|
2025
|
+
);
|
|
1899
2026
|
await withTimeout(transport.flush());
|
|
1900
2027
|
if (dockerExecutor) {
|
|
1901
2028
|
await withTimeout(
|