@pushpalsdev/cli 1.0.33 → 1.0.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
|
@@ -236,6 +236,7 @@ export class DockerExecutor {
|
|
|
236
236
|
private readonly jobRetryMaxAttempts: number;
|
|
237
237
|
private readonly jobRetryBackoffMs: number;
|
|
238
238
|
private readonly failureCooldownMs: number;
|
|
239
|
+
private readonly worktreeVisibilityTimeoutMs: number;
|
|
239
240
|
private lastLoggedExecutionConfig = "";
|
|
240
241
|
private lastLoggedEndpointRewrite = "";
|
|
241
242
|
private warmedBackends = new Set<string>();
|
|
@@ -294,6 +295,7 @@ export class DockerExecutor {
|
|
|
294
295
|
20_000,
|
|
295
296
|
300_000,
|
|
296
297
|
);
|
|
298
|
+
this.worktreeVisibilityTimeoutMs = process.platform === "win32" ? 15_000 : 5_000;
|
|
297
299
|
|
|
298
300
|
// Ensure worktrees directory exists
|
|
299
301
|
try {
|
|
@@ -416,7 +418,10 @@ export class DockerExecutor {
|
|
|
416
418
|
try {
|
|
417
419
|
await this.createWorktree(worktreePath, this.options.baseRef);
|
|
418
420
|
await this.runGitSelfCheckContainer(worktreePath);
|
|
419
|
-
|
|
421
|
+
await this.ensureWorktreeAccessibleInWarmContainer(worktreePath);
|
|
422
|
+
console.log(
|
|
423
|
+
`[DockerExecutor] Startup self-check passed (git/worktree in container and warm container).`,
|
|
424
|
+
);
|
|
420
425
|
} finally {
|
|
421
426
|
await this.removeWorktree(worktreePath).catch(() => {
|
|
422
427
|
// Ignore cleanup failures for startup self-check artifacts.
|
|
@@ -837,6 +842,41 @@ export class DockerExecutor {
|
|
|
837
842
|
};
|
|
838
843
|
}
|
|
839
844
|
|
|
845
|
+
private async runWarmWorktreeProbe(containerWorktreePath: string): Promise<{
|
|
846
|
+
ok: boolean;
|
|
847
|
+
stdout: string;
|
|
848
|
+
stderr: string;
|
|
849
|
+
exitCode: number;
|
|
850
|
+
}> {
|
|
851
|
+
const proc = Bun.spawn(
|
|
852
|
+
[
|
|
853
|
+
resolveDockerExecutable(),
|
|
854
|
+
"exec",
|
|
855
|
+
"-w",
|
|
856
|
+
containerWorktreePath,
|
|
857
|
+
this.warmContainerName,
|
|
858
|
+
"/bin/sh",
|
|
859
|
+
"-lc",
|
|
860
|
+
"git rev-parse --is-inside-work-tree && git rev-parse --git-dir",
|
|
861
|
+
],
|
|
862
|
+
{
|
|
863
|
+
stdout: "pipe",
|
|
864
|
+
stderr: "pipe",
|
|
865
|
+
},
|
|
866
|
+
);
|
|
867
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
868
|
+
new Response(proc.stdout).text(),
|
|
869
|
+
new Response(proc.stderr).text(),
|
|
870
|
+
proc.exited,
|
|
871
|
+
]);
|
|
872
|
+
return {
|
|
873
|
+
ok: exitCode === 0,
|
|
874
|
+
stdout: stdout.trim(),
|
|
875
|
+
stderr: stderr.trim(),
|
|
876
|
+
exitCode,
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
|
|
840
880
|
private async inspectWarmContainerState(): Promise<string> {
|
|
841
881
|
const proc = Bun.spawn(
|
|
842
882
|
[
|
|
@@ -1054,10 +1094,10 @@ export class DockerExecutor {
|
|
|
1054
1094
|
): Promise<DockerJobResult> {
|
|
1055
1095
|
await this.ensureWarmRuntimeReady(job, onLog);
|
|
1056
1096
|
const startedAtMs = Date.now();
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1097
|
+
const containerWorktreePath = await this.ensureWorktreeAccessibleInWarmContainer(
|
|
1098
|
+
worktreePath,
|
|
1099
|
+
onLog,
|
|
1100
|
+
);
|
|
1061
1101
|
|
|
1062
1102
|
const args: string[] = [
|
|
1063
1103
|
"exec",
|
|
@@ -1154,6 +1194,51 @@ export class DockerExecutor {
|
|
|
1154
1194
|
);
|
|
1155
1195
|
}
|
|
1156
1196
|
|
|
1197
|
+
private async ensureWorktreeAccessibleInWarmContainer(
|
|
1198
|
+
worktreePath: string,
|
|
1199
|
+
onLog?: (stream: "stdout" | "stderr", line: string) => void,
|
|
1200
|
+
): Promise<string> {
|
|
1201
|
+
const worktreeRelPath = relative(this.options.repo, worktreePath).replace(/\\/g, "/");
|
|
1202
|
+
const containerWorktreePath = `/repo/${worktreeRelPath}`;
|
|
1203
|
+
let lastError: unknown = null;
|
|
1204
|
+
|
|
1205
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
1206
|
+
try {
|
|
1207
|
+
await this.ensureWarmContainer();
|
|
1208
|
+
await this.waitForWorktreePathInWarmContainer(
|
|
1209
|
+
containerWorktreePath,
|
|
1210
|
+
this.worktreeVisibilityTimeoutMs,
|
|
1211
|
+
);
|
|
1212
|
+
const probe = await this.runWarmWorktreeProbe(containerWorktreePath);
|
|
1213
|
+
if (probe.ok) {
|
|
1214
|
+
return containerWorktreePath;
|
|
1215
|
+
}
|
|
1216
|
+
const detail = [probe.stderr, probe.stdout].filter(Boolean).join("\n").trim();
|
|
1217
|
+
throw new Error(
|
|
1218
|
+
`warm container git probe failed (exit ${probe.exitCode})${detail ? `: ${detail}` : ""}`,
|
|
1219
|
+
);
|
|
1220
|
+
} catch (err) {
|
|
1221
|
+
lastError = err;
|
|
1222
|
+
if (attempt >= 2) {
|
|
1223
|
+
const diagnostics = await this.inspectWarmContainerState().catch(() => "");
|
|
1224
|
+
throw new Error(
|
|
1225
|
+
`worktree not accessible inside warm container after ${attempt} attempts: ${containerWorktreePath}${
|
|
1226
|
+
lastError ? ` (${this.compactError(lastError)})` : ""
|
|
1227
|
+
}${diagnostics ? ` | container=${diagnostics}` : ""}`,
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
const note =
|
|
1231
|
+
`[DockerExecutor] Warm container could not access worktree ${containerWorktreePath}; ` +
|
|
1232
|
+
`recycling container and retrying once (${this.compactError(err)}).`;
|
|
1233
|
+
console.warn(note);
|
|
1234
|
+
onLog?.("stderr", note);
|
|
1235
|
+
await this.stopWarmContainer("worktree visibility retry", true);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
return containerWorktreePath;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1157
1242
|
private normalizeProvider(raw: string): string {
|
|
1158
1243
|
const value = raw.trim().toLowerCase();
|
|
1159
1244
|
if (!value) return "auto";
|
|
@@ -215,6 +215,14 @@ function isNoisyProgressLine(line: string): boolean {
|
|
|
215
215
|
return /^(📦 Installing \[\d+\/\d+\]|🔍 Resolving\.\.\.|🔒 Saving lockfile\.\.\.)$/.test(line);
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
export function shouldEmitDirectSessionJobEvent(options: {
|
|
219
|
+
ok: boolean;
|
|
220
|
+
statusPersistedToServer: boolean;
|
|
221
|
+
}): boolean {
|
|
222
|
+
if (options.ok) return true;
|
|
223
|
+
return !options.statusPersistedToServer;
|
|
224
|
+
}
|
|
225
|
+
|
|
218
226
|
function shouldRecycleWorkerForCodexUnavailableFailure(
|
|
219
227
|
summary: string,
|
|
220
228
|
stderr?: string | null,
|
|
@@ -950,13 +958,15 @@ async function failActiveJobOnShutdown(
|
|
|
950
958
|
|
|
951
959
|
const message = "Worker process shutting down during claimed job";
|
|
952
960
|
const detail = `worker=${opts.workerId}; signal=${signalName}; action=fail-claimed-job-on-shutdown`;
|
|
961
|
+
let statusPersistedToServer = false;
|
|
953
962
|
|
|
954
963
|
try {
|
|
955
|
-
await fetch(`${opts.server}/jobs/${activeJobId}/fail`, {
|
|
964
|
+
const response = await fetch(`${opts.server}/jobs/${activeJobId}/fail`, {
|
|
956
965
|
method: "POST",
|
|
957
966
|
headers,
|
|
958
967
|
body: JSON.stringify({ message, detail }),
|
|
959
968
|
});
|
|
969
|
+
statusPersistedToServer = response.ok;
|
|
960
970
|
} catch (err) {
|
|
961
971
|
console.error(
|
|
962
972
|
`[WorkerPals] Failed to mark active job ${activeJobId} as failed during shutdown:`,
|
|
@@ -964,7 +974,10 @@ async function failActiveJobOnShutdown(
|
|
|
964
974
|
);
|
|
965
975
|
}
|
|
966
976
|
|
|
967
|
-
if (
|
|
977
|
+
if (
|
|
978
|
+
runtimeState.currentSessionId &&
|
|
979
|
+
shouldEmitDirectSessionJobEvent({ ok: false, statusPersistedToServer })
|
|
980
|
+
) {
|
|
968
981
|
await sendCommand(opts.server, runtimeState.currentSessionId, headers, {
|
|
969
982
|
type: "job_failed",
|
|
970
983
|
payload: {
|
|
@@ -1242,6 +1255,7 @@ async function workerLoop(
|
|
|
1242
1255
|
}
|
|
1243
1256
|
}
|
|
1244
1257
|
|
|
1258
|
+
let statusPersistedToServer = false;
|
|
1245
1259
|
if (result.ok) {
|
|
1246
1260
|
const reviewAgent =
|
|
1247
1261
|
parsedParams.reviewAgent && typeof parsedParams.reviewAgent === "object"
|
|
@@ -1253,7 +1267,7 @@ async function workerLoop(
|
|
|
1253
1267
|
reviewAgent.prUrl.trim().length > 0
|
|
1254
1268
|
? reviewAgent.prUrl.trim()
|
|
1255
1269
|
: null;
|
|
1256
|
-
await fetch(`${opts.server}/jobs/${job.id}/complete`, {
|
|
1270
|
+
const response = await fetch(`${opts.server}/jobs/${job.id}/complete`, {
|
|
1257
1271
|
method: "POST",
|
|
1258
1272
|
headers,
|
|
1259
1273
|
body: JSON.stringify({
|
|
@@ -1266,11 +1280,12 @@ async function workerLoop(
|
|
|
1266
1280
|
],
|
|
1267
1281
|
}),
|
|
1268
1282
|
});
|
|
1283
|
+
statusPersistedToServer = response.ok;
|
|
1269
1284
|
console.log(
|
|
1270
1285
|
`[WorkerPals] Job ${job.id} completed in ${formatDurationMs(jobDurationMs)}: ${result.summary}`,
|
|
1271
1286
|
);
|
|
1272
1287
|
} else {
|
|
1273
|
-
await fetch(`${opts.server}/jobs/${job.id}/fail`, {
|
|
1288
|
+
const response = await fetch(`${opts.server}/jobs/${job.id}/fail`, {
|
|
1274
1289
|
method: "POST",
|
|
1275
1290
|
headers,
|
|
1276
1291
|
body: JSON.stringify({
|
|
@@ -1279,6 +1294,7 @@ async function workerLoop(
|
|
|
1279
1294
|
durationMs: jobDurationMs,
|
|
1280
1295
|
}),
|
|
1281
1296
|
});
|
|
1297
|
+
statusPersistedToServer = response.ok;
|
|
1282
1298
|
console.log(
|
|
1283
1299
|
`[WorkerPals] Job ${job.id} failed in ${formatDurationMs(jobDurationMs)}: ${result.summary}`,
|
|
1284
1300
|
);
|
|
@@ -1319,29 +1335,31 @@ async function workerLoop(
|
|
|
1319
1335
|
}
|
|
1320
1336
|
}
|
|
1321
1337
|
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1338
|
+
if (shouldEmitDirectSessionJobEvent({ ok: result.ok, statusPersistedToServer })) {
|
|
1339
|
+
const eventCmd = result.ok
|
|
1340
|
+
? {
|
|
1341
|
+
type: "job_completed" as const,
|
|
1342
|
+
payload: {
|
|
1343
|
+
jobId: job.id,
|
|
1344
|
+
summary: result.summary,
|
|
1345
|
+
artifacts: result.stdout
|
|
1346
|
+
? [{ kind: "log" as const, text: result.stdout }]
|
|
1347
|
+
: undefined,
|
|
1348
|
+
},
|
|
1349
|
+
from: `worker:${opts.workerId}`,
|
|
1350
|
+
}
|
|
1351
|
+
: {
|
|
1352
|
+
type: "job_failed" as const,
|
|
1353
|
+
payload: {
|
|
1354
|
+
jobId: job.id,
|
|
1355
|
+
message: result.summary,
|
|
1356
|
+
detail: redactSensitiveText(result.stderr ?? ""),
|
|
1357
|
+
},
|
|
1358
|
+
from: `worker:${opts.workerId}`,
|
|
1359
|
+
};
|
|
1343
1360
|
|
|
1344
|
-
|
|
1361
|
+
await sendCommand(opts.server, job.sessionId, headers, eventCmd);
|
|
1362
|
+
}
|
|
1345
1363
|
}
|
|
1346
1364
|
} finally {
|
|
1347
1365
|
clearInterval(busyHeartbeat);
|