opencode-supertask 0.1.40 → 0.1.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.
package/dist/cli/index.js CHANGED
@@ -4506,7 +4506,7 @@ var init_sql = __esm({
4506
4506
  return new SQL([new StringChunk(str)]);
4507
4507
  }
4508
4508
  sql2.raw = raw2;
4509
- function join8(chunks, separator) {
4509
+ function join9(chunks, separator) {
4510
4510
  const result = [];
4511
4511
  for (const [i, chunk] of chunks.entries()) {
4512
4512
  if (i > 0 && separator !== void 0) {
@@ -4516,7 +4516,7 @@ var init_sql = __esm({
4516
4516
  }
4517
4517
  return new SQL(result);
4518
4518
  }
4519
- sql2.join = join8;
4519
+ sql2.join = join9;
4520
4520
  function identifier(value) {
4521
4521
  return new Name(value);
4522
4522
  }
@@ -7132,7 +7132,7 @@ var init_select2 = __esm({
7132
7132
  return (table, on) => {
7133
7133
  const baseTableName = this.tableName;
7134
7134
  const tableName = getTableLikeName(table);
7135
- if (typeof tableName === "string" && this.config.joins?.some((join8) => join8.alias === tableName)) {
7135
+ if (typeof tableName === "string" && this.config.joins?.some((join9) => join9.alias === tableName)) {
7136
7136
  throw new Error(`Alias "${tableName}" is already used in this query`);
7137
7137
  }
7138
7138
  if (!this.isPartialSelect) {
@@ -7974,7 +7974,7 @@ var init_update = __esm({
7974
7974
  createJoin(joinType) {
7975
7975
  return (table, on) => {
7976
7976
  const tableName = getTableLikeName(table);
7977
- if (typeof tableName === "string" && this.config.joins.some((join8) => join8.alias === tableName)) {
7977
+ if (typeof tableName === "string" && this.config.joins.some((join9) => join9.alias === tableName)) {
7978
7978
  throw new Error(`Alias "${tableName}" is already used in this query`);
7979
7979
  }
7980
7980
  if (typeof on === "function") {
@@ -9790,13 +9790,23 @@ var init_task_service = __esm({
9790
9790
  });
9791
9791
  const maxRetries = normalizedData.maxRetries ?? task.maxRetries ?? 3;
9792
9792
  const exhausted = task.status === "failed" && (task.retryCount ?? 0) > maxRetries;
9793
- return tx.update(tasks2).set({
9793
+ const updated = tx.update(tasks2).set({
9794
9794
  ...normalizedData,
9795
9795
  ...exhausted ? {
9796
9796
  status: "dead_letter",
9797
9797
  retryAfter: null
9798
9798
  } : {}
9799
9799
  }).where(eq(tasks2.id, id)).returning().get() ?? null;
9800
+ if (exhausted && updated) {
9801
+ const finishedAt = /* @__PURE__ */ new Date();
9802
+ tx.update(tasks2).set({
9803
+ status: "dead_letter",
9804
+ finishedAt,
9805
+ retryAfter: null,
9806
+ resultLog: `\u4F9D\u8D56\u4EFB\u52A1 #${id} \u5DF2\u8FDB\u5165\u4E0D\u53EF\u6062\u590D\u7EC8\u6001`
9807
+ }).where(blockedDependentsOf(id)).run();
9808
+ }
9809
+ return updated;
9800
9810
  }, { behavior: "immediate" });
9801
9811
  }
9802
9812
  static validateNewTask(data) {
@@ -9817,7 +9827,7 @@ var init_task_service = __esm({
9817
9827
  throw new Error(`${name} \u5FC5\u987B\u662F ${min} \u5230 ${max} \u4E4B\u95F4\u7684\u6574\u6570`);
9818
9828
  }
9819
9829
  }
9820
- static async next(scope = {}) {
9830
+ static buildRunnableTaskWhere(scope) {
9821
9831
  const baseConditions = [...this.buildScopeWhere(scope)];
9822
9832
  const nowMs = Date.now();
9823
9833
  const retryAfterFilter = or(
@@ -9852,40 +9862,43 @@ var init_task_service = __esm({
9852
9862
  if (batchFilter) {
9853
9863
  conditions.push(batchFilter);
9854
9864
  }
9855
- const result = await db.select().from(tasks2).where(and(
9865
+ return and(
9856
9866
  ...conditions,
9857
9867
  or(
9858
9868
  isNull(tasks2.dependsOn),
9859
9869
  sql`EXISTS (
9860
- SELECT 1 FROM tasks AS dependency_task
9861
- WHERE dependency_task.id = ${tasks2.dependsOn}
9862
- AND dependency_task.status = 'done'
9863
- AND dependency_task.cwd IS ${tasks2.cwd}
9864
- )`
9870
+ SELECT 1 FROM tasks AS dependency_task
9871
+ WHERE dependency_task.id = ${tasks2.dependsOn}
9872
+ AND dependency_task.status = 'done'
9873
+ AND dependency_task.cwd IS ${tasks2.cwd}
9874
+ )`
9865
9875
  ),
9866
9876
  or(
9867
9877
  isNull(tasks2.batchId),
9868
9878
  sql`trim(${tasks2.batchId}, ${TASK_BATCH_TRIM_CHARACTERS}) = ''`,
9869
9879
  sql`NOT EXISTS (
9870
- SELECT 1 FROM tasks AS running_batch_task
9871
- WHERE trim(running_batch_task.batch_id, ${TASK_BATCH_TRIM_CHARACTERS})
9872
- = trim(${tasks2.batchId}, ${TASK_BATCH_TRIM_CHARACTERS})
9873
- AND (
9874
- running_batch_task.status = 'running'
9875
- OR EXISTS (
9876
- SELECT 1 FROM task_runs AS running_batch_run
9877
- WHERE running_batch_run.task_id = running_batch_task.id
9878
- AND running_batch_run.status = 'running'
9879
- )
9880
+ SELECT 1 FROM tasks AS running_batch_task
9881
+ WHERE trim(running_batch_task.batch_id, ${TASK_BATCH_TRIM_CHARACTERS})
9882
+ = trim(${tasks2.batchId}, ${TASK_BATCH_TRIM_CHARACTERS})
9883
+ AND (
9884
+ running_batch_task.status = 'running'
9885
+ OR EXISTS (
9886
+ SELECT 1 FROM task_runs AS running_batch_run
9887
+ WHERE running_batch_run.task_id = running_batch_task.id
9888
+ AND running_batch_run.status = 'running'
9880
9889
  )
9881
- )`
9890
+ )
9891
+ )`
9882
9892
  ),
9883
9893
  sql`NOT EXISTS (
9884
- SELECT 1 FROM task_runs AS candidate_active_run
9885
- WHERE candidate_active_run.task_id = ${tasks2.id}
9886
- AND candidate_active_run.status = 'running'
9887
- )`
9888
- )).orderBy(
9894
+ SELECT 1 FROM task_runs AS candidate_active_run
9895
+ WHERE candidate_active_run.task_id = ${tasks2.id}
9896
+ AND candidate_active_run.status = 'running'
9897
+ )`
9898
+ );
9899
+ }
9900
+ static async next(scope = {}) {
9901
+ const result = await db.select().from(tasks2).where(this.buildRunnableTaskWhere(scope)).orderBy(
9889
9902
  desc(tasks2.urgency),
9890
9903
  desc(tasks2.importance),
9891
9904
  asc(tasks2.createdAt),
@@ -9893,6 +9906,22 @@ var init_task_service = __esm({
9893
9906
  ).limit(1);
9894
9907
  return result[0] ?? null;
9895
9908
  }
9909
+ static async claimNext(scope = {}) {
9910
+ return db.transaction((tx) => {
9911
+ const candidate = tx.select().from(tasks2).where(this.buildRunnableTaskWhere(scope)).orderBy(
9912
+ desc(tasks2.urgency),
9913
+ desc(tasks2.importance),
9914
+ asc(tasks2.createdAt),
9915
+ asc(tasks2.id)
9916
+ ).limit(1).get();
9917
+ if (!candidate) return null;
9918
+ return tx.update(tasks2).set({
9919
+ status: "running",
9920
+ startedAt: /* @__PURE__ */ new Date(),
9921
+ finishedAt: null
9922
+ }).where(eq(tasks2.id, candidate.id)).returning().get() ?? null;
9923
+ }, { behavior: "immediate" });
9924
+ }
9896
9925
  static async countRunning(scope = {}) {
9897
9926
  const scopeConditions = scope.legacyCwd ? [sql`${tasks2.cwd} IS NULL OR trim(${tasks2.cwd}) = ''`] : this.buildScopeWhere(scope);
9898
9927
  if (scope.batchId !== void 0) {
@@ -10553,9 +10582,34 @@ var init_launch_protocol = __esm({
10553
10582
  });
10554
10583
 
10555
10584
  // src/core/process-control.ts
10556
- import { spawnSync } from "child_process";
10557
10585
  import { fileURLToPath as fileURLToPath2 } from "url";
10558
10586
  import { basename, dirname as dirname2, resolve } from "path";
10587
+ function runBoundedOsCommand(command, args, captureOutput = true) {
10588
+ const result = Bun.spawnSync({
10589
+ cmd: [
10590
+ process.execPath,
10591
+ "-e",
10592
+ BOUNDED_OS_COMMAND_RUNNER,
10593
+ "supertask-os-command",
10594
+ command,
10595
+ ...args
10596
+ ],
10597
+ detached: process.platform !== "win32",
10598
+ maxBuffer: OS_COMMAND_MAX_OUTPUT_BYTES + 1024,
10599
+ stdin: "ignore",
10600
+ stdout: "pipe",
10601
+ stderr: "ignore"
10602
+ });
10603
+ const output = new TextDecoder().decode(result.stdout);
10604
+ const markerIndex = output.lastIndexOf(OS_COMMAND_RESULT_MARKER);
10605
+ if (markerIndex < 0) return { status: null, stdout: "" };
10606
+ const statusText2 = output.slice(markerIndex + OS_COMMAND_RESULT_MARKER.length);
10607
+ if (!/^\d+$/.test(statusText2)) return { status: null, stdout: "" };
10608
+ return {
10609
+ status: Number(statusText2),
10610
+ stdout: captureOutput ? output.slice(0, markerIndex) : ""
10611
+ };
10612
+ }
10559
10613
  function isSafePid(pid) {
10560
10614
  return Number.isInteger(pid) && pid > 1 && pid !== process.pid;
10561
10615
  }
@@ -10592,12 +10646,10 @@ function inspectSpawnedProcessTreePresence(pid) {
10592
10646
  }
10593
10647
  }
10594
10648
  function inspectUnixProcess(pid) {
10595
- const result = spawnSync("ps", ["-o", "pgid=", "-o", "command=", "-p", String(pid)], {
10596
- encoding: "utf8",
10597
- stdio: ["ignore", "pipe", "ignore"],
10598
- timeout: OS_COMMAND_TIMEOUT_MS,
10599
- killSignal: "SIGKILL"
10600
- });
10649
+ const result = runBoundedOsCommand(
10650
+ "ps",
10651
+ ["-o", "pgid=", "-o", "command=", "-p", String(pid)]
10652
+ );
10601
10653
  if (result.status !== 0) return null;
10602
10654
  const match2 = result.stdout.trim().match(/^(\d+)\s+(.+)$/s);
10603
10655
  if (!match2) return null;
@@ -10605,30 +10657,18 @@ function inspectUnixProcess(pid) {
10605
10657
  }
10606
10658
  function inspectWindowsCommand(pid) {
10607
10659
  const script = `(Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}").CommandLine`;
10608
- const result = spawnSync(
10660
+ const result = runBoundedOsCommand(
10609
10661
  "powershell.exe",
10610
- ["-NoProfile", "-NonInteractive", "-Command", script],
10611
- {
10612
- encoding: "utf8",
10613
- stdio: ["ignore", "pipe", "ignore"],
10614
- timeout: OS_COMMAND_TIMEOUT_MS,
10615
- killSignal: "SIGKILL"
10616
- }
10662
+ ["-NoProfile", "-NonInteractive", "-Command", script]
10617
10663
  );
10618
10664
  if (result.status !== 0) return null;
10619
10665
  return result.stdout.trim() || null;
10620
10666
  }
10621
10667
  function inspectWindowsProcessTree(rootPid) {
10622
10668
  const script = `$root=${rootPid}; $all=@(Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId); $ids=New-Object 'System.Collections.Generic.HashSet[int]'; [void]$ids.Add($root); do { $added=$false; foreach($p in $all) { if($ids.Contains([int]$p.ParentProcessId) -and $ids.Add([int]$p.ProcessId)) { $added=$true } } } while($added); $all | Where-Object { $ids.Contains([int]$_.ProcessId) } | Select-Object -ExpandProperty ProcessId`;
10623
- const result = spawnSync(
10669
+ const result = runBoundedOsCommand(
10624
10670
  "powershell.exe",
10625
- ["-NoProfile", "-NonInteractive", "-Command", script],
10626
- {
10627
- encoding: "utf8",
10628
- stdio: ["ignore", "pipe", "ignore"],
10629
- timeout: OS_COMMAND_TIMEOUT_MS,
10630
- killSignal: "SIGKILL"
10631
- }
10671
+ ["-NoProfile", "-NonInteractive", "-Command", script]
10632
10672
  );
10633
10673
  if (result.status !== 0) return null;
10634
10674
  return result.stdout.split(/\s+/).filter(Boolean).map(Number).filter((pid) => Number.isInteger(pid) && pid > 0);
@@ -10699,11 +10739,7 @@ function signalRecordedProcessTreeWithResult(pid, signal, expectedExecutable, la
10699
10739
  }
10700
10740
  const args = ["/PID", String(pid), "/T"];
10701
10741
  if (signal === "SIGKILL") args.push("/F");
10702
- const status = spawnSync("taskkill", args, {
10703
- stdio: "ignore",
10704
- timeout: OS_COMMAND_TIMEOUT_MS,
10705
- killSignal: "SIGKILL"
10706
- }).status;
10742
+ const status = runBoundedOsCommand("taskkill", args, false).status;
10707
10743
  if (status === 0) return "signalled";
10708
10744
  return isProcessAlive(pid) ? "signal-failed" : absentLeaderResult(pid);
10709
10745
  }
@@ -10732,12 +10768,64 @@ async function waitForSpawnedProcessTreeExit(pid, timeoutMs = 5e3) {
10732
10768
  }
10733
10769
  return inspectSpawnedProcessTreePresence(pid) === "not-running";
10734
10770
  }
10735
- var OS_COMMAND_TIMEOUT_MS;
10771
+ var OS_COMMAND_TIMEOUT_MS, OS_COMMAND_MAX_OUTPUT_BYTES, OS_COMMAND_RESULT_MARKER, BOUNDED_OS_COMMAND_RUNNER;
10736
10772
  var init_process_control = __esm({
10737
10773
  "src/core/process-control.ts"() {
10738
10774
  "use strict";
10739
10775
  init_launch_protocol();
10740
10776
  OS_COMMAND_TIMEOUT_MS = 2e3;
10777
+ OS_COMMAND_MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
10778
+ OS_COMMAND_RESULT_MARKER = "\n\0supertask-os-command-result:";
10779
+ BOUNDED_OS_COMMAND_RUNNER = `
10780
+ const { writeSync } = await import('fs');
10781
+ let child;
10782
+ try {
10783
+ child = Bun.spawn(process.argv.slice(2), {
10784
+ stdin: 'ignore',
10785
+ stdout: 'pipe',
10786
+ stderr: 'ignore',
10787
+ });
10788
+ } catch {
10789
+ process.exit(127);
10790
+ }
10791
+ const terminateGroup = () => {
10792
+ if (process.platform !== 'win32') {
10793
+ try {
10794
+ process.kill(-process.pid, 'SIGKILL');
10795
+ } catch {
10796
+ }
10797
+ }
10798
+ try {
10799
+ child.kill('SIGKILL');
10800
+ } catch {
10801
+ }
10802
+ process.exit(1);
10803
+ };
10804
+ const timer = setTimeout(terminateGroup, ${OS_COMMAND_TIMEOUT_MS});
10805
+ try {
10806
+ const chunks = [];
10807
+ let outputBytes = 0;
10808
+ const readOutput = async () => {
10809
+ const reader = child.stdout.getReader();
10810
+ while (true) {
10811
+ const { done, value } = await reader.read();
10812
+ if (done) return;
10813
+ outputBytes += value.byteLength;
10814
+ if (outputBytes > ${OS_COMMAND_MAX_OUTPUT_BYTES}) terminateGroup();
10815
+ chunks.push(value);
10816
+ }
10817
+ };
10818
+ const [exitCode] = await Promise.all([child.exited, readOutput()]);
10819
+ clearTimeout(timer);
10820
+ for (const chunk of chunks) writeSync(1, chunk);
10821
+ writeSync(1, ${JSON.stringify(OS_COMMAND_RESULT_MARKER)} + String(exitCode));
10822
+ if (process.platform !== 'win32') terminateGroup();
10823
+ process.exit(0);
10824
+ } catch {
10825
+ clearTimeout(timer);
10826
+ terminateGroup();
10827
+ }
10828
+ `;
10741
10829
  }
10742
10830
  });
10743
10831
 
@@ -10889,7 +10977,7 @@ var init_task_run_service = __esm({
10889
10977
  }
10890
10978
  if (current.childPid != null) {
10891
10979
  throw new LegacyRunAbandonConflictError(
10892
- `run #${runId} \u5DF2\u8BB0\u5F55 child PID ${current.childPid}\uFF0C\u5FC5\u987B\u7531 Worker/Watchdog \u786E\u8BA4\u8FDB\u7A0B\u6811\u9000\u51FA`
10980
+ `run #${runId} \u5DF2\u8BB0\u5F55 child PID ${current.childPid}\uFF0C\u5FC5\u987B\u7531 Worker/Watchdog \u786E\u8BA4\u53D7\u7BA1\u8FDB\u7A0B\u7EC4\u6392\u7A7A`
10893
10981
  );
10894
10982
  }
10895
10983
  if (current.workerPid != null && isProcessAlive(current.workerPid)) {
@@ -21010,7 +21098,7 @@ __export(pm2_exports, {
21010
21098
  upgrade: () => upgrade,
21011
21099
  withGatewayMaintenance: () => withGatewayMaintenance
21012
21100
  });
21013
- import { execSync, spawnSync as spawnSync2 } from "child_process";
21101
+ import { execSync, spawnSync } from "child_process";
21014
21102
  import {
21015
21103
  accessSync,
21016
21104
  chmodSync as chmodSync2,
@@ -21115,7 +21203,7 @@ function diagnoseOpenCodeRuntime(runtime = {
21115
21203
  env: process.env
21116
21204
  }) {
21117
21205
  const executable = runtimeScope(runtime).opencodePath;
21118
- const result = spawnSync2(executable, ["--version"], {
21206
+ const result = spawnSync(executable, ["--version"], {
21119
21207
  cwd: runtime.cwd,
21120
21208
  env: runtime.env,
21121
21209
  encoding: "utf8",
@@ -21205,7 +21293,7 @@ function xmlEscape(value) {
21205
21293
  function resolvePm2Bin() {
21206
21294
  const configured = pm2Bin();
21207
21295
  if (isAbsolute2(configured)) return configured;
21208
- const result = spawnSync2("which", [configured], { encoding: "utf8" });
21296
+ const result = spawnSync("which", [configured], { encoding: "utf8" });
21209
21297
  const resolved = result.status === 0 ? result.stdout.trim().split("\n")[0] : "";
21210
21298
  if (!resolved) throw new Error(`[supertask] \u65E0\u6CD5\u89E3\u6790 pm2 \u53EF\u6267\u884C\u6587\u4EF6: ${configured}`);
21211
21299
  return resolved;
@@ -21262,18 +21350,18 @@ function isMacLaunchAgentConfigured(expectedPm2Home, expectedRuntime) {
21262
21350
  accessSync(bunPath, constants.X_OK);
21263
21351
  accessSync(supervisorEntry, constants.R_OK);
21264
21352
  accessSync(pm2Path, constants.X_OK);
21265
- if (spawnSync2(bunPath, ["--version"], {
21353
+ if (spawnSync(bunPath, ["--version"], {
21266
21354
  stdio: "ignore",
21267
21355
  timeout: pm2CommandTimeoutMs(),
21268
21356
  killSignal: "SIGKILL"
21269
21357
  }).status !== 0) return false;
21270
- if (spawnSync2(pm2Path, ["--version"], {
21358
+ if (spawnSync(pm2Path, ["--version"], {
21271
21359
  stdio: "ignore",
21272
21360
  env: { ...process.env, PM2_HOME: pm2Home },
21273
21361
  timeout: pm2CommandTimeoutMs(),
21274
21362
  killSignal: "SIGKILL"
21275
21363
  }).status !== 0) return false;
21276
- const loaded = spawnSync2(
21364
+ const loaded = spawnSync(
21277
21365
  launchctlBin(),
21278
21366
  ["print", `gui/${process.getuid()}/${MAC_LAUNCH_AGENT_LABEL}`],
21279
21367
  {
@@ -21301,7 +21389,7 @@ function isMacLaunchAgentConfigured(expectedPm2Home, expectedRuntime) {
21301
21389
  function bootstrapMacLaunchAgent(domain, path) {
21302
21390
  const retryDeadline = Date.now() + 2e3;
21303
21391
  const sleeper = new Int32Array(new SharedArrayBuffer(4));
21304
- let result = spawnSync2(launchctlBin(), ["bootstrap", domain, path], {
21392
+ let result = spawnSync(launchctlBin(), ["bootstrap", domain, path], {
21305
21393
  encoding: "utf8",
21306
21394
  timeout: pm2CommandTimeoutMs(),
21307
21395
  killSignal: "SIGKILL"
@@ -21310,7 +21398,7 @@ function bootstrapMacLaunchAgent(domain, path) {
21310
21398
  const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
21311
21399
  if (result.status !== 5 && !output.includes("Bootstrap failed: 5:")) break;
21312
21400
  Atomics.wait(sleeper, 0, 0, 100);
21313
- result = spawnSync2(launchctlBin(), ["bootstrap", domain, path], {
21401
+ result = spawnSync(launchctlBin(), ["bootstrap", domain, path], {
21314
21402
  encoding: "utf8",
21315
21403
  timeout: pm2CommandTimeoutMs(),
21316
21404
  killSignal: "SIGKILL"
@@ -21368,7 +21456,7 @@ function installMacLaunchAgent(expectedRuntime = currentGatewayRuntime()) {
21368
21456
  `;
21369
21457
  const domain = `gui/${process.getuid()}`;
21370
21458
  const previousPlist = existsSync5(path) ? readFileSync3(path) : null;
21371
- const wasLoaded = previousPlist != null && spawnSync2(
21459
+ const wasLoaded = previousPlist != null && spawnSync(
21372
21460
  launchctlBin(),
21373
21461
  ["print", `${domain}/${MAC_LAUNCH_AGENT_LABEL}`],
21374
21462
  {
@@ -21380,7 +21468,7 @@ function installMacLaunchAgent(expectedRuntime = currentGatewayRuntime()) {
21380
21468
  mkdirSync4(dirname6(path), { recursive: true });
21381
21469
  writeFileSync2(path, plist, { mode: 384 });
21382
21470
  chmodSync2(path, 384);
21383
- spawnSync2(launchctlBin(), ["bootout", `${domain}/${MAC_LAUNCH_AGENT_LABEL}`], {
21471
+ spawnSync(launchctlBin(), ["bootout", `${domain}/${MAC_LAUNCH_AGENT_LABEL}`], {
21384
21472
  stdio: "ignore",
21385
21473
  timeout: pm2CommandTimeoutMs(),
21386
21474
  killSignal: "SIGKILL"
@@ -21398,7 +21486,7 @@ function installMacLaunchAgent(expectedRuntime = currentGatewayRuntime()) {
21398
21486
  if (loaded.status !== 0 || !verified) {
21399
21487
  const output = loaded.status === 0 ? "supervisor \u672A\u4FDD\u6301 running \u6216 PM2 dump \u4E0D\u53EF\u6062\u590D" : `${loaded.stdout ?? ""}${loaded.stderr ?? ""}`.trim();
21400
21488
  if (loaded.status === 0) {
21401
- spawnSync2(launchctlBin(), ["bootout", `${domain}/${MAC_LAUNCH_AGENT_LABEL}`], {
21489
+ spawnSync(launchctlBin(), ["bootout", `${domain}/${MAC_LAUNCH_AGENT_LABEL}`], {
21402
21490
  stdio: "ignore",
21403
21491
  timeout: pm2CommandTimeoutMs(),
21404
21492
  killSignal: "SIGKILL"
@@ -21425,7 +21513,7 @@ function systemctlBin(env = process.env) {
21425
21513
  }
21426
21514
  function isLinuxStartupConfigured(env = process.env, cwd = process.cwd(), expectedRuntime) {
21427
21515
  const unit = env.SUPERTASK_PM2_SYSTEMD_UNIT ?? `pm2-${userInfo().username}.service`;
21428
- const enabled = spawnSync2(systemctlBin(env), ["is-enabled", unit], {
21516
+ const enabled = spawnSync(systemctlBin(env), ["is-enabled", unit], {
21429
21517
  encoding: "utf8",
21430
21518
  env,
21431
21519
  stdio: ["ignore", "pipe", "ignore"],
@@ -21433,7 +21521,7 @@ function isLinuxStartupConfigured(env = process.env, cwd = process.cwd(), expect
21433
21521
  killSignal: "SIGKILL"
21434
21522
  });
21435
21523
  if (enabled.status !== 0 || enabled.stdout.trim() !== "enabled") return false;
21436
- const contents = spawnSync2(systemctlBin(env), ["cat", unit], {
21524
+ const contents = spawnSync(systemctlBin(env), ["cat", unit], {
21437
21525
  encoding: "utf8",
21438
21526
  env,
21439
21527
  stdio: ["ignore", "pipe", "ignore"],
@@ -21457,7 +21545,7 @@ function isLinuxStartupConfigured(env = process.env, cwd = process.cwd(), expect
21457
21545
  }
21458
21546
  }
21459
21547
  function isPm2Installed(env = process.env) {
21460
- const result = spawnSync2(pm2Bin(env), ["--version"], {
21548
+ const result = spawnSync(pm2Bin(env), ["--version"], {
21461
21549
  stdio: "ignore",
21462
21550
  env,
21463
21551
  shell: process.platform === "win32",
@@ -21482,7 +21570,7 @@ function installPm2() {
21482
21570
  }
21483
21571
  function resolveCommand(command) {
21484
21572
  const lookup = process.platform === "win32" ? "where" : "which";
21485
- const result = spawnSync2(lookup, [command], { encoding: "utf8" });
21573
+ const result = spawnSync(lookup, [command], { encoding: "utf8" });
21486
21574
  return result.status === 0 ? result.stdout.trim().split("\n")[0] || null : null;
21487
21575
  }
21488
21576
  function npmPreferredEnvironment() {
@@ -21504,7 +21592,7 @@ function npmPreferredEnvironment() {
21504
21592
  function pm2Exec(args, options = {}) {
21505
21593
  const npmEnvironment = options.preferNpm ? npmPreferredEnvironment() : null;
21506
21594
  const effectiveEnv = options.env ?? npmEnvironment?.env ?? process.env;
21507
- const result = spawnSync2(npmEnvironment?.command ?? pm2Bin(effectiveEnv), args, {
21595
+ const result = spawnSync(npmEnvironment?.command ?? pm2Bin(effectiveEnv), args, {
21508
21596
  stdio: ["pipe", "pipe", "pipe"],
21509
21597
  encoding: "utf-8",
21510
21598
  env: effectiveEnv,
@@ -21579,7 +21667,7 @@ function gatewayRuntimeFromProcess(processInfo) {
21579
21667
  try {
21580
21668
  accessSync(savedBunPath, constants.X_OK);
21581
21669
  if (!statSync3(savedCwd).isDirectory()) return null;
21582
- if (spawnSync2(savedBunPath, ["--version"], {
21670
+ if (spawnSync(savedBunPath, ["--version"], {
21583
21671
  stdio: "ignore",
21584
21672
  env: savedEnv,
21585
21673
  timeout: pm2CommandTimeoutMs(savedEnv),
@@ -22041,7 +22129,7 @@ function removeMacLaunchAgent() {
22041
22129
  if (typeof process.getuid !== "function") return;
22042
22130
  const path = launchAgentPath();
22043
22131
  const domain = `gui/${process.getuid()}`;
22044
- const loaded = spawnSync2(
22132
+ const loaded = spawnSync(
22045
22133
  launchctlBin(),
22046
22134
  ["print", `${domain}/${MAC_LAUNCH_AGENT_LABEL}`],
22047
22135
  {
@@ -22054,7 +22142,7 @@ function removeMacLaunchAgent() {
22054
22142
  throw new Error(`[supertask] \u65E0\u6CD5\u786E\u8BA4 macOS LaunchAgent \u72B6\u6001: ${loaded.error.message}`);
22055
22143
  }
22056
22144
  if (loaded.status === 0) {
22057
- const removed = spawnSync2(
22145
+ const removed = spawnSync(
22058
22146
  launchctlBin(),
22059
22147
  ["bootout", `${domain}/${MAC_LAUNCH_AGENT_LABEL}`],
22060
22148
  {
@@ -22265,7 +22353,7 @@ __export(update_exports, {
22265
22353
  resolveInstalledPluginVersion: () => resolveInstalledPluginVersion,
22266
22354
  updateGlobalCli: () => updateGlobalCli
22267
22355
  });
22268
- import { spawnSync as spawnSync3 } from "child_process";
22356
+ import { spawnSync as spawnSync2 } from "child_process";
22269
22357
  import {
22270
22358
  chmodSync as chmodSync3,
22271
22359
  closeSync,
@@ -22330,7 +22418,7 @@ function bunBin() {
22330
22418
  function resolveGlobalCliExecutable() {
22331
22419
  const override = process.env.SUPERTASK_CLI_BIN;
22332
22420
  if (override) return existsSync6(override) ? resolve4(override) : null;
22333
- const result = spawnSync3(process.platform === "win32" ? "where" : "which", ["supertask"], {
22421
+ const result = spawnSync2(process.platform === "win32" ? "where" : "which", ["supertask"], {
22334
22422
  encoding: "utf8",
22335
22423
  env: process.env,
22336
22424
  timeout: 1e4
@@ -22365,7 +22453,7 @@ function packageVersion(packageDir) {
22365
22453
  }
22366
22454
  }
22367
22455
  function npmGlobalRoot() {
22368
- const result = spawnSync3(npmBin(), ["root", "-g"], {
22456
+ const result = spawnSync2(npmBin(), ["root", "-g"], {
22369
22457
  encoding: "utf8",
22370
22458
  env: process.env,
22371
22459
  timeout: 3e4
@@ -22429,7 +22517,7 @@ function updateGlobalCli(version2) {
22429
22517
  }
22430
22518
  const command = before.packageManager === "npm" ? npmBin() : bunBin();
22431
22519
  const args = before.packageManager === "npm" ? ["install", "-g", `${PACKAGE_NAME}@${version2}`] : ["add", "-g", `${PACKAGE_NAME}@${version2}`];
22432
- const result = spawnSync3(command, args, {
22520
+ const result = spawnSync2(command, args, {
22433
22521
  encoding: "utf8",
22434
22522
  env: process.env,
22435
22523
  timeout: 12e4
@@ -22450,7 +22538,7 @@ function updateGlobalCli(version2) {
22450
22538
  return { ...after, action: "updated" };
22451
22539
  }
22452
22540
  function getLatestVersion() {
22453
- const result = spawnSync3(npmBin(), [
22541
+ const result = spawnSync2(npmBin(), [
22454
22542
  "view",
22455
22543
  PACKAGE_NAME,
22456
22544
  "dist-tags.latest",
@@ -22533,7 +22621,7 @@ function getOpenCodePluginDiagnostic() {
22533
22621
  chmodSync3(outputDirectory, 448);
22534
22622
  const outputPath = join5(outputDirectory, "resolved-config.json");
22535
22623
  outputFd = openSync(outputPath, "w", 384);
22536
- const result = spawnSync3(opencodeBin(), ["debug", "config", "--pure"], {
22624
+ const result = spawnSync2(opencodeBin(), ["debug", "config", "--pure"], {
22537
22625
  encoding: "utf8",
22538
22626
  env: process.env,
22539
22627
  timeout: 3e4,
@@ -22603,7 +22691,7 @@ function installPluginVersion(version2) {
22603
22691
  if (!isSemanticVersion(version2)) {
22604
22692
  throw new Error(`[supertask] OpenCode \u63D2\u4EF6\u7248\u672C\u65E0\u6548: ${version2}`);
22605
22693
  }
22606
- const result = spawnSync3(opencodeBin(), [
22694
+ const result = spawnSync2(opencodeBin(), [
22607
22695
  "plugin",
22608
22696
  `${PACKAGE_NAME}@${version2}`,
22609
22697
  "--global",
@@ -22782,7 +22870,7 @@ function runCommandContext(executable, args, cwd) {
22782
22870
  function assertWorkerProcessIsolationSupported(platform = process.platform) {
22783
22871
  if (platform === "win32") {
22784
22872
  throw new Error(
22785
- "Windows Worker \u5DF2\u5B89\u5168\u7981\u7528\uFF1A\u5F53\u524D\u8FD0\u884C\u65F6\u65E0\u6CD5\u7528 Job Object \u8BC1\u660E\u6574\u4E2A OpenCode \u8FDB\u7A0B\u6811\u5DF2\u9000\u51FA"
22873
+ "Windows Worker \u5DF2\u5B89\u5168\u7981\u7528\uFF1A\u5F53\u524D\u8FD0\u884C\u65F6\u65E0\u6CD5\u7528 Job Object \u63D0\u4F9B\u7B49\u4EF7\u7684\u53D7\u7BA1\u8FDB\u7A0B\u9694\u79BB\u4E0E\u6392\u7A7A\u8BC1\u660E"
22786
22874
  );
22787
22875
  }
22788
22876
  }
@@ -22806,17 +22894,24 @@ var init_worker = __esm({
22806
22894
  pollTimer = null;
22807
22895
  heartbeatTimer = null;
22808
22896
  pollCyclePromise = null;
22897
+ shutdownDeadlineMs = null;
22898
+ settlementRetryWakeups = /* @__PURE__ */ new Set();
22809
22899
  cfg;
22810
22900
  opencodeBin;
22811
22901
  maxOutputChars;
22902
+ settlementRetryDelaysMs;
22903
+ settlementRetryIntervalMs;
22812
22904
  constructor(cfg, options = {}) {
22813
22905
  this.cfg = cfg.worker;
22814
22906
  this.opencodeBin = options.opencodeBin ?? process.env.SUPERTASK_OPENCODE_BIN ?? "opencode";
22815
22907
  this.maxOutputChars = options.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
22908
+ this.settlementRetryDelaysMs = options.settlementRetryDelaysMs ?? [250, 1e3, 4e3];
22909
+ this.settlementRetryIntervalMs = options.settlementRetryIntervalMs ?? 5e3;
22816
22910
  }
22817
22911
  start() {
22818
22912
  assertWorkerProcessIsolationSupported();
22819
22913
  this.stopped = false;
22914
+ this.shutdownDeadlineMs = null;
22820
22915
  markGatewayActivity("worker");
22821
22916
  this.poll();
22822
22917
  this.heartbeatTimer = setInterval(() => {
@@ -22825,6 +22920,8 @@ var init_worker = __esm({
22825
22920
  }
22826
22921
  async stop(gracePeriodMs = 0) {
22827
22922
  this.stopped = true;
22923
+ this.shutdownDeadlineMs = Date.now() + Math.max(0, gracePeriodMs);
22924
+ for (const wake of [...this.settlementRetryWakeups]) wake();
22828
22925
  if (this.pollTimer) {
22829
22926
  clearTimeout(this.pollTimer);
22830
22927
  this.pollTimer = null;
@@ -22894,21 +22991,18 @@ var init_worker = __esm({
22894
22991
  if (databaseRunningCount >= this.cfg.maxConcurrency) break;
22895
22992
  let task;
22896
22993
  try {
22897
- task = await TaskService.next({ excludedBatchIds: [...this.activeBatchIds] });
22994
+ task = await TaskService.claimNext({ excludedBatchIds: [...this.activeBatchIds] });
22898
22995
  } catch (err) {
22899
22996
  this.logError("task claim failed", err);
22900
22997
  throw err;
22901
22998
  }
22902
22999
  if (!task) break;
22903
- if (this.stopped) break;
22904
- if (!await TaskService.start(task.id)) continue;
22905
- const batchId = normalizeTaskBatchId(task.batchId);
22906
- if (batchId) this.activeBatchIds.add(batchId);
22907
23000
  if (this.stopped) {
22908
23001
  await TaskService.resetRunningToPending([task.id]);
22909
- this.releaseBatch(task);
22910
23002
  break;
22911
23003
  }
23004
+ const batchId = normalizeTaskBatchId(task.batchId);
23005
+ if (batchId) this.activeBatchIds.add(batchId);
22912
23006
  let runId = null;
22913
23007
  try {
22914
23008
  const launchIdentity = `gateway-${process.pid}:launch:${randomUUID3()}`;
@@ -23119,7 +23213,7 @@ var init_worker = __esm({
23119
23213
  if (!entry.guardianDrained) {
23120
23214
  const pid = entry.child.pid;
23121
23215
  const presence = pid == null ? spawnError == null ? "unknown" : "not-running" : inspectSpawnedProcessTreePresence(pid);
23122
- const message = "guardian \u672A\u63D0\u4F9B\u8FDB\u7A0B\u6811\u6392\u7A7A\u8BC1\u660E";
23216
+ const message = "guardian \u672A\u63D0\u4F9B\u53D7\u7BA1\u8FDB\u7A0B\u7EC4\u6392\u7A7A\u8BC1\u660E";
23123
23217
  if (presence !== "not-running") {
23124
23218
  if (entry.timeoutTimer) {
23125
23219
  clearTimeout(entry.timeoutTimer);
@@ -23155,17 +23249,48 @@ var init_worker = __esm({
23155
23249
  clearTimeout(entry.timeoutTimer);
23156
23250
  entry.timeoutTimer = null;
23157
23251
  }
23158
- const settlement = this.commitEntry(entry, code, failure).then(() => true).catch((error) => {
23159
- markGatewayFailure("worker", error);
23160
- this.logError("task settlement failed", error, entry.task.id);
23161
- return false;
23162
- }).finally(() => {
23252
+ const settlement = this.commitEntryWithRetry(entry, code, failure).finally(() => {
23163
23253
  this.runningTasks.delete(entry.task.id);
23164
23254
  this.releaseBatch(entry.task);
23165
23255
  });
23166
23256
  entry.settlementPromise = settlement;
23167
23257
  return settlement;
23168
23258
  }
23259
+ async commitEntryWithRetry(entry, code, failure) {
23260
+ for (let attempt = 0; ; attempt += 1) {
23261
+ try {
23262
+ await this.commitEntry(entry, code, failure);
23263
+ return true;
23264
+ } catch (error) {
23265
+ markGatewayFailure("worker", error);
23266
+ const shortRetryDelayMs = this.settlementRetryDelaysMs[attempt];
23267
+ let retryDelayMs = shortRetryDelayMs ?? this.settlementRetryIntervalMs;
23268
+ if (this.stopped) {
23269
+ const remainingMs = Math.max(0, (this.shutdownDeadlineMs ?? 0) - Date.now());
23270
+ if (remainingMs === 0) return false;
23271
+ retryDelayMs = Math.min(retryDelayMs, remainingMs);
23272
+ }
23273
+ this.logError(
23274
+ `task settlement failed; retrying in ${retryDelayMs}ms`,
23275
+ error,
23276
+ entry.task.id
23277
+ );
23278
+ await this.waitForSettlementRetry(retryDelayMs);
23279
+ }
23280
+ }
23281
+ }
23282
+ waitForSettlementRetry(delayMs) {
23283
+ return new Promise((resolve6) => {
23284
+ let timer;
23285
+ const finish = () => {
23286
+ clearTimeout(timer);
23287
+ this.settlementRetryWakeups.delete(finish);
23288
+ resolve6();
23289
+ };
23290
+ timer = setTimeout(finish, delayMs);
23291
+ this.settlementRetryWakeups.add(finish);
23292
+ });
23293
+ }
23169
23294
  async commitEntry(entry, code, failure) {
23170
23295
  const termination = entry.termination;
23171
23296
  if (termination?.kind === "shutdown") return;
@@ -26260,9 +26385,9 @@ async function loadOpenCodeCatalog(cwd, options = {}) {
26260
26385
  const executable = options.executable ?? process.env.SUPERTASK_OPENCODE_BIN ?? "opencode";
26261
26386
  const timeoutMs = options.timeoutMs ?? COMMAND_TIMEOUT_MS;
26262
26387
  const cacheKey = `${executable}\0${cwd}`;
26263
- const cached = catalogCache.get(cacheKey);
26264
- if (options.useCache !== false && cached && cached.expiresAt > Date.now()) {
26265
- return cached.result;
26388
+ const cached2 = catalogCache.get(cacheKey);
26389
+ if (options.useCache !== false && cached2 && cached2.expiresAt > Date.now()) {
26390
+ return cached2.result;
26266
26391
  }
26267
26392
  const result = Promise.all([
26268
26393
  runOpenCode(executable, ["models", "--verbose"], cwd, timeoutMs).catch((error) => {
@@ -26301,6 +26426,109 @@ var init_opencode_catalog = __esm({
26301
26426
  }
26302
26427
  });
26303
26428
 
26429
+ // src/web/gateway-diagnostic.ts
26430
+ import { spawn as spawn3 } from "child_process";
26431
+ import { existsSync as existsSync8 } from "fs";
26432
+ import { dirname as dirname9, join as join7 } from "path";
26433
+ import { fileURLToPath as fileURLToPath6 } from "url";
26434
+ function killDiagnosticGroup(pid) {
26435
+ if (process.platform !== "win32") {
26436
+ try {
26437
+ process.kill(-pid, "SIGKILL");
26438
+ return;
26439
+ } catch {
26440
+ }
26441
+ }
26442
+ try {
26443
+ process.kill(pid, "SIGKILL");
26444
+ } catch {
26445
+ }
26446
+ }
26447
+ function diagnosticEntry() {
26448
+ const extension = import.meta.url.endsWith(".ts") ? "ts" : "js";
26449
+ const moduleDir = dirname9(fileURLToPath6(import.meta.url));
26450
+ const candidates = [
26451
+ join7(moduleDir, `../daemon/gateway-diagnostic-runner.${extension}`),
26452
+ join7(moduleDir, `../../src/daemon/gateway-diagnostic-runner.${extension}`)
26453
+ ];
26454
+ const entry = candidates.find((candidate) => existsSync8(candidate));
26455
+ if (!entry) throw new Error(`Gateway diagnostic runner \u4E0D\u5B58\u5728\uFF1A${candidates.join(", ")}`);
26456
+ return entry;
26457
+ }
26458
+ async function runGatewayDiagnostic() {
26459
+ return new Promise((resolve6, reject) => {
26460
+ const child = spawn3(process.execPath, [diagnosticEntry()], {
26461
+ cwd: process.cwd(),
26462
+ env: process.env,
26463
+ detached: process.platform !== "win32",
26464
+ stdio: ["ignore", "pipe", "pipe"]
26465
+ });
26466
+ let stdout = "";
26467
+ let stderr = "";
26468
+ let timedOut = false;
26469
+ if (child.pid) activeProcessGroups.add(child.pid);
26470
+ child.stdout?.on("data", (chunk) => {
26471
+ stdout += chunk.toString();
26472
+ });
26473
+ child.stderr?.on("data", (chunk) => {
26474
+ stderr += chunk.toString();
26475
+ });
26476
+ child.once("error", reject);
26477
+ child.once("close", (exitCode) => {
26478
+ clearTimeout(timer);
26479
+ if (child.pid) {
26480
+ killDiagnosticGroup(child.pid);
26481
+ activeProcessGroups.delete(child.pid);
26482
+ }
26483
+ if (timedOut) {
26484
+ reject(new Error(`Gateway diagnostic runner \u8D85\u8FC7 ${DIAGNOSTIC_TIMEOUT_MS}ms \u672A\u5B8C\u6210`));
26485
+ return;
26486
+ }
26487
+ if (exitCode !== 0) {
26488
+ reject(new Error(stderr.trim() || `Gateway diagnostic runner \u9000\u51FA\u7801 ${exitCode}`));
26489
+ return;
26490
+ }
26491
+ try {
26492
+ resolve6(JSON.parse(stdout));
26493
+ } catch (error) {
26494
+ reject(error);
26495
+ }
26496
+ });
26497
+ const timer = setTimeout(() => {
26498
+ timedOut = true;
26499
+ if (child.pid) killDiagnosticGroup(child.pid);
26500
+ else child.kill("SIGKILL");
26501
+ }, DIAGNOSTIC_TIMEOUT_MS);
26502
+ });
26503
+ }
26504
+ async function getDashboardGatewayDiagnostic(options = {}) {
26505
+ if (!options.fresh && cached && cached.expiresAt > Date.now()) return cached.diagnostic;
26506
+ if (!options.fresh && pending) return pending;
26507
+ const operation = runGatewayDiagnostic().then((diagnostic) => {
26508
+ cached = { expiresAt: Date.now() + CACHE_TTL_MS, diagnostic };
26509
+ return diagnostic;
26510
+ });
26511
+ if (options.fresh) return operation;
26512
+ pending = operation.finally(() => {
26513
+ pending = null;
26514
+ });
26515
+ return pending;
26516
+ }
26517
+ var CACHE_TTL_MS, DIAGNOSTIC_TIMEOUT_MS, cached, pending, activeProcessGroups;
26518
+ var init_gateway_diagnostic = __esm({
26519
+ "src/web/gateway-diagnostic.ts"() {
26520
+ "use strict";
26521
+ CACHE_TTL_MS = 5e3;
26522
+ DIAGNOSTIC_TIMEOUT_MS = 2e4;
26523
+ cached = null;
26524
+ pending = null;
26525
+ activeProcessGroups = /* @__PURE__ */ new Set();
26526
+ process.once("exit", () => {
26527
+ for (const pid of activeProcessGroups) killDiagnosticGroup(pid);
26528
+ });
26529
+ }
26530
+ });
26531
+
26304
26532
  // src/web/ui.ts
26305
26533
  function t(locale, key, values = {}) {
26306
26534
  const template = (locale === "en" ? EN : ZH)[key];
@@ -26966,7 +27194,7 @@ var init_ui = __esm({
26966
27194
  "details.enabledYes": "\u5DF2\u542F\u7528",
26967
27195
  "details.enabledNo": "\u5DF2\u505C\u7528",
26968
27196
  "dialog.cancelTask": "\u53D6\u6D88\u4EFB\u52A1 #{id}\uFF1F",
26969
- "dialog.cancelTaskBody": "\u8FD0\u884C\u4E2D\u7684\u4EFB\u52A1\u4F1A\u5728\u4E0B\u4E00\u4E2A\u8F6E\u8BE2\u5468\u671F\u7EC8\u6B62\u5BF9\u5E94\u8FDB\u7A0B\u6811\u3002",
27197
+ "dialog.cancelTaskBody": "\u8FD0\u884C\u4E2D\u7684\u4EFB\u52A1\u4F1A\u5728\u4E0B\u4E00\u4E2A\u8F6E\u8BE2\u5468\u671F\u7EC8\u6B62\u5BF9\u5E94\u7684\u53D7\u7BA1\u8FDB\u7A0B\u7EC4\u3002",
26970
27198
  "dialog.retryTask": "\u91CD\u8BD5\u4EFB\u52A1 #{id}\uFF1F",
26971
27199
  "dialog.retryTaskBody": "\u4EFB\u52A1\u5C06\u56DE\u5230\u5F85\u6267\u884C\u72B6\u6001\uFF0C\u5E76\u91CD\u7F6E\u81EA\u52A8\u91CD\u8BD5\u9884\u7B97\u3002",
26972
27200
  "dialog.deleteTask": "\u5220\u9664\u4EFB\u52A1 #{id}\uFF1F",
@@ -27271,7 +27499,7 @@ var init_ui = __esm({
27271
27499
  "details.enabledYes": "Enabled",
27272
27500
  "details.enabledNo": "Disabled",
27273
27501
  "dialog.cancelTask": "Cancel task #{id}?",
27274
- "dialog.cancelTaskBody": "A running task will terminate its process tree on the next worker poll.",
27502
+ "dialog.cancelTaskBody": "A running task will terminate its managed process group on the next worker poll.",
27275
27503
  "dialog.retryTask": "Retry task #{id}?",
27276
27504
  "dialog.retryTaskBody": "The task returns to pending and its automatic retry budget is reset.",
27277
27505
  "dialog.deleteTask": "Delete task #{id}?",
@@ -27692,7 +27920,7 @@ __export(web_exports, {
27692
27920
  setDashboardRuntimeConfig: () => setDashboardRuntimeConfig
27693
27921
  });
27694
27922
  import {
27695
- existsSync as existsSync8,
27923
+ existsSync as existsSync9,
27696
27924
  mkdirSync as mkdirSync5,
27697
27925
  readFileSync as readFileSync5,
27698
27926
  readdirSync as readdirSync2,
@@ -27701,7 +27929,7 @@ import {
27701
27929
  writeFileSync as writeFileSync3
27702
27930
  } from "fs";
27703
27931
  import { homedir as homedir5 } from "os";
27704
- import { basename as basename3, dirname as dirname9, join as join7 } from "path";
27932
+ import { basename as basename3, dirname as dirname10, join as join8 } from "path";
27705
27933
  function setDashboardRuntimeConfig(config) {
27706
27934
  runtimeConfig = config === null ? null : structuredClone(config);
27707
27935
  }
@@ -27726,10 +27954,13 @@ function resolveDashboardConfigState(runtimeAvailable, restartRequired, managedR
27726
27954
  function isSafeDashboardRestartTarget(diagnostic, currentPid) {
27727
27955
  return diagnostic.pm2Installed && diagnostic.processFound && diagnostic.status === "online" && diagnostic.pid === currentPid && diagnostic.ready && diagnostic.scopeMatches;
27728
27956
  }
27729
- function canRestartCurrentGateway() {
27957
+ async function canRestartCurrentGateway(fresh = false) {
27730
27958
  if (runtimeConfig === null) return false;
27731
27959
  try {
27732
- return isSafeDashboardRestartTarget(getGatewayDiagnostic(), process.pid);
27960
+ return isSafeDashboardRestartTarget(
27961
+ await getDashboardGatewayDiagnostic({ fresh }),
27962
+ process.pid
27963
+ );
27733
27964
  } catch {
27734
27965
  return false;
27735
27966
  }
@@ -27744,7 +27975,7 @@ function parseTaskStatus2(value) {
27744
27975
  }
27745
27976
  function listChildDirectories(path) {
27746
27977
  return readdirSync2(path, { withFileTypes: true }).flatMap((entry) => {
27747
- const entryPath = join7(path, entry.name);
27978
+ const entryPath = join8(path, entry.name);
27748
27979
  if (entry.isDirectory()) return [{ name: entry.name, path: entryPath, hidden: entry.name.startsWith(".") }];
27749
27980
  if (!entry.isSymbolicLink()) return [];
27750
27981
  try {
@@ -27984,7 +28215,7 @@ function esc(value) {
27984
28215
  }
27985
28216
  function readCurrentConfig() {
27986
28217
  const configPath = getConfigPath();
27987
- if (!existsSync8(configPath)) return {};
28218
+ if (!existsSync9(configPath)) return {};
27988
28219
  try {
27989
28220
  return JSON.parse(readFileSync5(configPath, "utf-8"));
27990
28221
  } catch {
@@ -27993,8 +28224,8 @@ function readCurrentConfig() {
27993
28224
  }
27994
28225
  function writeConfig(cfg) {
27995
28226
  const configPath = getConfigPath();
27996
- const dir = dirname9(configPath);
27997
- if (!existsSync8(dir)) mkdirSync5(dir, { recursive: true });
28227
+ const dir = dirname10(configPath);
28228
+ if (!existsSync9(dir)) mkdirSync5(dir, { recursive: true });
27998
28229
  const tempPath = `${configPath}.${process.pid}.tmp`;
27999
28230
  writeFileSync3(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
28000
28231
  renameSync2(tempPath, configPath);
@@ -28048,7 +28279,7 @@ function pagination(locale, basePath, page, pages, total, suffix = "") {
28048
28279
  const next = page < pages ? `<a class="btn" href="${basePath}?page=${page + 1}${suffix}">${t(locale, "pagination.next")}${icon("chevronRight")}</a>` : "";
28049
28280
  return `<div class="pagination">${previous}<span class="summary">${t(locale, "pagination.summary", { page, pages, total })}</span>${next}</div>`;
28050
28281
  }
28051
- var app, LEGACY_PROJECT_FILTER, TASK_STATUSES2, SESSION_ID_PATTERN, runtimeConfig, restartScheduled, dashboardApp, web_default;
28282
+ var app, LEGACY_PROJECT_FILTER, TASK_STATUSES2, SESSION_ID_PATTERN, LOOPBACK_HOST_PATTERN, runtimeConfig, restartScheduled, dashboardApp, web_default;
28052
28283
  var init_web = __esm({
28053
28284
  "src/web/index.tsx"() {
28054
28285
  "use strict";
@@ -28065,7 +28296,7 @@ var init_web = __esm({
28065
28296
  init_config();
28066
28297
  init_health();
28067
28298
  init_job_templates();
28068
- init_pm2();
28299
+ init_gateway_diagnostic();
28069
28300
  init_ui();
28070
28301
  app = new Hono2();
28071
28302
  LEGACY_PROJECT_FILTER = "__supertask_legacy__";
@@ -28078,9 +28309,18 @@ var init_web = __esm({
28078
28309
  "cancelled"
28079
28310
  ]);
28080
28311
  SESSION_ID_PATTERN = /^ses_[A-Za-z0-9_]+$/;
28312
+ LOOPBACK_HOST_PATTERN = /^(?:localhost|127\.0\.0\.1|\[::1\])(?::([1-9]\d{0,4}))?$/i;
28081
28313
  runtimeConfig = null;
28082
28314
  restartScheduled = false;
28083
28315
  app.use("*", async (c, next) => {
28316
+ const requestHostname = new URL(c.req.url).hostname;
28317
+ const hostHeader = c.req.header("Host");
28318
+ const loopbackHosts = ["localhost", "127.0.0.1", "[::1]"];
28319
+ const hostMatch = hostHeader?.match(LOOPBACK_HOST_PATTERN) ?? null;
28320
+ const hostPort = hostMatch?.[1] === void 0 ? null : Number(hostMatch[1]);
28321
+ if (!loopbackHosts.includes(requestHostname) || hostHeader !== void 0 && (!hostMatch || hostPort !== null && hostPort > 65535)) {
28322
+ return c.json({ error: "invalid dashboard host" }, 421);
28323
+ }
28084
28324
  await next();
28085
28325
  c.header("X-Content-Type-Options", "nosniff");
28086
28326
  c.header("X-Frame-Options", "DENY");
@@ -28117,7 +28357,7 @@ var init_web = __esm({
28117
28357
  validateTaskWorkingDirectory(requestedPath);
28118
28358
  return c.json({
28119
28359
  path: requestedPath,
28120
- parent: dirname9(requestedPath),
28360
+ parent: dirname10(requestedPath),
28121
28361
  home: homedir5(),
28122
28362
  directories: listChildDirectories(requestedPath)
28123
28363
  });
@@ -28452,7 +28692,7 @@ var init_web = __esm({
28452
28692
  const config = loadConfig();
28453
28693
  const activeConfig = runtimeConfig ?? config;
28454
28694
  const restartRequired = runtimeConfig !== null && !configsEqual(config, runtimeConfig);
28455
- const managedRestart = canRestartCurrentGateway();
28695
+ const managedRestart = await canRestartCurrentGateway();
28456
28696
  const configState = resolveDashboardConfigState(
28457
28697
  runtimeConfig !== null,
28458
28698
  restartRequired,
@@ -28471,7 +28711,7 @@ var init_web = __esm({
28471
28711
  TaskRunService.getAllRunningRuns(),
28472
28712
  TaskTemplateService.stats()
28473
28713
  ]);
28474
- const configExists = existsSync8(configPath);
28714
+ const configExists = existsSync9(configPath);
28475
28715
  const runRows = runningRuns.map((run) => {
28476
28716
  const session = maskSessionId(run.sessionId);
28477
28717
  return `<tr><td class="faint" data-label="${t(locale, "table.run")}">#${run.id}</td><td data-primary data-label="${t(locale, "table.task")}">#${run.taskId}</td><td data-label="${t(locale, "table.session")}" class="m small">${esc(session)}</td>
@@ -28572,9 +28812,9 @@ var init_web = __esm({
28572
28812
  if (!isValidSessionId(run.sessionId)) return c.json({ error: "session unavailable" }, 409);
28573
28813
  return c.json({ command: sessionCommand(run.sessionId) });
28574
28814
  });
28575
- app.get("/api/gateway/status", (c) => {
28815
+ app.get("/api/gateway/status", async (c) => {
28576
28816
  const savedConfig = loadConfig();
28577
- const managed = canRestartCurrentGateway();
28817
+ const managed = await canRestartCurrentGateway();
28578
28818
  return c.json({
28579
28819
  pid: process.pid,
28580
28820
  managed,
@@ -28699,7 +28939,7 @@ var init_web = __esm({
28699
28939
  return c.json({
28700
28940
  success: true,
28701
28941
  restartRequired: runtimeConfig === null || !configsEqual(savedConfig, runtimeConfig),
28702
- managed: canRestartCurrentGateway()
28942
+ managed: await canRestartCurrentGateway()
28703
28943
  });
28704
28944
  } catch (error) {
28705
28945
  return c.json({
@@ -28715,9 +28955,10 @@ var init_web = __esm({
28715
28955
  return c.json({ error: "confirmation must be RESTART" }, 400);
28716
28956
  }
28717
28957
  if (restartScheduled) return c.json({ error: "restart already scheduled" }, 409);
28718
- if (!canRestartCurrentGateway()) {
28958
+ if (!await canRestartCurrentGateway(true)) {
28719
28959
  return c.json({ error: "\u5F53\u524D Gateway \u4E0D\u662F\u7531\u5339\u914D\u8FD0\u884C\u4F5C\u7528\u57DF\u7684 PM2 \u8FDB\u7A0B\u6258\u7BA1\uFF0C\u65E0\u6CD5\u4ECE\u7F51\u9875\u5B89\u5168\u91CD\u542F" }, 409);
28720
28960
  }
28961
+ if (restartScheduled) return c.json({ error: "restart already scheduled" }, 409);
28721
28962
  restartScheduled = true;
28722
28963
  const previousPid = process.pid;
28723
28964
  setTimeout(() => {
@@ -29686,13 +29927,13 @@ databaseCommand.command("restore").description(t2("\u81EA\u52A8\u5907\u4EFD\u5F5
29686
29927
  }, (error) => renderDatabaseError(error, { forceJson: options.json })));
29687
29928
  program2.addCommand(databaseCommand);
29688
29929
  program2.command("init").description(t2("\u521D\u59CB\u5316 SuperTask\uFF08\u521B\u5EFA\u914D\u7F6E\u5E76\u6267\u884C\u8FC1\u79FB\uFF09", "initialize SuperTask (create config and run migrations)")).action(async () => withDb(async () => {
29689
- const { existsSync: existsSync9, mkdirSync: mkdirSync6, writeFileSync: writeFileSync4 } = await import("fs");
29690
- const { dirname: dirname10 } = await import("path");
29930
+ const { existsSync: existsSync10, mkdirSync: mkdirSync6, writeFileSync: writeFileSync4 } = await import("fs");
29931
+ const { dirname: dirname11 } = await import("path");
29691
29932
  const { getConfigPath: getConfigPath2 } = await Promise.resolve().then(() => (init_config(), config_exports));
29692
29933
  const configPath = getConfigPath2();
29693
- if (!existsSync9(configPath)) {
29694
- const dir = dirname10(configPath);
29695
- if (!existsSync9(dir)) mkdirSync6(dir, { recursive: true });
29934
+ if (!existsSync10(configPath)) {
29935
+ const dir = dirname11(configPath);
29936
+ if (!existsSync10(dir)) mkdirSync6(dir, { recursive: true });
29696
29937
  writeFileSync4(configPath, JSON.stringify({
29697
29938
  configVersion: 2,
29698
29939
  worker: { maxConcurrency: 2 },