@pushpalsdev/cli 1.1.14 → 1.1.16
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 +57 -3
- package/package.json +1 -1
- package/runtime/sandbox/.pushpals-remotebuddy-fallback.js +15 -12
- package/runtime/sandbox/apps/workerpals/src/common/generic_python_executor.ts +60 -5
- package/runtime/sandbox/apps/workerpals/src/docker_executor.ts +91 -15
- package/runtime/sandbox/apps/workerpals/src/execute_job.ts +117 -8
- package/runtime/sandbox/apps/workerpals/src/job_runner.ts +11 -2
- package/runtime/sandbox/apps/workerpals/src/workerpals_main.ts +18 -2
package/dist/pushpals-cli.js
CHANGED
|
@@ -1637,6 +1637,7 @@ var DEFAULT_RUNTIME_BOOT_TIMEOUT_MS = 90000;
|
|
|
1637
1637
|
var DEFAULT_RUNTIME_BOOT_POLL_MS = 1000;
|
|
1638
1638
|
var DEFAULT_SERVER_BOOT_TIMEOUT_MS = 20000;
|
|
1639
1639
|
var DEFAULT_SERVICE_STABILITY_GRACE_MS = 4000;
|
|
1640
|
+
var DEFAULT_REMOTEBUDDY_CONSUMER_STARTUP_GRACE_MS = 8000;
|
|
1640
1641
|
var DEFAULT_COMMAND_OUTPUT_DRAIN_TIMEOUT_MS = 2000;
|
|
1641
1642
|
var DEFAULT_COMMAND_OUTPUT_MAX_CHARS = 512000;
|
|
1642
1643
|
var DEFAULT_REMOTEBUDDY_SILENT_STARTUP_FALLBACK_MS = 20000;
|
|
@@ -1648,6 +1649,7 @@ var DEFAULT_EMBEDDED_SERVICE_LAUNCH_WARN_MS = 5000;
|
|
|
1648
1649
|
var EMBEDDED_SERVICE_RESTART_MAX_ATTEMPTS = 4;
|
|
1649
1650
|
var WORKERPAL_STARTUP_READINESS_PROBE_MAX_MS = 15000;
|
|
1650
1651
|
var CLI_SESSION_JOB_LOG_MAX_CHARS = 700;
|
|
1652
|
+
var CLI_SESSION_SHOW_JOB_EVENTS_ENV = "PUSHPALS_CLI_SHOW_JOB_EVENTS";
|
|
1651
1653
|
var EMBEDDED_RUNTIME_SAFETY_CAP_DISABLE_ENV = "PUSHPALS_DISABLE_EMBEDDED_SAFETY_CAPS";
|
|
1652
1654
|
var EMBEDDED_RUNTIME_WINDOWS_SAFETY_CAPS = {
|
|
1653
1655
|
REMOTEBUDDY_WORKERPAL_STARTUP_TIMEOUT_MS: "120000",
|
|
@@ -1674,6 +1676,12 @@ function formatTimestampedCliLine(line, at = new Date) {
|
|
|
1674
1676
|
}
|
|
1675
1677
|
return `[${at.toISOString()}]${text}`;
|
|
1676
1678
|
}
|
|
1679
|
+
function isTruthyCliEnvValue(value) {
|
|
1680
|
+
return /^(1|true|yes|on)$/i.test(String(value ?? "").trim());
|
|
1681
|
+
}
|
|
1682
|
+
function shouldShowCliSessionOperationalEvents(env = process.env) {
|
|
1683
|
+
return isTruthyCliEnvValue(env[CLI_SESSION_SHOW_JOB_EVENTS_ENV]);
|
|
1684
|
+
}
|
|
1677
1685
|
function formatRuntimeStartupTimingSummary(input) {
|
|
1678
1686
|
const phaseSummary = input.phases.map((phase) => `${phase.name}=${Math.max(0, Math.floor(phase.durationMs))}ms(${phase.status.trim() || "unknown"})`).join(" ");
|
|
1679
1687
|
const detail = typeof input.detail === "string" && input.detail.trim() ? ` detail=${input.detail.trim()}` : "";
|
|
@@ -3443,7 +3451,7 @@ function isWorkerpalEphemeralWorktreePath(repoRoot, worktreePath) {
|
|
|
3443
3451
|
if (!normalizedPath.startsWith(expectedPrefix))
|
|
3444
3452
|
return false;
|
|
3445
3453
|
const leaf = basename(normalizedPath);
|
|
3446
|
-
return /^(job|selfcheck)
|
|
3454
|
+
return /^(job|selfcheck)-[a-z0-9][a-z0-9._-]*$/i.test(leaf);
|
|
3447
3455
|
}
|
|
3448
3456
|
function resolveConfiguredDockerExecutable(env, platform = process.platform) {
|
|
3449
3457
|
const configured = String(env.PUSHPALS_DOCKER_BIN_ABSOLUTE ?? env.PUSHPALS_DOCKER_BIN ?? (platform === "win32" ? "docker.exe" : "docker")).trim();
|
|
@@ -4241,6 +4249,16 @@ function extractRemoteBuddySessionConsumerHealth(statusPayload, sessionId) {
|
|
|
4241
4249
|
detail: `No connected RemoteBuddy session consumer found for session ${sessionId}`
|
|
4242
4250
|
};
|
|
4243
4251
|
}
|
|
4252
|
+
function shouldDeferRemoteBuddySessionConsumerReadiness(opts) {
|
|
4253
|
+
if (opts.localBuddyEnabled)
|
|
4254
|
+
return false;
|
|
4255
|
+
if (opts.remoteBuddyReady)
|
|
4256
|
+
return false;
|
|
4257
|
+
if (!opts.remoteBuddyServiceRunning)
|
|
4258
|
+
return false;
|
|
4259
|
+
const startupGraceMs = Math.max(0, opts.startupGraceMs ?? DEFAULT_REMOTEBUDDY_CONSUMER_STARTUP_GRACE_MS);
|
|
4260
|
+
return opts.readinessElapsedMs >= startupGraceMs;
|
|
4261
|
+
}
|
|
4244
4262
|
async function probeRemoteBuddySessionConsumer(serverUrl, sessionId) {
|
|
4245
4263
|
try {
|
|
4246
4264
|
const response = await fetchWithTimeout(`${serverUrl}/system/status`, {}, 1e4);
|
|
@@ -4709,6 +4727,7 @@ ${tail}` : ""}`);
|
|
|
4709
4727
|
const optionalServiceExitWarned = new Set;
|
|
4710
4728
|
let lastReadinessWaitLogAt = 0;
|
|
4711
4729
|
let lastReadinessWaitDetail = "";
|
|
4730
|
+
let deferredRemoteBuddyConsumerLogged = false;
|
|
4712
4731
|
while (Date.now() < deadline) {
|
|
4713
4732
|
reportRemoteBuddyAutonomousEngineState();
|
|
4714
4733
|
if (maybeActivateRemoteBuddyWindowsFallback("silent_startup")) {
|
|
@@ -4752,7 +4771,15 @@ ${tail}` : ""}`);
|
|
|
4752
4771
|
}
|
|
4753
4772
|
const health = localBuddyEnabled ? await probeLocalBuddy(opts.localAgentUrl) : null;
|
|
4754
4773
|
const remoteBuddyHealth2 = await probeRemoteBuddySessionConsumer(opts.serverUrl, opts.sessionId);
|
|
4755
|
-
|
|
4774
|
+
const remoteBuddyServiceRunning = serviceManager.getServices().some((service) => service.name === "remotebuddy" && !service.exited);
|
|
4775
|
+
const deferRemoteBuddyConsumer = shouldDeferRemoteBuddySessionConsumerReadiness({
|
|
4776
|
+
localBuddyEnabled,
|
|
4777
|
+
remoteBuddyReady: remoteBuddyHealth2.ok,
|
|
4778
|
+
remoteBuddyServiceRunning,
|
|
4779
|
+
readinessElapsedMs: Date.now() - readinessPhaseStartedAt
|
|
4780
|
+
});
|
|
4781
|
+
const remoteBuddyReadyForCli = remoteBuddyHealth2.ok || deferRemoteBuddyConsumer;
|
|
4782
|
+
if (localBuddyEnabled && !health?.ok || !remoteBuddyReadyForCli) {
|
|
4756
4783
|
const localBuddyDetail = localBuddyEnabled ? health?.ok ? "LocalBuddy ready" : "LocalBuddy not ready" : "LocalBuddy skipped";
|
|
4757
4784
|
const readinessDetail = `${localBuddyDetail}; ${remoteBuddyHealth2.detail}`;
|
|
4758
4785
|
const now = Date.now();
|
|
@@ -4763,7 +4790,11 @@ ${tail}` : ""}`);
|
|
|
4763
4790
|
lastReadinessWaitLogAt = now;
|
|
4764
4791
|
}
|
|
4765
4792
|
}
|
|
4766
|
-
if ((!localBuddyEnabled || health?.ok) &&
|
|
4793
|
+
if ((!localBuddyEnabled || health?.ok) && remoteBuddyReadyForCli) {
|
|
4794
|
+
if (deferRemoteBuddyConsumer && !deferredRemoteBuddyConsumerLogged) {
|
|
4795
|
+
appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] continuing startup after ${Date.now() - readinessPhaseStartedAt}ms without a connected RemoteBuddy session consumer; embedded RemoteBuddy is running and the CLI session will connect after startup (${remoteBuddyHealth2.detail}).`);
|
|
4796
|
+
deferredRemoteBuddyConsumerLogged = true;
|
|
4797
|
+
}
|
|
4767
4798
|
reportRemoteBuddyAutonomousEngineState();
|
|
4768
4799
|
const stabilityDeadline = Date.now() + DEFAULT_SERVICE_STABILITY_GRACE_MS;
|
|
4769
4800
|
while (Date.now() < stabilityDeadline) {
|
|
@@ -5218,7 +5249,10 @@ function formatSessionEventLine(event) {
|
|
|
5218
5249
|
const type = String(event.type ?? "").toLowerCase();
|
|
5219
5250
|
const from = String(event.from ?? "");
|
|
5220
5251
|
const payload = event.payload ?? {};
|
|
5252
|
+
const showOperationalEvents = shouldShowCliSessionOperationalEvents();
|
|
5221
5253
|
if (type === "job_enqueued") {
|
|
5254
|
+
if (!showOperationalEvents)
|
|
5255
|
+
return null;
|
|
5222
5256
|
const jobId = String(payload.jobId ?? "").slice(0, 8);
|
|
5223
5257
|
const kind = String(payload.kind ?? "").trim();
|
|
5224
5258
|
const taskId = String(payload.taskId ?? "").slice(0, 8);
|
|
@@ -5226,11 +5260,15 @@ function formatSessionEventLine(event) {
|
|
|
5226
5260
|
return `[job ${jobId}] queued: ${detail}`;
|
|
5227
5261
|
}
|
|
5228
5262
|
if (type === "job_claimed") {
|
|
5263
|
+
if (!showOperationalEvents)
|
|
5264
|
+
return null;
|
|
5229
5265
|
const jobId = String(payload.jobId ?? "").slice(0, 8);
|
|
5230
5266
|
const workerId = String(payload.workerId ?? "").trim();
|
|
5231
5267
|
return `[job ${jobId}] claimed${workerId ? ` by ${workerId}` : ""}`;
|
|
5232
5268
|
}
|
|
5233
5269
|
if (type === "job_log") {
|
|
5270
|
+
if (!showOperationalEvents)
|
|
5271
|
+
return null;
|
|
5234
5272
|
const jobId = String(payload.jobId ?? "").slice(0, 8);
|
|
5235
5273
|
const stream = String(payload.stream ?? "").toLowerCase() === "stderr" ? " stderr" : "";
|
|
5236
5274
|
const phase = compactCliSessionJobLogLine(String(payload.phase ?? "").trim());
|
|
@@ -5239,6 +5277,8 @@ function formatSessionEventLine(event) {
|
|
|
5239
5277
|
return line ? `[job ${jobId}${stream}${phaseLabel}] ${line}` : null;
|
|
5240
5278
|
}
|
|
5241
5279
|
if (type === "job_failed") {
|
|
5280
|
+
if (!showOperationalEvents)
|
|
5281
|
+
return null;
|
|
5242
5282
|
const jobId = String(payload.jobId ?? "").slice(0, 8);
|
|
5243
5283
|
const message = String(payload.message ?? "").trim();
|
|
5244
5284
|
return `[job ${jobId}] failed: ${message || "unknown"}`;
|
|
@@ -5251,24 +5291,34 @@ function formatSessionEventLine(event) {
|
|
|
5251
5291
|
const text = String(payload.text ?? "").trim();
|
|
5252
5292
|
if (!text)
|
|
5253
5293
|
return null;
|
|
5294
|
+
if (/^All systems online\b/i.test(text))
|
|
5295
|
+
return null;
|
|
5254
5296
|
return `assistant> ${text}`;
|
|
5255
5297
|
}
|
|
5256
5298
|
if (type === "task_progress") {
|
|
5299
|
+
if (!showOperationalEvents)
|
|
5300
|
+
return null;
|
|
5257
5301
|
const taskId = String(payload.taskId ?? "").slice(0, 8);
|
|
5258
5302
|
const message = String(payload.message ?? "").trim();
|
|
5259
5303
|
return message ? `[task ${taskId}] ${message}` : null;
|
|
5260
5304
|
}
|
|
5261
5305
|
if (type === "task_failed") {
|
|
5306
|
+
if (!showOperationalEvents)
|
|
5307
|
+
return null;
|
|
5262
5308
|
const taskId = String(payload.taskId ?? "").slice(0, 8);
|
|
5263
5309
|
const message = String(payload.message ?? "").trim();
|
|
5264
5310
|
return `[task ${taskId}] failed: ${message || "unknown"}`;
|
|
5265
5311
|
}
|
|
5266
5312
|
if (type === "task_completed") {
|
|
5313
|
+
if (!showOperationalEvents)
|
|
5314
|
+
return null;
|
|
5267
5315
|
const taskId = String(payload.taskId ?? "").slice(0, 8);
|
|
5268
5316
|
const summary = String(payload.summary ?? "").trim();
|
|
5269
5317
|
return `[task ${taskId}] completed${summary ? `: ${summary}` : ""}`;
|
|
5270
5318
|
}
|
|
5271
5319
|
if (type === "job_completed") {
|
|
5320
|
+
if (!showOperationalEvents)
|
|
5321
|
+
return null;
|
|
5272
5322
|
const jobId = String(payload.jobId ?? "").slice(0, 8);
|
|
5273
5323
|
const summary = String(payload.summary ?? "").trim();
|
|
5274
5324
|
return `[job ${jobId}] completed${summary ? `: ${summary}` : ""}`;
|
|
@@ -5277,6 +5327,8 @@ function formatSessionEventLine(event) {
|
|
|
5277
5327
|
return null;
|
|
5278
5328
|
}
|
|
5279
5329
|
if (type === "status") {
|
|
5330
|
+
if (!showOperationalEvents)
|
|
5331
|
+
return null;
|
|
5280
5332
|
const state = String(payload.state ?? "").trim();
|
|
5281
5333
|
const detail = String(payload.detail ?? "").trim();
|
|
5282
5334
|
const source = from || String(payload.agentId ?? "status");
|
|
@@ -6009,8 +6061,10 @@ export {
|
|
|
6009
6061
|
startEmbeddedMonitoringHub,
|
|
6010
6062
|
shutdownEmbeddedServiceManagerGracefully,
|
|
6011
6063
|
shouldUseRemoteBuddySilentStartupFallback,
|
|
6064
|
+
shouldShowCliSessionOperationalEvents,
|
|
6012
6065
|
shouldRunEmbeddedRuntimeStartupPrechecks,
|
|
6013
6066
|
shouldRestartEmbeddedService,
|
|
6067
|
+
shouldDeferRemoteBuddySessionConsumerReadiness,
|
|
6014
6068
|
runCommandWithEnv,
|
|
6015
6069
|
resolveWorkerpalDockerProbe,
|
|
6016
6070
|
resolveWorkerExecutionReadiness,
|
package/package.json
CHANGED
|
@@ -4276,6 +4276,9 @@ var IDEATION_SYSTEM_PROMPT = loadPromptTemplate("remotebuddy/autonomy_ideation_s
|
|
|
4276
4276
|
var SCORING_SYSTEM_PROMPT = loadPromptTemplate("remotebuddy/autonomy_scoring_system_prompt.md").trim();
|
|
4277
4277
|
var PLANNING_SYSTEM_PROMPT = loadPromptTemplate("remotebuddy/autonomy_planning_system_prompt.md").trim();
|
|
4278
4278
|
var IDEATION_TIMEOUT_RECOVERY_INSTRUCTION = "Previous ideation timed out before you returned JSON. For this round only, stay within the time budget: prioritize the top 1-3 highest-confidence candidates, keep reasoning brief, avoid exhaustive exploration, and return valid JSON as soon as possible.";
|
|
4279
|
+
var IDEATION_NORMAL_MAX_TOKENS = 1800;
|
|
4280
|
+
var IDEATION_RETRY_MAX_TOKENS = 900;
|
|
4281
|
+
var IDEATION_NORMAL_MAX_CANDIDATES = 5;
|
|
4279
4282
|
var STARTUP_FAST_TICK_MAX_ATTEMPTS = 4;
|
|
4280
4283
|
var STARTUP_FAST_TICK_MAX_DELAY_MS = 15000;
|
|
4281
4284
|
var STARTUP_STALE_LOCK_AFTER_MS = 30000;
|
|
@@ -7371,17 +7374,17 @@ ${JSON.stringify(input.messages ?? [])}`),
|
|
|
7371
7374
|
this.setPhase("ideation");
|
|
7372
7375
|
const buildIdeationInput = (ideationRecovery2, compactRetry) => {
|
|
7373
7376
|
const reduced = compactRetry || Boolean(ideationRecovery2);
|
|
7374
|
-
const ideationTopSignals = snapshot.top_signals.slice(0, reduced ? 5 :
|
|
7375
|
-
const ideationStateTraits = snapshot.state_traits.slice(0, reduced ? 6 :
|
|
7376
|
-
const ideationFeedbackPriors = snapshot.feedback_priors.slice(0, reduced ? 4 :
|
|
7377
|
-
const ideationEngineIdeaPriors = (snapshot.engine_idea_priors ?? []).slice(0, reduced ? 4 :
|
|
7378
|
-
const ideationOpenObjectives = snapshot.open_objectives.slice(0, reduced ? 4 :
|
|
7379
|
-
const ideationActiveCooldowns = snapshot.active_cooldowns.slice(0, reduced ? 4 :
|
|
7380
|
-
const ideationRepoTargets = repoTargets.slice(0, reduced ? 4 :
|
|
7377
|
+
const ideationTopSignals = snapshot.top_signals.slice(0, reduced ? 5 : 10);
|
|
7378
|
+
const ideationStateTraits = snapshot.state_traits.slice(0, reduced ? 6 : 12);
|
|
7379
|
+
const ideationFeedbackPriors = snapshot.feedback_priors.slice(0, reduced ? 4 : 8);
|
|
7380
|
+
const ideationEngineIdeaPriors = (snapshot.engine_idea_priors ?? []).slice(0, reduced ? 4 : 8);
|
|
7381
|
+
const ideationOpenObjectives = snapshot.open_objectives.slice(0, reduced ? 4 : 8);
|
|
7382
|
+
const ideationActiveCooldowns = snapshot.active_cooldowns.slice(0, reduced ? 4 : 8);
|
|
7383
|
+
const ideationRepoTargets = repoTargets.slice(0, reduced ? 4 : 8);
|
|
7381
7384
|
return {
|
|
7382
7385
|
system: IDEATION_SYSTEM_PROMPT,
|
|
7383
7386
|
json: true,
|
|
7384
|
-
maxTokens: reduced ?
|
|
7387
|
+
maxTokens: reduced ? IDEATION_RETRY_MAX_TOKENS : IDEATION_NORMAL_MAX_TOKENS,
|
|
7385
7388
|
temperature: 0.2,
|
|
7386
7389
|
messages: [
|
|
7387
7390
|
...ideationRecovery2 ? [
|
|
@@ -7402,7 +7405,7 @@ ${JSON.stringify(input.messages ?? [])}`),
|
|
|
7402
7405
|
open_objectives: ideationOpenObjectives,
|
|
7403
7406
|
active_cooldowns: ideationActiveCooldowns
|
|
7404
7407
|
},
|
|
7405
|
-
vision:
|
|
7408
|
+
vision: compactVisionContextForIdeationRetry(visionContext),
|
|
7406
7409
|
repo_targets: ideationRepoTargets.map((target) => ({
|
|
7407
7410
|
component_area: target.component_area,
|
|
7408
7411
|
target_paths: target.target_paths,
|
|
@@ -7410,12 +7413,12 @@ ${JSON.stringify(input.messages ?? [])}`),
|
|
|
7410
7413
|
label: target.label,
|
|
7411
7414
|
keywords: target.keywords.slice(0, reduced ? 4 : 8)
|
|
7412
7415
|
})),
|
|
7413
|
-
engine_inspiration:
|
|
7416
|
+
engine_inspiration: compactEngineInspirationForIdeationRetry(engineInspiration),
|
|
7414
7417
|
limits: {
|
|
7415
|
-
ideation_max_candidates: reduced ? Math.max(1, Math.min(3, this.cfg.ideationMaxCandidates)) : this.cfg.ideationMaxCandidates,
|
|
7418
|
+
ideation_max_candidates: reduced ? Math.max(1, Math.min(3, this.cfg.ideationMaxCandidates)) : Math.max(1, Math.min(IDEATION_NORMAL_MAX_CANDIDATES, this.cfg.ideationMaxCandidates)),
|
|
7416
7419
|
min_confidence: this.cfg.minConfidence
|
|
7417
7420
|
}
|
|
7418
|
-
}, null,
|
|
7421
|
+
}, null, 0)
|
|
7419
7422
|
}
|
|
7420
7423
|
]
|
|
7421
7424
|
};
|
|
@@ -204,6 +204,50 @@ function formatGenericPythonExecutorTimeoutDetail(
|
|
|
204
204
|
return `${configPath}=${configuredTimeoutMs}ms within planning executionBudgetMs=${executionBudgetMs}ms`;
|
|
205
205
|
}
|
|
206
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
|
+
|
|
207
251
|
export function createGenericPythonExecutor(
|
|
208
252
|
config: GenericPythonExecutorConfig,
|
|
209
253
|
): BackendTaskExecutor {
|
|
@@ -390,16 +434,27 @@ export function createGenericPythonExecutor(
|
|
|
390
434
|
parsed.usage,
|
|
391
435
|
estimateJobTokenUsage(backendName, modelId, params, summary, parsedStdout, parsedStderr),
|
|
392
436
|
);
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
437
|
+
const normalized = normalizeGenericPythonExecutorParsedResultForTimeout({
|
|
438
|
+
backendName,
|
|
439
|
+
kind,
|
|
440
|
+
timedOut,
|
|
441
|
+
timeoutMs,
|
|
442
|
+
timeoutDetail,
|
|
396
443
|
summary,
|
|
397
|
-
stdout:
|
|
398
|
-
stderr:
|
|
444
|
+
stdout: parsedStdout,
|
|
445
|
+
stderr: parsedStderr,
|
|
399
446
|
exitCode:
|
|
400
447
|
typeof parsed.exitCode === "number" && Number.isFinite(parsed.exitCode)
|
|
401
448
|
? parsed.exitCode
|
|
402
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,
|
|
403
458
|
usage,
|
|
404
459
|
};
|
|
405
460
|
} catch (err) {
|
|
@@ -428,13 +428,20 @@ export class DockerExecutor {
|
|
|
428
428
|
|
|
429
429
|
// Step 3: Run Docker container with the worktree mounted
|
|
430
430
|
for (let attempt = 1; attempt <= this.jobRetryMaxAttempts; attempt++) {
|
|
431
|
+
const attemptStartedAtMs = Date.now();
|
|
431
432
|
try {
|
|
432
433
|
this.logExecutionConfig();
|
|
433
434
|
const result = await this.runInWarmContainer(worktreePath, base64Spec, job, onLog);
|
|
434
435
|
if (result.ok) return result;
|
|
435
436
|
|
|
436
437
|
const retryableFailure = this.isRetryableJobFailure(result);
|
|
437
|
-
|
|
438
|
+
const attemptElapsedMs = Math.max(1, Date.now() - attemptStartedAtMs);
|
|
439
|
+
const timeoutMs = resolveDockerJobTimeoutMs(this.options.timeoutMs, job);
|
|
440
|
+
const hasBudgetForRetry =
|
|
441
|
+
retryableFailure &&
|
|
442
|
+
attempt < this.jobRetryMaxAttempts &&
|
|
443
|
+
this.hasBudgetForJobRetry(attempt, attemptElapsedMs, timeoutMs, onLog);
|
|
444
|
+
if (attempt >= this.jobRetryMaxAttempts || !retryableFailure || !hasBudgetForRetry) {
|
|
438
445
|
if (
|
|
439
446
|
retryableFailure &&
|
|
440
447
|
attempt >= this.jobRetryMaxAttempts &&
|
|
@@ -458,7 +465,13 @@ export class DockerExecutor {
|
|
|
458
465
|
await this.sleep(retryInMs);
|
|
459
466
|
} catch (err) {
|
|
460
467
|
const retryableError = this.isRetryableError(err);
|
|
461
|
-
|
|
468
|
+
const attemptElapsedMs = Math.max(1, Date.now() - attemptStartedAtMs);
|
|
469
|
+
const timeoutMs = resolveDockerJobTimeoutMs(this.options.timeoutMs, job);
|
|
470
|
+
const hasBudgetForRetry =
|
|
471
|
+
retryableError &&
|
|
472
|
+
attempt < this.jobRetryMaxAttempts &&
|
|
473
|
+
this.hasBudgetForJobRetry(attempt, attemptElapsedMs, timeoutMs, onLog);
|
|
474
|
+
if (attempt >= this.jobRetryMaxAttempts || !retryableError || !hasBudgetForRetry) {
|
|
462
475
|
if (
|
|
463
476
|
retryableError &&
|
|
464
477
|
attempt >= this.jobRetryMaxAttempts &&
|
|
@@ -1208,17 +1221,28 @@ export class DockerExecutor {
|
|
|
1208
1221
|
"bun",
|
|
1209
1222
|
"run",
|
|
1210
1223
|
"/workspace/apps/workerpals/src/job_runner.ts",
|
|
1211
|
-
|
|
1224
|
+
"--spec-stdin",
|
|
1212
1225
|
];
|
|
1213
1226
|
|
|
1214
1227
|
console.log(
|
|
1215
1228
|
`[DockerExecutor] Running job in warm container: ${this.warmContainerName} (${this.executionConfigSummary()})`,
|
|
1216
1229
|
);
|
|
1217
1230
|
|
|
1218
|
-
const
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1231
|
+
const dockerArgv = [resolveDockerExecutable(), ...args];
|
|
1232
|
+
let proc: ReturnType<typeof Bun.spawn>;
|
|
1233
|
+
try {
|
|
1234
|
+
proc = Bun.spawn(dockerArgv, {
|
|
1235
|
+
stdin: "pipe",
|
|
1236
|
+
stdout: "pipe",
|
|
1237
|
+
stderr: "pipe",
|
|
1238
|
+
});
|
|
1239
|
+
} catch (err) {
|
|
1240
|
+
throw new Error(
|
|
1241
|
+
`failed to spawn warm-container docker exec (${this.warmContainerName}, cwd=${containerWorktreePath}, argv_chars=${dockerArgv.join("\u0000").length}, spec_chars=${base64Spec.length}): ${this.compactError(
|
|
1242
|
+
err,
|
|
1243
|
+
)}`,
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
1222
1246
|
const timeoutMs = resolveDockerJobTimeoutMs(this.options.timeoutMs, job);
|
|
1223
1247
|
if (timeoutMs !== this.options.timeoutMs) {
|
|
1224
1248
|
const verb = timeoutMs > this.options.timeoutMs ? "Extended" : "Capped";
|
|
@@ -1263,10 +1287,24 @@ export class DockerExecutor {
|
|
|
1263
1287
|
const stdoutLines: string[] = [];
|
|
1264
1288
|
const stderrLines: string[] = [];
|
|
1265
1289
|
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1290
|
+
try {
|
|
1291
|
+
await Promise.all([
|
|
1292
|
+
this.writeJobSpecToStdin(proc, base64Spec),
|
|
1293
|
+
this.readStream(proc.stdout, "stdout", onLog, stdoutLines),
|
|
1294
|
+
this.readStream(proc.stderr, "stderr", onLog, stderrLines),
|
|
1295
|
+
]);
|
|
1296
|
+
} catch (err) {
|
|
1297
|
+
try {
|
|
1298
|
+
proc.kill();
|
|
1299
|
+
} catch {
|
|
1300
|
+
// Ignore cleanup errors after stream setup failures.
|
|
1301
|
+
}
|
|
1302
|
+
throw new Error(
|
|
1303
|
+
`failed while streaming warm-container job execution (${this.warmContainerName}, spec_chars=${base64Spec.length}): ${this.compactError(
|
|
1304
|
+
err,
|
|
1305
|
+
)}`,
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1270
1308
|
|
|
1271
1309
|
clearTimeout(warningTimer);
|
|
1272
1310
|
clearTimeout(timer);
|
|
@@ -1283,6 +1321,28 @@ export class DockerExecutor {
|
|
|
1283
1321
|
return result;
|
|
1284
1322
|
}
|
|
1285
1323
|
|
|
1324
|
+
private async writeJobSpecToStdin(
|
|
1325
|
+
proc: ReturnType<typeof Bun.spawn>,
|
|
1326
|
+
base64Spec: string,
|
|
1327
|
+
): Promise<void> {
|
|
1328
|
+
const stdin = proc.stdin as WritableStream<Uint8Array> | undefined;
|
|
1329
|
+
if (!stdin) {
|
|
1330
|
+
throw new Error("docker exec stdin pipe was not available");
|
|
1331
|
+
}
|
|
1332
|
+
const writer = stdin.getWriter();
|
|
1333
|
+
try {
|
|
1334
|
+
await writer.write(new TextEncoder().encode(base64Spec));
|
|
1335
|
+
await writer.close();
|
|
1336
|
+
} catch (err) {
|
|
1337
|
+
try {
|
|
1338
|
+
await writer.abort(err);
|
|
1339
|
+
} catch {
|
|
1340
|
+
// Ignore abort failures; the original write error is more useful.
|
|
1341
|
+
}
|
|
1342
|
+
throw err;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1286
1346
|
private async ensureWorktreeDependencyArtifacts(
|
|
1287
1347
|
containerWorktreePath: string,
|
|
1288
1348
|
onLog?: (stream: "stdout" | "stderr", line: string) => void,
|
|
@@ -1785,6 +1845,23 @@ export class DockerExecutor {
|
|
|
1785
1845
|
return transientPatterns.some((pattern) => pattern.test(text));
|
|
1786
1846
|
}
|
|
1787
1847
|
|
|
1848
|
+
private hasBudgetForJobRetry(
|
|
1849
|
+
attempt: number,
|
|
1850
|
+
attemptElapsedMs: number,
|
|
1851
|
+
timeoutMs: number,
|
|
1852
|
+
onLog?: (stream: "stdout" | "stderr", line: string) => void,
|
|
1853
|
+
): boolean {
|
|
1854
|
+
if (attempt >= this.jobRetryMaxAttempts) return false;
|
|
1855
|
+
const consumedRatio = timeoutMs > 0 ? attemptElapsedMs / timeoutMs : 1;
|
|
1856
|
+
if (attemptElapsedMs < Math.max(300_000, timeoutMs * 0.8) && consumedRatio < 0.8) return true;
|
|
1857
|
+
const note = `[DockerExecutor] Skipping retry attempt ${
|
|
1858
|
+
attempt + 1
|
|
1859
|
+
}/${this.jobRetryMaxAttempts}: prior attempt consumed ${attemptElapsedMs}ms of ${timeoutMs}ms budget.`;
|
|
1860
|
+
console.warn(note);
|
|
1861
|
+
onLog?.("stderr", note);
|
|
1862
|
+
return false;
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1788
1865
|
/**
|
|
1789
1866
|
* Convert Windows path to Docker-compatible path
|
|
1790
1867
|
* C:\foo\bar → /c/foo/bar
|
|
@@ -1839,10 +1916,9 @@ export class DockerExecutor {
|
|
|
1839
1916
|
}
|
|
1840
1917
|
|
|
1841
1918
|
private buildEphemeralWorktreeName(prefix: "job" | "selfcheck", token: string): string {
|
|
1842
|
-
const
|
|
1843
|
-
const
|
|
1844
|
-
|
|
1845
|
-
return `${prefix}-${safeToken}-${safeWorker}-${nonce}`;
|
|
1919
|
+
const safeToken = this.sanitizeWorktreeToken(token, prefix === "job" ? 8 : 12);
|
|
1920
|
+
const nonce = `${Date.now().toString(36).slice(-6)}-${randomUUID().slice(0, 6).toLowerCase()}`;
|
|
1921
|
+
return `${prefix}-${safeToken}-${nonce}`;
|
|
1846
1922
|
}
|
|
1847
1923
|
|
|
1848
1924
|
private sanitizeWorktreeToken(value: string, maxLength: number): string {
|
|
@@ -191,6 +191,34 @@ export function qualityRevisionLoopUpperBound(policy: {
|
|
|
191
191
|
);
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
export function qualityRevisionBudgetDecision(opts: {
|
|
195
|
+
jobElapsedMs: number;
|
|
196
|
+
executionBudgetMs: number;
|
|
197
|
+
}): {
|
|
198
|
+
shouldStart: boolean;
|
|
199
|
+
remainingBudgetMs: number;
|
|
200
|
+
minimumRevisionBudgetMs: number;
|
|
201
|
+
} {
|
|
202
|
+
const executionBudgetMs = Number(opts.executionBudgetMs);
|
|
203
|
+
if (!Number.isFinite(executionBudgetMs) || executionBudgetMs <= 0) {
|
|
204
|
+
return {
|
|
205
|
+
shouldStart: true,
|
|
206
|
+
remainingBudgetMs: Number.POSITIVE_INFINITY,
|
|
207
|
+
minimumRevisionBudgetMs: 0,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
const elapsedMs = Math.max(0, Number(opts.jobElapsedMs) || 0);
|
|
211
|
+
const remainingBudgetMs = Math.max(0, Math.floor(executionBudgetMs - elapsedMs));
|
|
212
|
+
const minimumRevisionBudgetMs = Math.floor(
|
|
213
|
+
Math.min(executionBudgetMs, Math.max(180_000, Math.min(600_000, executionBudgetMs * 0.35))),
|
|
214
|
+
);
|
|
215
|
+
return {
|
|
216
|
+
shouldStart: remainingBudgetMs >= minimumRevisionBudgetMs,
|
|
217
|
+
remainingBudgetMs,
|
|
218
|
+
minimumRevisionBudgetMs,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
194
222
|
function taskRequestsBrowserValidation(params: Record<string, unknown>): boolean {
|
|
195
223
|
const candidates: string[] = [];
|
|
196
224
|
const collect = (value: unknown) => {
|
|
@@ -384,6 +412,11 @@ function isNonPublishableArtifactPath(path: string): boolean {
|
|
|
384
412
|
);
|
|
385
413
|
}
|
|
386
414
|
|
|
415
|
+
function isNestedNodeModulesChange(path: string): boolean {
|
|
416
|
+
const normalized = path.replace(/\\/g, "/").replace(/^\.?\//, "").replace(/\/+$/, "");
|
|
417
|
+
return /(^|\/)node_modules\/.+/i.test(normalized);
|
|
418
|
+
}
|
|
419
|
+
|
|
387
420
|
export function publishableChangedPaths(changedPaths: string[]): string[] {
|
|
388
421
|
return changedPaths.filter((path) => !isNonPublishableArtifactPath(path));
|
|
389
422
|
}
|
|
@@ -1578,6 +1611,45 @@ function pathMatchesScopeHint(path: string, hint: string): boolean {
|
|
|
1578
1611
|
return normalizedPath === prefix || normalizedPath.startsWith(`${prefix}/`);
|
|
1579
1612
|
}
|
|
1580
1613
|
|
|
1614
|
+
function isValidationScopeTestPathHint(path: string): boolean {
|
|
1615
|
+
const normalized = path.trim().replace(/\\/g, "/").replace(/^\.\/+/, "");
|
|
1616
|
+
return /(^|\/)(__tests__|tests?)(\/|$)|\.(test|spec)\.[cm]?[jt]sx?$/i.test(normalized);
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
function shouldTreatBrowserAssertionAsTaskScope(
|
|
1620
|
+
planning: TaskExecutePlanning,
|
|
1621
|
+
changedPaths: string[],
|
|
1622
|
+
targetPath?: string,
|
|
1623
|
+
): boolean {
|
|
1624
|
+
const pathHints = [
|
|
1625
|
+
targetPath ?? "",
|
|
1626
|
+
...changedPaths,
|
|
1627
|
+
...(planning.targetPaths ?? []),
|
|
1628
|
+
...(planning.scope.writeGlobs ?? []),
|
|
1629
|
+
]
|
|
1630
|
+
.map((entry) => entry.trim().replace(/\\/g, "/"))
|
|
1631
|
+
.filter(Boolean);
|
|
1632
|
+
const allHintsAreTests =
|
|
1633
|
+
pathHints.length > 0 && pathHints.every((hint) => isValidationScopeTestPathHint(hint));
|
|
1634
|
+
const planningText = collectPlanningText(planning);
|
|
1635
|
+
const explicitlyBrowserValidation =
|
|
1636
|
+
/\b(browser|web:e2e|e2e|playwright|smoke)\b/i.test(planningText);
|
|
1637
|
+
if (allHintsAreTests && !explicitlyBrowserValidation) return false;
|
|
1638
|
+
|
|
1639
|
+
const productPathChanged = changedPaths.some((path) => {
|
|
1640
|
+
const normalized = path.trim().replace(/\\/g, "/").replace(/^\.\/+/, "");
|
|
1641
|
+
return (
|
|
1642
|
+
!isValidationScopeTestPathHint(normalized) &&
|
|
1643
|
+
/^(app|components|screens|styles|utils)\//i.test(normalized)
|
|
1644
|
+
);
|
|
1645
|
+
});
|
|
1646
|
+
if (productPathChanged) return true;
|
|
1647
|
+
|
|
1648
|
+
return /\b(ui|visual|render(?:ing)?|style|screen|route|home|settings|shop|game|battlefield|component|control panel|control-panel)\b/i.test(
|
|
1649
|
+
planningText,
|
|
1650
|
+
);
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1581
1653
|
export function classifyValidationFailureScope(
|
|
1582
1654
|
runs: ValidationExecutionResult[],
|
|
1583
1655
|
planning: TaskExecutePlanning,
|
|
@@ -1600,12 +1672,14 @@ export function classifyValidationFailureScope(
|
|
|
1600
1672
|
.flatMap((run) => [run.stdout, run.stderr])
|
|
1601
1673
|
.filter(Boolean)
|
|
1602
1674
|
.join("\n");
|
|
1675
|
+
const hasBrowserAssertionFailure = failedRuns.some(
|
|
1676
|
+
(run) =>
|
|
1677
|
+
isLongRunningBrowserValidationCommand(run.command) &&
|
|
1678
|
+
isBrowserAssertionDigest([run.stdout, run.stderr].filter(Boolean).join("\n")),
|
|
1679
|
+
);
|
|
1603
1680
|
if (
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
isLongRunningBrowserValidationCommand(run.command) &&
|
|
1607
|
-
isBrowserAssertionDigest([run.stdout, run.stderr].filter(Boolean).join("\n")),
|
|
1608
|
-
)
|
|
1681
|
+
hasBrowserAssertionFailure &&
|
|
1682
|
+
shouldTreatBrowserAssertionAsTaskScope(planning, changedPaths, targetPath)
|
|
1609
1683
|
) {
|
|
1610
1684
|
return "task_scope";
|
|
1611
1685
|
}
|
|
@@ -1620,7 +1694,9 @@ export function classifyValidationFailureScope(
|
|
|
1620
1694
|
const pathTokens = extractPathTokensFromValidationOutput(combined).filter(
|
|
1621
1695
|
(token) => !/^(node_modules|\.bun|bun|npm|pnpm|yarn)\//i.test(token),
|
|
1622
1696
|
);
|
|
1623
|
-
if (pathTokens.length === 0)
|
|
1697
|
+
if (pathTokens.length === 0) {
|
|
1698
|
+
return hasBrowserAssertionFailure ? "outside_task_scope" : "none";
|
|
1699
|
+
}
|
|
1624
1700
|
if (pathTokens.some((token) => scopeHints.some((hint) => pathMatchesScopeHint(token, hint)))) {
|
|
1625
1701
|
return "task_scope";
|
|
1626
1702
|
}
|
|
@@ -2901,7 +2977,7 @@ export function collectPrePublishHygieneIssues(params: {
|
|
|
2901
2977
|
}
|
|
2902
2978
|
}
|
|
2903
2979
|
|
|
2904
|
-
if (changedPaths.some((path) =>
|
|
2980
|
+
if (changedPaths.some((path) => isNestedNodeModulesChange(path))) {
|
|
2905
2981
|
issues.push("attempted to publish node_modules changes; dependency installs must not become PR content.");
|
|
2906
2982
|
}
|
|
2907
2983
|
|
|
@@ -6741,6 +6817,7 @@ export async function executeJob(
|
|
|
6741
6817
|
|
|
6742
6818
|
let revisionAttempt = 0;
|
|
6743
6819
|
let revisionHint = "";
|
|
6820
|
+
const jobStartedAt = Date.now();
|
|
6744
6821
|
const previousValidationFailureDigests = new Map<string, string>();
|
|
6745
6822
|
const failureJobFamily = buildTaskFailureJobFamily(normalizedParams);
|
|
6746
6823
|
while (revisionAttempt <= qualityRevisionLoopMax) {
|
|
@@ -6854,7 +6931,7 @@ export async function executeJob(
|
|
|
6854
6931
|
);
|
|
6855
6932
|
return {
|
|
6856
6933
|
ok: false,
|
|
6857
|
-
summary:
|
|
6934
|
+
summary: `Executor produced no publishable code changes (${detail})`,
|
|
6858
6935
|
stdout: result.stdout,
|
|
6859
6936
|
stderr: [result.stderr ?? "", detail].filter(Boolean).join("\n"),
|
|
6860
6937
|
exitCode: 4,
|
|
@@ -7180,6 +7257,38 @@ export async function executeJob(
|
|
|
7180
7257
|
};
|
|
7181
7258
|
}
|
|
7182
7259
|
|
|
7260
|
+
const revisionBudget = qualityRevisionBudgetDecision({
|
|
7261
|
+
jobElapsedMs: Date.now() - jobStartedAt,
|
|
7262
|
+
executionBudgetMs,
|
|
7263
|
+
});
|
|
7264
|
+
if (!revisionBudget.shouldStart) {
|
|
7265
|
+
const budgetSummary = `Quality gate needs revision ${
|
|
7266
|
+
revisionAttempt + 1
|
|
7267
|
+
}/${activeMaxAutoRevisions}, but remaining execution budget is ${
|
|
7268
|
+
revisionBudget.remainingBudgetMs
|
|
7269
|
+
}ms (< ${revisionBudget.minimumRevisionBudgetMs}ms); stopping before another worker turn to preserve a structured result: ${toSingleLine(
|
|
7270
|
+
issueSummary,
|
|
7271
|
+
220,
|
|
7272
|
+
)}`;
|
|
7273
|
+
onLog?.("stderr", `[QualityGate] ${budgetSummary}`);
|
|
7274
|
+
return {
|
|
7275
|
+
ok: false,
|
|
7276
|
+
summary: budgetSummary,
|
|
7277
|
+
stdout: result.stdout,
|
|
7278
|
+
stderr: truncate(
|
|
7279
|
+
[
|
|
7280
|
+
result.stderr ?? "",
|
|
7281
|
+
...quality.validationRuns.flatMap((run) => [run.stdout, run.stderr]).filter(Boolean),
|
|
7282
|
+
critic ? `Critic raw: ${critic.raw}` : "",
|
|
7283
|
+
]
|
|
7284
|
+
.filter(Boolean)
|
|
7285
|
+
.join("\n"),
|
|
7286
|
+
outputPolicyForRuntime(runtimeConfig),
|
|
7287
|
+
),
|
|
7288
|
+
exitCode: 4,
|
|
7289
|
+
};
|
|
7290
|
+
}
|
|
7291
|
+
|
|
7183
7292
|
revisionAttempt += 1;
|
|
7184
7293
|
revisionHint = buildQualityRevisionHint(
|
|
7185
7294
|
issues,
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Usage (inside container):
|
|
9
9
|
* bun run job_runner.ts <base64-encoded-job-spec>
|
|
10
|
+
* bun run job_runner.ts --spec-stdin
|
|
10
11
|
*
|
|
11
12
|
* The job spec is base64-encoded JSON: { jobId, taskId, kind, params, workerId }
|
|
12
13
|
*
|
|
@@ -116,11 +117,19 @@ echo "password=${token}"
|
|
|
116
117
|
|
|
117
118
|
async function main(): Promise<void> {
|
|
118
119
|
const args = process.argv.slice(2);
|
|
119
|
-
const
|
|
120
|
+
const rawSpecArg = args[0];
|
|
120
121
|
|
|
122
|
+
if (!rawSpecArg) {
|
|
123
|
+
// eslint-disable-next-line no-console
|
|
124
|
+
console.error("Usage: bun run job_runner.ts <base64-encoded-job-spec>|--spec-stdin");
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const base64Spec =
|
|
129
|
+
rawSpecArg === "--spec-stdin" ? (await Bun.stdin.text()).trim() : rawSpecArg;
|
|
121
130
|
if (!base64Spec) {
|
|
122
131
|
// eslint-disable-next-line no-console
|
|
123
|
-
console.error("
|
|
132
|
+
console.error("Job spec was empty");
|
|
124
133
|
process.exit(1);
|
|
125
134
|
}
|
|
126
135
|
|
|
@@ -100,6 +100,14 @@ function estimateTokensFromText(text: string): number {
|
|
|
100
100
|
return Math.max(0, Math.ceil(String(text ?? "").length / 3));
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
function compactWorkerError(error: unknown, maxLength = 220): string {
|
|
104
|
+
const raw = error instanceof Error ? error.message : String(error ?? "");
|
|
105
|
+
const normalized = raw.replace(/\s+/g, " ").trim();
|
|
106
|
+
if (!normalized) return "unknown error";
|
|
107
|
+
if (normalized.length <= maxLength) return normalized;
|
|
108
|
+
return `${normalized.slice(0, maxLength - 3)}...`;
|
|
109
|
+
}
|
|
110
|
+
|
|
103
111
|
async function postJsonWithTimeout(
|
|
104
112
|
url: string,
|
|
105
113
|
headers: Record<string, string>,
|
|
@@ -693,10 +701,17 @@ async function createIsolatedWorktree(
|
|
|
693
701
|
): Promise<string> {
|
|
694
702
|
const worktreeRoot = resolve(repo, ".worktrees");
|
|
695
703
|
mkdirSync(worktreeRoot, { recursive: true });
|
|
704
|
+
const safeJobId = jobId
|
|
705
|
+
.toLowerCase()
|
|
706
|
+
.replace(/[^a-z0-9]+/g, "")
|
|
707
|
+
.slice(0, 8);
|
|
708
|
+
const nonce = `${Date.now().toString(36).slice(-6)}-${Math.random()
|
|
709
|
+
.toString(36)
|
|
710
|
+
.slice(2, 6)}`;
|
|
696
711
|
|
|
697
712
|
const worktreePath = resolve(
|
|
698
713
|
worktreeRoot,
|
|
699
|
-
`
|
|
714
|
+
`job-${safeJobId || "host"}-${nonce}`,
|
|
700
715
|
);
|
|
701
716
|
|
|
702
717
|
const addResult = await git(repo, ["worktree", "add", "--detach", worktreePath, baseRef]);
|
|
@@ -1532,9 +1547,10 @@ async function workerLoop(
|
|
|
1532
1547
|
Number.isFinite(err.cooldownMs) ? err.cooldownMs : 0,
|
|
1533
1548
|
);
|
|
1534
1549
|
}
|
|
1550
|
+
const errorSummary = compactWorkerError(err);
|
|
1535
1551
|
result = {
|
|
1536
1552
|
ok: false,
|
|
1537
|
-
summary:
|
|
1553
|
+
summary: `Job execution failed before completion: ${errorSummary}`,
|
|
1538
1554
|
stderr: String(err),
|
|
1539
1555
|
...(cooldownAfterJobMs > 0 ? { cooldownMs: cooldownAfterJobMs } : {}),
|
|
1540
1556
|
};
|