@pushpalsdev/cli 1.0.39 → 1.0.41

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.
@@ -189,7 +189,7 @@ class ServiceManager {
189
189
  stderr: "ignore"
190
190
  });
191
191
  } else {
192
- service.proc.kill();
192
+ service.proc.kill("SIGKILL");
193
193
  }
194
194
  } catch {}
195
195
  }
@@ -1701,6 +1701,7 @@ function printUsage() {
1701
1701
  console.log(" --no-auto-start Disable runtime auto-start when the server is down");
1702
1702
  console.log(" --no-stream Disable live session event stream");
1703
1703
  console.log(" --runtime-only Start the local runtime and wait for shutdown without opening the interactive chat");
1704
+ console.log(" --status-once Print active endpoints once and exit");
1704
1705
  console.log(" --clear Remove repo-local PushPals state and exit");
1705
1706
  console.log(" -h, --help Show this help");
1706
1707
  console.log("");
@@ -1720,6 +1721,7 @@ function parseArgs(argv) {
1720
1721
  noAutoStart: false,
1721
1722
  noStream: false,
1722
1723
  runtimeOnly: false,
1724
+ statusOnce: false,
1723
1725
  clear: false
1724
1726
  };
1725
1727
  for (let i = 0;i < argv.length; i++) {
@@ -1740,6 +1742,10 @@ function parseArgs(argv) {
1740
1742
  options.runtimeOnly = true;
1741
1743
  continue;
1742
1744
  }
1745
+ if (arg === "--status-once") {
1746
+ options.statusOnce = true;
1747
+ continue;
1748
+ }
1743
1749
  if (arg === "--clear") {
1744
1750
  options.clear = true;
1745
1751
  continue;
@@ -1812,7 +1818,7 @@ function parsePositiveInt(value, fallback) {
1812
1818
  function jsonHtmlBootstrap(value) {
1813
1819
  return JSON.stringify(value).replace(/</g, "\\u003c");
1814
1820
  }
1815
- async function runCommandWithEnv(command, cwd, env) {
1821
+ async function runCommandWithEnv(command, cwd, env, timeoutMs) {
1816
1822
  try {
1817
1823
  const proc = Bun.spawn(command, {
1818
1824
  cwd,
@@ -1820,12 +1826,48 @@ async function runCommandWithEnv(command, cwd, env) {
1820
1826
  stdout: "pipe",
1821
1827
  stderr: "pipe"
1822
1828
  });
1829
+ let timedOut = false;
1830
+ let timer = null;
1831
+ if (typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0) {
1832
+ timer = setTimeout(() => {
1833
+ timedOut = true;
1834
+ try {
1835
+ const stopCommand = buildServiceStopCommand(proc.pid, process.platform);
1836
+ if (stopCommand) {
1837
+ Bun.spawnSync(stopCommand, {
1838
+ stdin: "ignore",
1839
+ stdout: "ignore",
1840
+ stderr: "ignore"
1841
+ });
1842
+ } else {
1843
+ proc.kill("SIGKILL");
1844
+ }
1845
+ } catch {}
1846
+ }, timeoutMs);
1847
+ }
1823
1848
  const [stdout, stderr, exitCode] = await Promise.all([
1824
1849
  new Response(proc.stdout).text(),
1825
1850
  new Response(proc.stderr).text(),
1826
1851
  proc.exited
1827
1852
  ]);
1828
- return { ok: exitCode === 0, stdout: stdout.trim(), stderr: stderr.trim(), exitCode };
1853
+ if (timer)
1854
+ clearTimeout(timer);
1855
+ const normalizedStdout = stdout.trim();
1856
+ const normalizedStderr = stderr.trim();
1857
+ if (timedOut) {
1858
+ return {
1859
+ ok: false,
1860
+ stdout: normalizedStdout,
1861
+ stderr: `timed out after ${timeoutMs}ms${normalizedStderr ? ` | ${normalizedStderr}` : ""}`,
1862
+ exitCode
1863
+ };
1864
+ }
1865
+ return {
1866
+ ok: exitCode === 0,
1867
+ stdout: normalizedStdout,
1868
+ stderr: normalizedStderr,
1869
+ exitCode
1870
+ };
1829
1871
  } catch (err) {
1830
1872
  return {
1831
1873
  ok: false,
@@ -2610,7 +2652,7 @@ function stopRuntimeServices(services) {
2610
2652
  stderr: "ignore"
2611
2653
  });
2612
2654
  } else {
2613
- service.proc.kill();
2655
+ service.proc.kill("SIGKILL");
2614
2656
  }
2615
2657
  } catch {}
2616
2658
  }
@@ -2661,6 +2703,34 @@ async function stopRuntimeServicesGracefully(services, timeoutMs = 1e4) {
2661
2703
  stopRuntimeServices(remaining);
2662
2704
  }
2663
2705
  }
2706
+ async function shutdownEmbeddedServiceManagerGracefully(options) {
2707
+ const {
2708
+ serviceManager,
2709
+ serverUrl,
2710
+ repoRoot,
2711
+ reason,
2712
+ requestShutdown = requestLocalRuntimeShutdown,
2713
+ shutdownAcceptedDelayMs = 1500,
2714
+ onLog = (line) => console.log(line),
2715
+ onWarn = (line) => console.warn(line),
2716
+ cleanupTasks = []
2717
+ } = options;
2718
+ serviceManager.beginShutdown();
2719
+ const services = serviceManager.getServices();
2720
+ const shutdown = await requestShutdown(serverUrl, repoRoot, reason);
2721
+ if (shutdown.attempted && shutdown.accepted) {
2722
+ onLog("[pushpals] Local runtime shutdown accepted; waiting for services to exit...");
2723
+ await Bun.sleep(Math.max(0, shutdownAcceptedDelayMs));
2724
+ } else if (shutdown.attempted) {
2725
+ onWarn(`[pushpals] Local runtime shutdown request was not accepted${shutdown.detail ? `: ${shutdown.detail}` : "."}`);
2726
+ } else if (shutdown.detail) {
2727
+ onWarn(`[pushpals] ${shutdown.detail}`);
2728
+ }
2729
+ await stopRuntimeServicesGracefully(services);
2730
+ for (const task of cleanupTasks) {
2731
+ await task();
2732
+ }
2733
+ }
2664
2734
  function prependExecutableDirToPath(env, executablePath, platform = process.platform) {
2665
2735
  const resolvedPath = String(executablePath ?? "").trim();
2666
2736
  if (!resolvedPath)
@@ -2930,6 +3000,7 @@ function resolveConfiguredDockerExecutable(env, platform = process.platform) {
2930
3000
  }
2931
3001
  async function cleanupLingeringWorkerpalWarmContainers(opts) {
2932
3002
  const runCommandWithEnvFn = opts.runCommandWithEnvFn ?? runCommandWithEnv;
3003
+ const commandTimeoutMs = typeof opts.commandTimeoutMs === "number" && Number.isFinite(opts.commandTimeoutMs) ? Math.max(1, Math.floor(opts.commandTimeoutMs)) : 5000;
2933
3004
  const dockerExecutable = resolveConfiguredDockerExecutable(opts.env, opts.platform ?? process.platform);
2934
3005
  const list = await runCommandWithEnvFn([
2935
3006
  dockerExecutable,
@@ -2939,7 +3010,7 @@ async function cleanupLingeringWorkerpalWarmContainers(opts) {
2939
3010
  `label=${WORKERPAL_WARM_COMPONENT_LABEL}`,
2940
3011
  "--filter",
2941
3012
  `label=pushpals.repo=${opts.repoRoot}`
2942
- ], opts.repoRoot, opts.env);
3013
+ ], opts.repoRoot, opts.env, commandTimeoutMs);
2943
3014
  if (!list.ok) {
2944
3015
  const detail = list.stderr || list.stdout || `exit ${list.exitCode}`;
2945
3016
  return {
@@ -2956,7 +3027,7 @@ async function cleanupLingeringWorkerpalWarmContainers(opts) {
2956
3027
  removed: 0
2957
3028
  };
2958
3029
  }
2959
- const remove = await runCommandWithEnvFn([dockerExecutable, "rm", "-f", ...containerIds], opts.repoRoot, opts.env);
3030
+ const remove = await runCommandWithEnvFn([dockerExecutable, "rm", "-f", ...containerIds], opts.repoRoot, opts.env, commandTimeoutMs);
2960
3031
  if (!remove.ok) {
2961
3032
  const detail = remove.stderr || remove.stdout || `exit ${remove.exitCode}`;
2962
3033
  return {
@@ -2974,7 +3045,8 @@ async function cleanupLingeringWorkerpalWarmContainers(opts) {
2974
3045
  async function cleanupLingeringPushPalsGitWorktrees(opts) {
2975
3046
  const runCommandWithEnvFn = opts.runCommandWithEnvFn ?? runCommandWithEnv;
2976
3047
  const forceDeleteWorktreePathFn = opts.forceDeleteWorktreePathFn ?? forceDeleteWorktreePath;
2977
- const list = await runCommandWithEnvFn(["git", "worktree", "list", "--porcelain"], opts.repoRoot, opts.env);
3048
+ const commandTimeoutMs = typeof opts.commandTimeoutMs === "number" && Number.isFinite(opts.commandTimeoutMs) ? Math.max(1, Math.floor(opts.commandTimeoutMs)) : 5000;
3049
+ const list = await runCommandWithEnvFn(["git", "worktree", "list", "--porcelain"], opts.repoRoot, opts.env, commandTimeoutMs);
2978
3050
  if (!list.ok) {
2979
3051
  const detail = list.stderr || list.stdout || `exit ${list.exitCode}`;
2980
3052
  return {
@@ -2996,7 +3068,7 @@ async function cleanupLingeringPushPalsGitWorktrees(opts) {
2996
3068
  let removed = 0;
2997
3069
  const failures = [];
2998
3070
  for (const entry of removable) {
2999
- const remove = await runCommandWithEnvFn(["git", "worktree", "remove", "--force", "--force", entry.path], opts.repoRoot, opts.env);
3071
+ const remove = await runCommandWithEnvFn(["git", "worktree", "remove", "--force", "--force", entry.path], opts.repoRoot, opts.env, commandTimeoutMs);
3000
3072
  if (remove.ok) {
3001
3073
  removed += 1;
3002
3074
  continue;
@@ -3009,7 +3081,7 @@ async function cleanupLingeringPushPalsGitWorktrees(opts) {
3009
3081
  const removeDetail = remove.stderr || remove.stdout || `exit ${remove.exitCode}`;
3010
3082
  failures.push(`${entry.path}: ${removeDetail}${forced.lastError ? ` | fallback: ${forced.lastError}` : ""}`);
3011
3083
  }
3012
- const prune = await runCommandWithEnvFn(["git", "worktree", "prune"], opts.repoRoot, opts.env);
3084
+ const prune = await runCommandWithEnvFn(["git", "worktree", "prune"], opts.repoRoot, opts.env, commandTimeoutMs);
3013
3085
  if (!prune.ok) {
3014
3086
  failures.push(`prune: ${prune.stderr || prune.stdout || `exit ${prune.exitCode}`}`);
3015
3087
  }
@@ -3018,13 +3090,13 @@ async function cleanupLingeringPushPalsGitWorktrees(opts) {
3018
3090
  "for-each-ref",
3019
3091
  "--format=%(refname:short)",
3020
3092
  `refs/heads/${SOURCE_CONTROL_MANAGER_TEMP_BRANCH_PREFIX}`
3021
- ], opts.repoRoot, opts.env);
3093
+ ], opts.repoRoot, opts.env, commandTimeoutMs);
3022
3094
  if (!deleteTempBranches.ok) {
3023
3095
  failures.push(`list temp branches: ${deleteTempBranches.stderr || deleteTempBranches.stdout || `exit ${deleteTempBranches.exitCode}`}`);
3024
3096
  } else {
3025
3097
  const branches = deleteTempBranches.stdout.split(/\r?\n/g).map((value) => value.trim()).filter(Boolean);
3026
3098
  for (const branch of branches) {
3027
- const deleteResult = await runCommandWithEnvFn(["git", "branch", "-D", branch], opts.repoRoot, opts.env);
3099
+ const deleteResult = await runCommandWithEnvFn(["git", "branch", "-D", branch], opts.repoRoot, opts.env, commandTimeoutMs);
3028
3100
  if (!deleteResult.ok) {
3029
3101
  failures.push(`${branch}: ${deleteResult.stderr || deleteResult.stdout || `exit ${deleteResult.exitCode}`}`);
3030
3102
  } else {
@@ -4799,21 +4871,16 @@ async function main() {
4799
4871
  return;
4800
4872
  const serviceManager = autoStartedServiceManager;
4801
4873
  autoStartedServiceManager = null;
4802
- serviceManager.beginShutdown();
4803
- const shutdown = await requestLocalRuntimeShutdown(serverUrl, repoRoot, reason);
4804
- if (shutdown.attempted && shutdown.accepted) {
4805
- console.log("[pushpals] Local runtime shutdown accepted; waiting for services to exit...");
4806
- await Bun.sleep(1500);
4807
- } else if (shutdown.attempted) {
4808
- console.warn(`[pushpals] Local runtime shutdown request was not accepted${shutdown.detail ? `: ${shutdown.detail}` : "."}`);
4809
- } else if (shutdown.detail) {
4810
- console.warn(`[pushpals] ${shutdown.detail}`);
4811
- }
4812
- serviceManager.stop();
4813
- const services = serviceManager.getServices();
4814
- await stopRuntimeServicesGracefully(services);
4815
- await cleanupWorkerpalWarmContainersIfNeeded("cli shutdown");
4816
- await cleanupPushPalsGitWorktreesIfNeeded("cli shutdown");
4874
+ await shutdownEmbeddedServiceManagerGracefully({
4875
+ serviceManager,
4876
+ serverUrl,
4877
+ repoRoot,
4878
+ reason,
4879
+ cleanupTasks: [
4880
+ () => cleanupWorkerpalWarmContainersIfNeeded("cli shutdown"),
4881
+ () => cleanupPushPalsGitWorktreesIfNeeded("cli shutdown")
4882
+ ]
4883
+ });
4817
4884
  };
4818
4885
  if (!serverHealthy && workerpalDockerPrecheck.status === "failed") {
4819
4886
  console.error(`[pushpals] Precheck failed: Docker-backed WorkerPal auto-spawn is required but Docker is unavailable (${workerpalDockerPrecheck.detail}).`);
@@ -5002,7 +5069,7 @@ ${line}
5002
5069
  }
5003
5070
  console.log(line);
5004
5071
  };
5005
- const streamTask = parsed.noStream ? Promise.resolve() : parsed.runtimeOnly ? Promise.resolve() : runSessionStream(serverUrl, activeSessionId, cliClient, printIncoming, streamAbort.signal);
5072
+ const streamTask = parsed.noStream ? Promise.resolve() : parsed.runtimeOnly || parsed.statusOnce ? Promise.resolve() : runSessionStream(serverUrl, activeSessionId, cliClient, printIncoming, streamAbort.signal);
5006
5073
  let stopPromise = null;
5007
5074
  const requestStop = () => {
5008
5075
  if (stopPromise)
@@ -5066,10 +5133,25 @@ ${line}
5066
5133
  await Promise.race([streamTask, Bun.sleep(2000)]);
5067
5134
  return;
5068
5135
  }
5136
+ const printStatusSnapshot = async () => {
5137
+ console.log(`[pushpals] serverUrl=${serverUrl}`);
5138
+ console.log(`[pushpals] sessionId=${activeSessionId}`);
5139
+ console.log(`[pushpals] repoRoot=${repoRoot}`);
5140
+ console.log(`[pushpals] pushpalsLog=${pushpalsLogPath ?? "unavailable"}`);
5141
+ console.log(monitoringHubUrl ? `[pushpals] monitoringHubUrl=${monitoringHubUrl}` : "[pushpals] monitoringHubUrl=unavailable");
5142
+ await reportWorkerExecutionReadiness();
5143
+ reportEmbeddedRuntimeHealth();
5144
+ };
5145
+ if (parsed.statusOnce) {
5146
+ await printStatusSnapshot();
5147
+ await requestStop();
5148
+ await Promise.race([streamTask, Bun.sleep(2000)]);
5149
+ return;
5150
+ }
5069
5151
  rl = createInterface({
5070
5152
  input: process.stdin,
5071
5153
  output: process.stdout,
5072
- terminal: true
5154
+ terminal: Boolean(process.stdin.isTTY && process.stdout.isTTY)
5073
5155
  });
5074
5156
  rl.setPrompt("you> ");
5075
5157
  rl.prompt();
@@ -5089,13 +5171,7 @@ ${line}
5089
5171
  continue;
5090
5172
  }
5091
5173
  if (text === "/status") {
5092
- console.log(`[pushpals] serverUrl=${serverUrl}`);
5093
- console.log(`[pushpals] sessionId=${activeSessionId}`);
5094
- console.log(`[pushpals] repoRoot=${repoRoot}`);
5095
- console.log(`[pushpals] pushpalsLog=${pushpalsLogPath ?? "unavailable"}`);
5096
- console.log(monitoringHubUrl ? `[pushpals] monitoringHubUrl=${monitoringHubUrl}` : "[pushpals] monitoringHubUrl=unavailable");
5097
- await reportWorkerExecutionReadiness();
5098
- reportEmbeddedRuntimeHealth();
5174
+ await printStatusSnapshot();
5099
5175
  rl.prompt();
5100
5176
  continue;
5101
5177
  }
@@ -5134,6 +5210,7 @@ if (import.meta.main) {
5134
5210
  export {
5135
5211
  waitForWorkerpalCapacity,
5136
5212
  startEmbeddedMonitoringHub,
5213
+ shutdownEmbeddedServiceManagerGracefully,
5137
5214
  shouldRunEmbeddedRuntimeStartupPrechecks,
5138
5215
  shouldRestartEmbeddedService,
5139
5216
  resolveWorkerExecutionReadiness,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushpalsdev/cli",
3
- "version": "1.0.39",
3
+ "version": "1.0.41",
4
4
  "description": "PushPals terminal CLI for LocalBuddy -> RemoteBuddy orchestration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -240,6 +240,7 @@ export class DockerExecutor {
240
240
  private lastLoggedExecutionConfig = "";
241
241
  private lastLoggedEndpointRewrite = "";
242
242
  private warmedBackends = new Set<string>();
243
+ private preparedMergeConflictJobs = new Set<string>();
243
244
  private mergeConflictRefreshPromise: Promise<void> | null = null;
244
245
  private readonly config: WorkerpalsRuntimeConfig;
245
246
 
@@ -318,7 +319,6 @@ export class DockerExecutor {
318
319
  const worktreePath = resolve(this.worktreeDir, worktreeName);
319
320
 
320
321
  try {
321
- await this.ensureFreshImageForMergeConflictJob(job, onLog);
322
322
  const worktreeBaseRef = await this.resolveWorktreeBaseRefForJob(job, onLog);
323
323
  // Step 1: Create isolated git worktree
324
324
  await this.createWorktree(worktreePath, worktreeBaseRef);
@@ -398,6 +398,7 @@ export class DockerExecutor {
398
398
  stderr: `Retries exhausted after ${this.jobRetryMaxAttempts} attempts`,
399
399
  };
400
400
  } finally {
401
+ this.preparedMergeConflictJobs.delete(job.id);
401
402
  this.activeJobs = Math.max(0, this.activeJobs - 1);
402
403
  // Step 4: Clean up worktree (always cleanup)
403
404
  await this.removeWorktree(worktreePath).catch((err) => {
@@ -1690,6 +1691,22 @@ export class DockerExecutor {
1690
1691
  return resolutionType === "merge_conflict";
1691
1692
  }
1692
1693
 
1694
+ shouldPrepareMergeConflictJobBeforeExecution(job: Job): boolean {
1695
+ return this.isMergeConflictResolutionJob(job) && !this.preparedMergeConflictJobs.has(job.id);
1696
+ }
1697
+
1698
+ async prepareMergeConflictJobEnvironment(
1699
+ job: Job,
1700
+ onLog?: (stream: "stdout" | "stderr", line: string) => void,
1701
+ ): Promise<void> {
1702
+ await this.ensureFreshImageForMergeConflictJob(job, onLog);
1703
+ this.preparedMergeConflictJobs.add(job.id);
1704
+ }
1705
+
1706
+ recommendedMergeConflictDeferMs(): number {
1707
+ return Math.max(60_000, Math.min(this.options.timeoutMs, 5 * 60_000));
1708
+ }
1709
+
1693
1710
  private async ensureFreshImageForMergeConflictJob(
1694
1711
  job: Job,
1695
1712
  onLog?: (stream: "stdout" | "stderr", line: string) => void,
@@ -965,6 +965,40 @@ async function failActiveJobOnShutdown(
965
965
  }
966
966
  }
967
967
 
968
+ async function deferClaimedJobForMaintenance(
969
+ opts: ReturnType<typeof parseArgs>,
970
+ headers: Record<string, string>,
971
+ jobId: string,
972
+ deferMs: number,
973
+ ): Promise<{ ok: boolean; availableAt?: string; message?: string }> {
974
+ try {
975
+ const response = await postJsonWithTimeout(`${opts.server}/jobs/${jobId}/defer`, headers, {
976
+ workerId: opts.workerId,
977
+ deferMs,
978
+ });
979
+ const payload = (await response.json().catch(() => ({}))) as {
980
+ ok?: boolean;
981
+ availableAt?: string;
982
+ message?: string;
983
+ };
984
+ if (!response.ok || !payload.ok) {
985
+ return {
986
+ ok: false,
987
+ message: payload.message || `HTTP ${response.status}`,
988
+ };
989
+ }
990
+ return {
991
+ ok: true,
992
+ availableAt: payload.availableAt,
993
+ };
994
+ } catch (error) {
995
+ return {
996
+ ok: false,
997
+ message: error instanceof Error ? error.message : String(error),
998
+ };
999
+ }
1000
+ }
1001
+
968
1002
  async function workerLoop(
969
1003
  opts: ReturnType<typeof parseArgs>,
970
1004
  dockerExecutor: DockerExecutor | null,
@@ -1034,6 +1068,83 @@ async function workerLoop(
1034
1068
  const job = data.job;
1035
1069
 
1036
1070
  if (job) {
1071
+ if (
1072
+ dockerExecutor &&
1073
+ dockerExecutor.shouldPrepareMergeConflictJobBeforeExecution(job)
1074
+ ) {
1075
+ const deferMs = dockerExecutor.recommendedMergeConflictDeferMs();
1076
+ const deferred = await deferClaimedJobForMaintenance(opts, headers, job.id, deferMs);
1077
+ if (!deferred.ok) {
1078
+ console.warn(
1079
+ `[WorkerPals] Failed to defer merge-conflict job ${job.id} for image refresh; falling back to claimed execution path: ${
1080
+ deferred.message || "unknown error"
1081
+ }`,
1082
+ );
1083
+ } else {
1084
+ console.log(
1085
+ `[WorkerPals] Deferred merge-conflict job ${job.id} until ${
1086
+ deferred.availableAt ?? "maintenance complete"
1087
+ } while refreshing Docker image outside claimed-job lifetime.`,
1088
+ );
1089
+ const maintenanceHeartbeat = setInterval(() => {
1090
+ void transport.sendHeartbeat({
1091
+ ...buildHeartbeatPayload("idle", null),
1092
+ details: {
1093
+ repo: opts.repo,
1094
+ baseRef: opts.worktreeBaseRef,
1095
+ dockerImage: opts.docker ? opts.dockerImage : null,
1096
+ dockerNetworkMode: opts.docker ? opts.dockerNetworkMode : null,
1097
+ maintenance: "merge_conflict_image_refresh",
1098
+ deferredJobId: job.id,
1099
+ },
1100
+ });
1101
+ }, heartbeatEveryMs);
1102
+ try {
1103
+ await maybeHeartbeat("idle", null, true);
1104
+ await dockerExecutor.prepareMergeConflictJobEnvironment(job);
1105
+ } catch (error) {
1106
+ const detail = redactSensitiveText(
1107
+ error instanceof Error ? error.stack || error.message : String(error),
1108
+ );
1109
+ console.error(
1110
+ `[WorkerPals] Merge-conflict environment preparation failed for ${job.id}: ${detail}`,
1111
+ );
1112
+ try {
1113
+ const failResponse = await postJsonWithTimeout(
1114
+ `${opts.server}/jobs/${job.id}/fail-deferred`,
1115
+ headers,
1116
+ {
1117
+ workerId: opts.workerId,
1118
+ message: "Merge-conflict environment preparation failed",
1119
+ detail,
1120
+ },
1121
+ );
1122
+ const failPayload = (await failResponse.json().catch(() => ({}))) as {
1123
+ ok?: boolean;
1124
+ message?: string;
1125
+ };
1126
+ if (!failResponse.ok || !failPayload.ok) {
1127
+ console.error(
1128
+ `[WorkerPals] Failed to mark deferred job ${job.id} as failed: ${
1129
+ failPayload.message || `HTTP ${failResponse.status}`
1130
+ }`,
1131
+ );
1132
+ }
1133
+ } catch (failErr) {
1134
+ console.error(
1135
+ `[WorkerPals] Failed to mark deferred job ${job.id} as failed: ${
1136
+ failErr instanceof Error ? failErr.message : String(failErr)
1137
+ }`,
1138
+ );
1139
+ }
1140
+ } finally {
1141
+ clearInterval(maintenanceHeartbeat);
1142
+ }
1143
+ await maybeHeartbeat("idle", null, true);
1144
+ continue;
1145
+ }
1146
+ }
1147
+
1037
1148
  runtimeState.currentJobId = job.id;
1038
1149
  runtimeState.currentSessionId = job.sessionId ?? null;
1039
1150
  console.log(`[WorkerPals] Claimed job ${job.id} (${job.kind})`);