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.
@@ -6110,8 +6110,8 @@ import { homedir } from "os";
6110
6110
  import { join, dirname } from "path";
6111
6111
  import { fileURLToPath } from "url";
6112
6112
  function getMigrationsFolder() {
6113
- const __dirname2 = dirname(fileURLToPath(import.meta.url));
6114
- let dir = __dirname2;
6113
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6114
+ let dir = __dirname;
6115
6115
  for (let i = 0; i < 5; i++) {
6116
6116
  const candidate = join(dir, "drizzle", "meta", "_journal.json");
6117
6117
  if (existsSync(candidate)) {
@@ -6119,7 +6119,7 @@ function getMigrationsFolder() {
6119
6119
  }
6120
6120
  dir = dirname(dir);
6121
6121
  }
6122
- return join(__dirname2, "../../drizzle");
6122
+ return join(__dirname, "../../drizzle");
6123
6123
  }
6124
6124
  function ensureGatewayLock(sqliteDb) {
6125
6125
  sqliteDb.exec(`
@@ -6598,13 +6598,23 @@ var init_task_service = __esm({
6598
6598
  });
6599
6599
  const maxRetries = normalizedData.maxRetries ?? task.maxRetries ?? 3;
6600
6600
  const exhausted = task.status === "failed" && (task.retryCount ?? 0) > maxRetries;
6601
- return tx.update(tasks2).set({
6601
+ const updated = tx.update(tasks2).set({
6602
6602
  ...normalizedData,
6603
6603
  ...exhausted ? {
6604
6604
  status: "dead_letter",
6605
6605
  retryAfter: null
6606
6606
  } : {}
6607
6607
  }).where(eq(tasks2.id, id)).returning().get() ?? null;
6608
+ if (exhausted && updated) {
6609
+ const finishedAt = /* @__PURE__ */ new Date();
6610
+ tx.update(tasks2).set({
6611
+ status: "dead_letter",
6612
+ finishedAt,
6613
+ retryAfter: null,
6614
+ resultLog: `\u4F9D\u8D56\u4EFB\u52A1 #${id} \u5DF2\u8FDB\u5165\u4E0D\u53EF\u6062\u590D\u7EC8\u6001`
6615
+ }).where(blockedDependentsOf(id)).run();
6616
+ }
6617
+ return updated;
6608
6618
  }, { behavior: "immediate" });
6609
6619
  }
6610
6620
  static validateNewTask(data) {
@@ -6625,7 +6635,7 @@ var init_task_service = __esm({
6625
6635
  throw new Error(`${name} \u5FC5\u987B\u662F ${min} \u5230 ${max} \u4E4B\u95F4\u7684\u6574\u6570`);
6626
6636
  }
6627
6637
  }
6628
- static async next(scope = {}) {
6638
+ static buildRunnableTaskWhere(scope) {
6629
6639
  const baseConditions = [...this.buildScopeWhere(scope)];
6630
6640
  const nowMs = Date.now();
6631
6641
  const retryAfterFilter = or(
@@ -6660,40 +6670,43 @@ var init_task_service = __esm({
6660
6670
  if (batchFilter) {
6661
6671
  conditions.push(batchFilter);
6662
6672
  }
6663
- const result = await db.select().from(tasks2).where(and(
6673
+ return and(
6664
6674
  ...conditions,
6665
6675
  or(
6666
6676
  isNull(tasks2.dependsOn),
6667
6677
  sql`EXISTS (
6668
- SELECT 1 FROM tasks AS dependency_task
6669
- WHERE dependency_task.id = ${tasks2.dependsOn}
6670
- AND dependency_task.status = 'done'
6671
- AND dependency_task.cwd IS ${tasks2.cwd}
6672
- )`
6678
+ SELECT 1 FROM tasks AS dependency_task
6679
+ WHERE dependency_task.id = ${tasks2.dependsOn}
6680
+ AND dependency_task.status = 'done'
6681
+ AND dependency_task.cwd IS ${tasks2.cwd}
6682
+ )`
6673
6683
  ),
6674
6684
  or(
6675
6685
  isNull(tasks2.batchId),
6676
6686
  sql`trim(${tasks2.batchId}, ${TASK_BATCH_TRIM_CHARACTERS}) = ''`,
6677
6687
  sql`NOT EXISTS (
6678
- SELECT 1 FROM tasks AS running_batch_task
6679
- WHERE trim(running_batch_task.batch_id, ${TASK_BATCH_TRIM_CHARACTERS})
6680
- = trim(${tasks2.batchId}, ${TASK_BATCH_TRIM_CHARACTERS})
6681
- AND (
6682
- running_batch_task.status = 'running'
6683
- OR EXISTS (
6684
- SELECT 1 FROM task_runs AS running_batch_run
6685
- WHERE running_batch_run.task_id = running_batch_task.id
6686
- AND running_batch_run.status = 'running'
6687
- )
6688
+ SELECT 1 FROM tasks AS running_batch_task
6689
+ WHERE trim(running_batch_task.batch_id, ${TASK_BATCH_TRIM_CHARACTERS})
6690
+ = trim(${tasks2.batchId}, ${TASK_BATCH_TRIM_CHARACTERS})
6691
+ AND (
6692
+ running_batch_task.status = 'running'
6693
+ OR EXISTS (
6694
+ SELECT 1 FROM task_runs AS running_batch_run
6695
+ WHERE running_batch_run.task_id = running_batch_task.id
6696
+ AND running_batch_run.status = 'running'
6688
6697
  )
6689
- )`
6698
+ )
6699
+ )`
6690
6700
  ),
6691
6701
  sql`NOT EXISTS (
6692
- SELECT 1 FROM task_runs AS candidate_active_run
6693
- WHERE candidate_active_run.task_id = ${tasks2.id}
6694
- AND candidate_active_run.status = 'running'
6695
- )`
6696
- )).orderBy(
6702
+ SELECT 1 FROM task_runs AS candidate_active_run
6703
+ WHERE candidate_active_run.task_id = ${tasks2.id}
6704
+ AND candidate_active_run.status = 'running'
6705
+ )`
6706
+ );
6707
+ }
6708
+ static async next(scope = {}) {
6709
+ const result = await db.select().from(tasks2).where(this.buildRunnableTaskWhere(scope)).orderBy(
6697
6710
  desc(tasks2.urgency),
6698
6711
  desc(tasks2.importance),
6699
6712
  asc(tasks2.createdAt),
@@ -6701,6 +6714,22 @@ var init_task_service = __esm({
6701
6714
  ).limit(1);
6702
6715
  return result[0] ?? null;
6703
6716
  }
6717
+ static async claimNext(scope = {}) {
6718
+ return db.transaction((tx) => {
6719
+ const candidate = tx.select().from(tasks2).where(this.buildRunnableTaskWhere(scope)).orderBy(
6720
+ desc(tasks2.urgency),
6721
+ desc(tasks2.importance),
6722
+ asc(tasks2.createdAt),
6723
+ asc(tasks2.id)
6724
+ ).limit(1).get();
6725
+ if (!candidate) return null;
6726
+ return tx.update(tasks2).set({
6727
+ status: "running",
6728
+ startedAt: /* @__PURE__ */ new Date(),
6729
+ finishedAt: null
6730
+ }).where(eq(tasks2.id, candidate.id)).returning().get() ?? null;
6731
+ }, { behavior: "immediate" });
6732
+ }
6704
6733
  static async countRunning(scope = {}) {
6705
6734
  const scopeConditions = scope.legacyCwd ? [sql`${tasks2.cwd} IS NULL OR trim(${tasks2.cwd}) = ''`] : this.buildScopeWhere(scope);
6706
6735
  if (scope.batchId !== void 0) {
@@ -7361,9 +7390,34 @@ var init_launch_protocol = __esm({
7361
7390
  });
7362
7391
 
7363
7392
  // src/core/process-control.ts
7364
- import { spawnSync } from "child_process";
7365
7393
  import { fileURLToPath as fileURLToPath2 } from "url";
7366
7394
  import { basename, dirname as dirname2, resolve } from "path";
7395
+ function runBoundedOsCommand(command, args, captureOutput = true) {
7396
+ const result = Bun.spawnSync({
7397
+ cmd: [
7398
+ process.execPath,
7399
+ "-e",
7400
+ BOUNDED_OS_COMMAND_RUNNER,
7401
+ "supertask-os-command",
7402
+ command,
7403
+ ...args
7404
+ ],
7405
+ detached: process.platform !== "win32",
7406
+ maxBuffer: OS_COMMAND_MAX_OUTPUT_BYTES + 1024,
7407
+ stdin: "ignore",
7408
+ stdout: "pipe",
7409
+ stderr: "ignore"
7410
+ });
7411
+ const output = new TextDecoder().decode(result.stdout);
7412
+ const markerIndex = output.lastIndexOf(OS_COMMAND_RESULT_MARKER);
7413
+ if (markerIndex < 0) return { status: null, stdout: "" };
7414
+ const statusText2 = output.slice(markerIndex + OS_COMMAND_RESULT_MARKER.length);
7415
+ if (!/^\d+$/.test(statusText2)) return { status: null, stdout: "" };
7416
+ return {
7417
+ status: Number(statusText2),
7418
+ stdout: captureOutput ? output.slice(0, markerIndex) : ""
7419
+ };
7420
+ }
7367
7421
  function isSafePid(pid) {
7368
7422
  return Number.isInteger(pid) && pid > 1 && pid !== process.pid;
7369
7423
  }
@@ -7400,12 +7454,10 @@ function inspectSpawnedProcessTreePresence(pid) {
7400
7454
  }
7401
7455
  }
7402
7456
  function inspectUnixProcess(pid) {
7403
- const result = spawnSync("ps", ["-o", "pgid=", "-o", "command=", "-p", String(pid)], {
7404
- encoding: "utf8",
7405
- stdio: ["ignore", "pipe", "ignore"],
7406
- timeout: OS_COMMAND_TIMEOUT_MS,
7407
- killSignal: "SIGKILL"
7408
- });
7457
+ const result = runBoundedOsCommand(
7458
+ "ps",
7459
+ ["-o", "pgid=", "-o", "command=", "-p", String(pid)]
7460
+ );
7409
7461
  if (result.status !== 0) return null;
7410
7462
  const match2 = result.stdout.trim().match(/^(\d+)\s+(.+)$/s);
7411
7463
  if (!match2) return null;
@@ -7413,30 +7465,18 @@ function inspectUnixProcess(pid) {
7413
7465
  }
7414
7466
  function inspectWindowsCommand(pid) {
7415
7467
  const script = `(Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}").CommandLine`;
7416
- const result = spawnSync(
7468
+ const result = runBoundedOsCommand(
7417
7469
  "powershell.exe",
7418
- ["-NoProfile", "-NonInteractive", "-Command", script],
7419
- {
7420
- encoding: "utf8",
7421
- stdio: ["ignore", "pipe", "ignore"],
7422
- timeout: OS_COMMAND_TIMEOUT_MS,
7423
- killSignal: "SIGKILL"
7424
- }
7470
+ ["-NoProfile", "-NonInteractive", "-Command", script]
7425
7471
  );
7426
7472
  if (result.status !== 0) return null;
7427
7473
  return result.stdout.trim() || null;
7428
7474
  }
7429
7475
  function inspectWindowsProcessTree(rootPid) {
7430
7476
  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`;
7431
- const result = spawnSync(
7477
+ const result = runBoundedOsCommand(
7432
7478
  "powershell.exe",
7433
- ["-NoProfile", "-NonInteractive", "-Command", script],
7434
- {
7435
- encoding: "utf8",
7436
- stdio: ["ignore", "pipe", "ignore"],
7437
- timeout: OS_COMMAND_TIMEOUT_MS,
7438
- killSignal: "SIGKILL"
7439
- }
7479
+ ["-NoProfile", "-NonInteractive", "-Command", script]
7440
7480
  );
7441
7481
  if (result.status !== 0) return null;
7442
7482
  return result.stdout.split(/\s+/).filter(Boolean).map(Number).filter((pid) => Number.isInteger(pid) && pid > 0);
@@ -7507,11 +7547,7 @@ function signalRecordedProcessTreeWithResult(pid, signal, expectedExecutable, la
7507
7547
  }
7508
7548
  const args = ["/PID", String(pid), "/T"];
7509
7549
  if (signal === "SIGKILL") args.push("/F");
7510
- const status = spawnSync("taskkill", args, {
7511
- stdio: "ignore",
7512
- timeout: OS_COMMAND_TIMEOUT_MS,
7513
- killSignal: "SIGKILL"
7514
- }).status;
7550
+ const status = runBoundedOsCommand("taskkill", args, false).status;
7515
7551
  if (status === 0) return "signalled";
7516
7552
  return isProcessAlive(pid) ? "signal-failed" : absentLeaderResult(pid);
7517
7553
  }
@@ -7540,12 +7576,64 @@ async function waitForSpawnedProcessTreeExit(pid, timeoutMs = 5e3) {
7540
7576
  }
7541
7577
  return inspectSpawnedProcessTreePresence(pid) === "not-running";
7542
7578
  }
7543
- var OS_COMMAND_TIMEOUT_MS;
7579
+ var OS_COMMAND_TIMEOUT_MS, OS_COMMAND_MAX_OUTPUT_BYTES, OS_COMMAND_RESULT_MARKER, BOUNDED_OS_COMMAND_RUNNER;
7544
7580
  var init_process_control = __esm({
7545
7581
  "src/core/process-control.ts"() {
7546
7582
  "use strict";
7547
7583
  init_launch_protocol();
7548
7584
  OS_COMMAND_TIMEOUT_MS = 2e3;
7585
+ OS_COMMAND_MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
7586
+ OS_COMMAND_RESULT_MARKER = "\n\0supertask-os-command-result:";
7587
+ BOUNDED_OS_COMMAND_RUNNER = `
7588
+ const { writeSync } = await import('fs');
7589
+ let child;
7590
+ try {
7591
+ child = Bun.spawn(process.argv.slice(2), {
7592
+ stdin: 'ignore',
7593
+ stdout: 'pipe',
7594
+ stderr: 'ignore',
7595
+ });
7596
+ } catch {
7597
+ process.exit(127);
7598
+ }
7599
+ const terminateGroup = () => {
7600
+ if (process.platform !== 'win32') {
7601
+ try {
7602
+ process.kill(-process.pid, 'SIGKILL');
7603
+ } catch {
7604
+ }
7605
+ }
7606
+ try {
7607
+ child.kill('SIGKILL');
7608
+ } catch {
7609
+ }
7610
+ process.exit(1);
7611
+ };
7612
+ const timer = setTimeout(terminateGroup, ${OS_COMMAND_TIMEOUT_MS});
7613
+ try {
7614
+ const chunks = [];
7615
+ let outputBytes = 0;
7616
+ const readOutput = async () => {
7617
+ const reader = child.stdout.getReader();
7618
+ while (true) {
7619
+ const { done, value } = await reader.read();
7620
+ if (done) return;
7621
+ outputBytes += value.byteLength;
7622
+ if (outputBytes > ${OS_COMMAND_MAX_OUTPUT_BYTES}) terminateGroup();
7623
+ chunks.push(value);
7624
+ }
7625
+ };
7626
+ const [exitCode] = await Promise.all([child.exited, readOutput()]);
7627
+ clearTimeout(timer);
7628
+ for (const chunk of chunks) writeSync(1, chunk);
7629
+ writeSync(1, ${JSON.stringify(OS_COMMAND_RESULT_MARKER)} + String(exitCode));
7630
+ if (process.platform !== 'win32') terminateGroup();
7631
+ process.exit(0);
7632
+ } catch {
7633
+ clearTimeout(timer);
7634
+ terminateGroup();
7635
+ }
7636
+ `;
7549
7637
  }
7550
7638
  });
7551
7639
 
@@ -7697,7 +7785,7 @@ var init_task_run_service = __esm({
7697
7785
  }
7698
7786
  if (current.childPid != null) {
7699
7787
  throw new LegacyRunAbandonConflictError(
7700
- `run #${runId} \u5DF2\u8BB0\u5F55 child PID ${current.childPid}\uFF0C\u5FC5\u987B\u7531 Worker/Watchdog \u786E\u8BA4\u8FDB\u7A0B\u6811\u9000\u51FA`
7788
+ `run #${runId} \u5DF2\u8BB0\u5F55 child PID ${current.childPid}\uFF0C\u5FC5\u987B\u7531 Worker/Watchdog \u786E\u8BA4\u53D7\u7BA1\u8FDB\u7A0B\u7EC4\u6392\u7A7A`
7701
7789
  );
7702
7790
  }
7703
7791
  if (current.workerPid != null && isProcessAlive(current.workerPid)) {
@@ -16941,8 +17029,8 @@ var require_CronFileParser = __commonJS({
16941
17029
  * @throws If file cannot be read
16942
17030
  */
16943
17031
  static parseFileSync(filePath) {
16944
- const { readFileSync: readFileSync5 } = __require("fs");
16945
- const data = readFileSync5(filePath, "utf8");
17032
+ const { readFileSync: readFileSync4 } = __require("fs");
17033
+ const data = readFileSync4(filePath, "utf8");
16946
17034
  return _CronFileParser.#parseContent(data);
16947
17035
  }
16948
17036
  /**
@@ -17387,32 +17475,6 @@ var init_job_templates = __esm({
17387
17475
  }
17388
17476
  });
17389
17477
 
17390
- // src/core/package-version.ts
17391
- import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
17392
- import { dirname as dirname4, join as join4 } from "path";
17393
- import { fileURLToPath as fileURLToPath4 } from "url";
17394
- function getPackageVersion() {
17395
- let directory = dirname4(fileURLToPath4(import.meta.url));
17396
- for (let depth = 0; depth < 5; depth += 1) {
17397
- const packagePath = join4(directory, "package.json");
17398
- if (existsSync4(packagePath)) {
17399
- try {
17400
- const pkg = JSON.parse(readFileSync2(packagePath, "utf8"));
17401
- if (typeof pkg.version === "string") return pkg.version;
17402
- } catch {
17403
- return "0.0.0";
17404
- }
17405
- }
17406
- directory = dirname4(directory);
17407
- }
17408
- return "0.0.0";
17409
- }
17410
- var init_package_version = __esm({
17411
- "src/core/package-version.ts"() {
17412
- "use strict";
17413
- }
17414
- });
17415
-
17416
17478
  // node_modules/hono/dist/compose.js
17417
17479
  var compose;
17418
17480
  var init_compose = __esm({
@@ -19670,7 +19732,7 @@ function cleanOutput(value) {
19670
19732
  return value.replace(ANSI_PATTERN, "").replace(/\r/g, "");
19671
19733
  }
19672
19734
  function runOpenCode(executable, args, cwd, timeoutMs) {
19673
- return new Promise((resolve4, reject) => {
19735
+ return new Promise((resolve3, reject) => {
19674
19736
  const child = spawn2(executable, args, {
19675
19737
  cwd,
19676
19738
  env: process.env,
@@ -19748,7 +19810,7 @@ function runOpenCode(executable, args, cwd, timeoutMs) {
19748
19810
  reject(new OpenCodeCommandExitError(`OpenCode ${args.join(" ")} \u5931\u8D25\uFF1A${detail}`));
19749
19811
  return;
19750
19812
  }
19751
- resolve4(cleanOutput(stdout));
19813
+ resolve3(cleanOutput(stdout));
19752
19814
  });
19753
19815
  });
19754
19816
  }
@@ -19819,9 +19881,9 @@ async function loadOpenCodeCatalog(cwd, options = {}) {
19819
19881
  const executable = options.executable ?? process.env.SUPERTASK_OPENCODE_BIN ?? "opencode";
19820
19882
  const timeoutMs = options.timeoutMs ?? COMMAND_TIMEOUT_MS;
19821
19883
  const cacheKey = `${executable}\0${cwd}`;
19822
- const cached = catalogCache.get(cacheKey);
19823
- if (options.useCache !== false && cached && cached.expiresAt > Date.now()) {
19824
- return cached.result;
19884
+ const cached2 = catalogCache.get(cacheKey);
19885
+ if (options.useCache !== false && cached2 && cached2.expiresAt > Date.now()) {
19886
+ return cached2.result;
19825
19887
  }
19826
19888
  const result = Promise.all([
19827
19889
  runOpenCode(executable, ["models", "--verbose"], cwd, timeoutMs).catch((error) => {
@@ -20296,464 +20358,106 @@ var init_database_maintenance_service = __esm({
20296
20358
  }
20297
20359
  });
20298
20360
 
20299
- // src/daemon/management-lock.ts
20300
- import { Database as Database4 } from "bun:sqlite";
20301
- var init_management_lock = __esm({
20302
- "src/daemon/management-lock.ts"() {
20303
- "use strict";
20304
- }
20305
- });
20306
-
20307
- // src/daemon/pm2.ts
20308
- import { execSync, spawnSync as spawnSync2 } from "child_process";
20309
- import {
20310
- accessSync,
20311
- chmodSync as chmodSync2,
20312
- constants,
20313
- existsSync as existsSync6,
20314
- mkdirSync as mkdirSync3,
20315
- readFileSync as readFileSync3,
20316
- rmSync,
20317
- statSync as statSync3,
20318
- writeFileSync as writeFileSync2
20319
- } from "fs";
20320
- import { homedir as homedir3, userInfo } from "os";
20321
- import { delimiter, dirname as dirname6, isAbsolute as isAbsolute2, join as join5, resolve as resolve3 } from "path";
20361
+ // src/web/gateway-diagnostic.ts
20362
+ import { spawn as spawn3 } from "child_process";
20363
+ import { existsSync as existsSync6 } from "fs";
20364
+ import { dirname as dirname6, join as join5 } from "path";
20322
20365
  import { fileURLToPath as fileURLToPath5 } from "url";
20323
- import { Database as Database5 } from "bun:sqlite";
20324
- function runtimeHome(env) {
20325
- return resolve3(env.HOME || homedir3());
20326
- }
20327
- function runtimePath(value, cwd) {
20328
- return resolve3(cwd, value);
20329
- }
20330
- function versionFile(env = process.env, cwd = process.cwd()) {
20331
- return env.SUPERTASK_VERSION_FILE ? runtimePath(env.SUPERTASK_VERSION_FILE, cwd) : join5(runtimeHome(env), ".local/share/opencode/supertask-gateway-version");
20332
- }
20333
- function getRunningVersion(env = process.env, cwd = process.cwd()) {
20334
- try {
20335
- const path = versionFile(env, cwd);
20336
- if (!existsSync6(path)) return null;
20337
- return readFileSync3(path, "utf-8").trim() || null;
20338
- } catch {
20339
- return null;
20340
- }
20341
- }
20342
- function packageVersionFromGatewayEntry(gatewayEntry) {
20343
- if (gatewayEntry === null) return null;
20344
- let directory = dirname6(gatewayEntry);
20345
- for (let depth = 0; depth < 6; depth += 1) {
20346
- const packagePath = join5(directory, "package.json");
20347
- if (existsSync6(packagePath)) {
20348
- try {
20349
- const pkg = JSON.parse(readFileSync3(packagePath, "utf8"));
20350
- if (pkg.name === "opencode-supertask" && typeof pkg.version === "string") {
20351
- return pkg.version;
20352
- }
20353
- } catch {
20354
- }
20355
- }
20356
- const parent = dirname6(directory);
20357
- if (parent === directory) break;
20358
- directory = parent;
20359
- }
20360
- return null;
20361
- }
20362
- function resolveRuntimeExecutable(command, env, cwd) {
20363
- if (isAbsolute2(command) || command.includes("/")) return runtimePath(command, cwd);
20364
- for (const entry of (env.PATH ?? "").split(delimiter)) {
20365
- if (!entry) continue;
20366
- const candidate = resolve3(cwd, entry, command);
20366
+ function killDiagnosticGroup(pid) {
20367
+ if (process.platform !== "win32") {
20367
20368
  try {
20368
- accessSync(candidate, constants.X_OK);
20369
- return candidate;
20369
+ process.kill(-pid, "SIGKILL");
20370
+ return;
20370
20371
  } catch {
20371
20372
  }
20372
20373
  }
20373
- return command;
20374
- }
20375
- function runtimeScope(runtime) {
20376
- const { cwd, env } = runtime;
20377
- const home = runtimeHome(env);
20378
- return {
20379
- cwd: resolve3(cwd),
20380
- databasePath: env.SUPERTASK_DB_PATH ? runtimePath(env.SUPERTASK_DB_PATH, cwd) : join5(home, ".local/share/opencode/tasks.db"),
20381
- configPath: env.SUPERTASK_CONFIG_PATH ? runtimePath(env.SUPERTASK_CONFIG_PATH, cwd) : join5(home, ".config/opencode/supertask.json"),
20382
- opencodePath: resolveRuntimeExecutable(env.SUPERTASK_OPENCODE_BIN ?? "opencode", env, cwd),
20383
- home,
20384
- pm2Home: env.PM2_HOME ? runtimePath(env.PM2_HOME, cwd) : join5(home, ".pm2"),
20385
- managementLockPath: canonicalManagementLockPath(env, cwd)
20386
- };
20387
- }
20388
- function scopesMatch(left, right) {
20389
- return left.databasePath === right.databasePath && left.configPath === right.configPath && left.opencodePath === right.opencodePath && left.home === right.home && left.pm2Home === right.pm2Home && left.managementLockPath === right.managementLockPath;
20390
- }
20391
- function currentScope() {
20392
- return runtimeScope({ cwd: process.cwd(), env: process.env });
20393
- }
20394
- function diagnoseOpenCodeRuntime(runtime = {
20395
- cwd: process.cwd(),
20396
- env: process.env
20397
- }) {
20398
- const executable = runtimeScope(runtime).opencodePath;
20399
- const result = spawnSync2(executable, ["--version"], {
20400
- cwd: runtime.cwd,
20401
- env: runtime.env,
20402
- encoding: "utf8",
20403
- timeout: 1e4,
20404
- killSignal: "SIGKILL"
20405
- });
20406
- const ok = result.status === 0 && result.error === void 0;
20407
- return {
20408
- ok,
20409
- executable,
20410
- version: ok ? result.stdout.trim() || result.stderr.trim() || null : null,
20411
- error: ok ? null : result.error?.message || result.stderr.trim() || result.stdout.trim() || `exit code ${result.status}`
20412
- };
20413
- }
20414
- function getGatewayDiagnostic(options = {}) {
20415
- const producerScope = currentScope();
20416
- if (!isPm2Installed()) {
20417
- return {
20418
- pm2Installed: false,
20419
- processFound: false,
20420
- status: null,
20421
- pid: null,
20422
- ready: false,
20423
- runningVersion: getRunningVersion(),
20424
- gatewayEntry: null,
20425
- gatewayPackageVersion: null,
20426
- logRotationInstalled: false,
20427
- startupConfigured: process.platform === "darwin" || process.platform === "linux" ? false : null,
20428
- currentScope: producerScope,
20429
- gatewayScope: null,
20430
- scopeMatches: false,
20431
- gatewayOpenCode: null
20432
- };
20433
- }
20434
- const processes = pm2JsonList();
20435
- const gateway = processes.find((item) => item.name === PROCESS_NAME);
20436
- const runtime = gatewayRuntimeFromProcess(gateway);
20437
- const gatewayEnv = runtime?.env ?? process.env;
20438
- const managedScope = runtime ? runtimeScope(runtime) : null;
20439
- const pid = typeof gateway?.pid === "number" ? gateway.pid : null;
20440
- const readyPath = managedScope?.databasePath ?? databasePath();
20441
- const lockedVersion = pid == null ? void 0 : gatewayVersionFromLock(pid, readyPath);
20442
- const gatewayEntry = runtime?.gatewayEntry ?? null;
20443
- return {
20444
- pm2Installed: true,
20445
- processFound: gateway != null,
20446
- status: gateway?.pm2_env?.status ?? null,
20447
- pid,
20448
- ready: pid != null && isGatewayReady(pid, readyPath),
20449
- runningVersion: lockedVersion === void 0 ? getRunningVersion(gatewayEnv, runtime?.cwd) : lockedVersion,
20450
- gatewayEntry,
20451
- gatewayPackageVersion: packageVersionFromGatewayEntry(gatewayEntry),
20452
- logRotationInstalled: processes.some((item) => item.name === "pm2-logrotate" && item.pm2_env?.status === "online"),
20453
- startupConfigured: process.platform === "darwin" ? isMacLaunchAgentConfigured(
20454
- gatewayEnv.PM2_HOME ?? join5(runtimeHome(gatewayEnv), ".pm2"),
20455
- runtime ?? void 0
20456
- ) : process.platform === "linux" ? isLinuxStartupConfigured(gatewayEnv, runtime?.cwd, runtime ?? void 0) : null,
20457
- currentScope: producerScope,
20458
- gatewayScope: managedScope,
20459
- scopeMatches: managedScope != null && scopesMatch(producerScope, managedScope),
20460
- gatewayOpenCode: runtime === null || !options.probeOpenCode ? null : diagnoseOpenCodeRuntime(runtime)
20461
- };
20462
- }
20463
- function pm2Bin(env = process.env) {
20464
- return env.SUPERTASK_PM2_BIN ?? (process.platform === "win32" ? "pm2.cmd" : "pm2");
20465
- }
20466
- function pm2CommandTimeoutMs(env = process.env) {
20467
- const configured = Number(env.SUPERTASK_PM2_COMMAND_TIMEOUT_MS ?? 15e3);
20468
- return Number.isFinite(configured) && configured > 0 ? configured : 15e3;
20469
- }
20470
- function resolvePm2Bin() {
20471
- const configured = pm2Bin();
20472
- if (isAbsolute2(configured)) return configured;
20473
- const result = spawnSync2("which", [configured], { encoding: "utf8" });
20474
- const resolved = result.status === 0 ? result.stdout.trim().split("\n")[0] : "";
20475
- if (!resolved) throw new Error(`[supertask] \u65E0\u6CD5\u89E3\u6790 pm2 \u53EF\u6267\u884C\u6587\u4EF6: ${configured}`);
20476
- return resolved;
20477
- }
20478
- function launchAgentPath() {
20479
- return process.env.SUPERTASK_LAUNCH_AGENT_PATH ?? join5(runtimeHome(process.env), "Library/LaunchAgents", `${MAC_LAUNCH_AGENT_LABEL}.plist`);
20480
- }
20481
- function launchctlBin() {
20482
- return process.env.SUPERTASK_LAUNCHCTL_BIN ?? "launchctl";
20483
- }
20484
- function xmlUnescape(value) {
20485
- return value.replaceAll("&apos;", "'").replaceAll("&quot;", '"').replaceAll("&gt;", ">").replaceAll("&lt;", "<").replaceAll("&amp;", "&");
20486
- }
20487
- function plistValue(contents, key) {
20488
- const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
20489
- const match2 = contents.match(new RegExp(`<key>\\s*${escapedKey}\\s*</key>\\s*<string>([^<]*)</string>`));
20490
- return match2?.[1] ? xmlUnescape(match2[1]) : null;
20491
- }
20492
- function isMacLaunchAgentConfigured(expectedPm2Home, expectedRuntime) {
20493
- if (typeof process.getuid !== "function") return false;
20494
- const plistPath = launchAgentPath();
20495
- if (!existsSync6(plistPath)) return false;
20496
20374
  try {
20497
- const contents = readFileSync3(plistPath, "utf8");
20498
- const argumentsBlock = contents.match(
20499
- /<key>\s*ProgramArguments\s*<\/key>\s*<array>([\s\S]*?)<\/array>/
20500
- )?.[1];
20501
- const programArguments = argumentsBlock ? [...argumentsBlock.matchAll(/<string>([^<]*)<\/string>/g)].map((match2) => xmlUnescape(match2[1])) : [];
20502
- const bunPath = programArguments[0];
20503
- const supervisorEntry = programArguments[1];
20504
- const pm2Path = programArguments[2];
20505
- const pm2Home = plistValue(contents, "PM2_HOME");
20506
- const managementLock = plistValue(contents, "SUPERTASK_PM2_MANAGEMENT_LOCK");
20507
- if (!bunPath || !supervisorEntry || !pm2Path || !pm2Home || !managementLock) return false;
20508
- if (expectedPm2Home && pm2Home !== expectedPm2Home) return false;
20509
- const expectedManagementLock = expectedRuntime ? managementLockPath(expectedRuntime.env, expectedRuntime.cwd) : process.env.SUPERTASK_PM2_MANAGEMENT_LOCK ? managementLockPath() : join5(expectedPm2Home ?? currentScope().pm2Home, "supertask-gateway.manage.sqlite");
20510
- if (managementLock !== expectedManagementLock) return false;
20511
- accessSync(bunPath, constants.X_OK);
20512
- accessSync(supervisorEntry, constants.R_OK);
20513
- accessSync(pm2Path, constants.X_OK);
20514
- if (spawnSync2(bunPath, ["--version"], {
20515
- stdio: "ignore",
20516
- timeout: pm2CommandTimeoutMs(),
20517
- killSignal: "SIGKILL"
20518
- }).status !== 0) return false;
20519
- if (spawnSync2(pm2Path, ["--version"], {
20520
- stdio: "ignore",
20521
- env: { ...process.env, PM2_HOME: pm2Home },
20522
- timeout: pm2CommandTimeoutMs(),
20523
- killSignal: "SIGKILL"
20524
- }).status !== 0) return false;
20525
- const loaded = spawnSync2(
20526
- launchctlBin(),
20527
- ["print", `gui/${process.getuid()}/${MAC_LAUNCH_AGENT_LABEL}`],
20528
- {
20529
- encoding: "utf8",
20530
- stdio: ["ignore", "pipe", "ignore"],
20531
- timeout: pm2CommandTimeoutMs(),
20532
- killSignal: "SIGKILL"
20533
- }
20534
- );
20535
- if (loaded.status !== 0) return false;
20536
- if (!loaded.stdout.includes("state = running")) return false;
20537
- if (!loaded.stdout.includes(`path = ${plistPath}`)) return false;
20538
- if (!loaded.stdout.includes(`program = ${bunPath}`)) return false;
20539
- const dumpPath = join5(pm2Home, "dump.pm2");
20540
- const dump = JSON.parse(readFileSync3(dumpPath, "utf8"));
20541
- if (!Array.isArray(dump)) return false;
20542
- const gateway = dump.find((item) => item.name === PROCESS_NAME);
20543
- const dumpRuntime = gatewayRuntimeFromProcess(gateway);
20544
- if (!dumpRuntime || !hasRestorableSavedGatewayRuntime(gateway)) return false;
20545
- return expectedRuntime === void 0 || dumpRuntime.gatewayEntry === expectedRuntime.gatewayEntry && dumpRuntime.bunPath === expectedRuntime.bunPath && dumpRuntime.cwd === expectedRuntime.cwd && scopesMatch(runtimeScope(dumpRuntime), runtimeScope(expectedRuntime));
20375
+ process.kill(pid, "SIGKILL");
20546
20376
  } catch {
20547
- return false;
20548
20377
  }
20549
20378
  }
20550
- function systemctlBin(env = process.env) {
20551
- return env.SUPERTASK_SYSTEMCTL_BIN ?? "systemctl";
20379
+ function diagnosticEntry() {
20380
+ const extension = import.meta.url.endsWith(".ts") ? "ts" : "js";
20381
+ const moduleDir = dirname6(fileURLToPath5(import.meta.url));
20382
+ const candidates = [
20383
+ join5(moduleDir, `../daemon/gateway-diagnostic-runner.${extension}`),
20384
+ join5(moduleDir, `../../src/daemon/gateway-diagnostic-runner.${extension}`)
20385
+ ];
20386
+ const entry = candidates.find((candidate) => existsSync6(candidate));
20387
+ if (!entry) throw new Error(`Gateway diagnostic runner \u4E0D\u5B58\u5728\uFF1A${candidates.join(", ")}`);
20388
+ return entry;
20552
20389
  }
20553
- function isLinuxStartupConfigured(env = process.env, cwd = process.cwd(), expectedRuntime) {
20554
- const unit = env.SUPERTASK_PM2_SYSTEMD_UNIT ?? `pm2-${userInfo().username}.service`;
20555
- const enabled = spawnSync2(systemctlBin(env), ["is-enabled", unit], {
20556
- encoding: "utf8",
20557
- env,
20558
- stdio: ["ignore", "pipe", "ignore"],
20559
- timeout: pm2CommandTimeoutMs(env),
20560
- killSignal: "SIGKILL"
20561
- });
20562
- if (enabled.status !== 0 || enabled.stdout.trim() !== "enabled") return false;
20563
- const contents = spawnSync2(systemctlBin(env), ["cat", unit], {
20564
- encoding: "utf8",
20565
- env,
20566
- stdio: ["ignore", "pipe", "ignore"],
20567
- timeout: pm2CommandTimeoutMs(env),
20568
- killSignal: "SIGKILL"
20390
+ async function runGatewayDiagnostic() {
20391
+ return new Promise((resolve3, reject) => {
20392
+ const child = spawn3(process.execPath, [diagnosticEntry()], {
20393
+ cwd: process.cwd(),
20394
+ env: process.env,
20395
+ detached: process.platform !== "win32",
20396
+ stdio: ["ignore", "pipe", "pipe"]
20397
+ });
20398
+ let stdout = "";
20399
+ let stderr = "";
20400
+ let timedOut = false;
20401
+ if (child.pid) activeProcessGroups.add(child.pid);
20402
+ child.stdout?.on("data", (chunk) => {
20403
+ stdout += chunk.toString();
20404
+ });
20405
+ child.stderr?.on("data", (chunk) => {
20406
+ stderr += chunk.toString();
20407
+ });
20408
+ child.once("error", reject);
20409
+ child.once("close", (exitCode) => {
20410
+ clearTimeout(timer);
20411
+ if (child.pid) {
20412
+ killDiagnosticGroup(child.pid);
20413
+ activeProcessGroups.delete(child.pid);
20414
+ }
20415
+ if (timedOut) {
20416
+ reject(new Error(`Gateway diagnostic runner \u8D85\u8FC7 ${DIAGNOSTIC_TIMEOUT_MS}ms \u672A\u5B8C\u6210`));
20417
+ return;
20418
+ }
20419
+ if (exitCode !== 0) {
20420
+ reject(new Error(stderr.trim() || `Gateway diagnostic runner \u9000\u51FA\u7801 ${exitCode}`));
20421
+ return;
20422
+ }
20423
+ try {
20424
+ resolve3(JSON.parse(stdout));
20425
+ } catch (error) {
20426
+ reject(error);
20427
+ }
20428
+ });
20429
+ const timer = setTimeout(() => {
20430
+ timedOut = true;
20431
+ if (child.pid) killDiagnosticGroup(child.pid);
20432
+ else child.kill("SIGKILL");
20433
+ }, DIAGNOSTIC_TIMEOUT_MS);
20569
20434
  });
20570
- if (contents.status !== 0) return false;
20571
- const expectedPm2Home = env.PM2_HOME ?? join5(runtimeHome(env), ".pm2");
20572
- if (!contents.stdout.includes("resurrect") || !contents.stdout.includes(expectedPm2Home)) {
20573
- return false;
20574
- }
20575
- try {
20576
- const dump = JSON.parse(readFileSync3(join5(expectedPm2Home, "dump.pm2"), "utf8"));
20577
- if (!Array.isArray(dump)) return false;
20578
- const gateway = dump.find((item) => item.name === PROCESS_NAME);
20579
- const runtime = gatewayRuntimeFromProcess(gateway);
20580
- if (!runtime || !hasRestorableSavedGatewayRuntime(gateway)) return false;
20581
- return runtime.cwd === resolve3(cwd) && scopesMatch(runtimeScope(runtime), runtimeScope({ cwd, env })) && (expectedRuntime === void 0 || runtime.gatewayEntry === expectedRuntime.gatewayEntry && runtime.bunPath === expectedRuntime.bunPath);
20582
- } catch {
20583
- return false;
20584
- }
20585
20435
  }
20586
- function isPm2Installed(env = process.env) {
20587
- const result = spawnSync2(pm2Bin(env), ["--version"], {
20588
- stdio: "ignore",
20589
- env,
20590
- shell: process.platform === "win32",
20591
- timeout: pm2CommandTimeoutMs(env),
20592
- killSignal: "SIGKILL"
20436
+ async function getDashboardGatewayDiagnostic(options = {}) {
20437
+ if (!options.fresh && cached && cached.expiresAt > Date.now()) return cached.diagnostic;
20438
+ if (!options.fresh && pending) return pending;
20439
+ const operation = runGatewayDiagnostic().then((diagnostic) => {
20440
+ cached = { expiresAt: Date.now() + CACHE_TTL_MS, diagnostic };
20441
+ return diagnostic;
20593
20442
  });
20594
- return result.status === 0;
20595
- }
20596
- function resolveCommand(command) {
20597
- const lookup = process.platform === "win32" ? "where" : "which";
20598
- const result = spawnSync2(lookup, [command], { encoding: "utf8" });
20599
- return result.status === 0 ? result.stdout.trim().split("\n")[0] || null : null;
20600
- }
20601
- function npmPreferredEnvironment() {
20602
- if (process.platform === "win32") return null;
20603
- const npm = resolveCommand("npm");
20604
- const node = resolveCommand("node");
20605
- if (!npm || !node) return null;
20606
- const command = resolvePm2Bin();
20607
- const path = [.../* @__PURE__ */ new Set([
20608
- dirname6(command),
20609
- dirname6(npm),
20610
- dirname6(node),
20611
- "/usr/local/bin",
20612
- "/usr/bin",
20613
- "/bin"
20614
- ])].join(delimiter);
20615
- return { command, env: { ...process.env, PATH: path } };
20616
- }
20617
- function pm2Exec(args, options = {}) {
20618
- const npmEnvironment = options.preferNpm ? npmPreferredEnvironment() : null;
20619
- const effectiveEnv = options.env ?? npmEnvironment?.env ?? process.env;
20620
- const result = spawnSync2(npmEnvironment?.command ?? pm2Bin(effectiveEnv), args, {
20621
- stdio: ["pipe", "pipe", "pipe"],
20622
- encoding: "utf-8",
20623
- env: effectiveEnv,
20624
- shell: process.platform === "win32",
20625
- timeout: options.timeoutMs ?? pm2CommandTimeoutMs(effectiveEnv),
20626
- killSignal: "SIGKILL"
20443
+ if (options.fresh) return operation;
20444
+ pending = operation.finally(() => {
20445
+ pending = null;
20627
20446
  });
20628
- const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
20629
- if (result.error) return { ok: false, output: result.error.message };
20630
- return { ok: result.status === 0, output };
20631
- }
20632
- function requirePm2(args, action, options = {}) {
20633
- const result = pm2Exec(args, options);
20634
- if (!result.ok) throw new Error(`[supertask] ${action} failed: ${result.output || "unknown pm2 error"}`);
20635
- return result.output;
20636
- }
20637
- function pm2JsonList(env = process.env) {
20638
- const output = requirePm2(["jlist"], "pm2 jlist", { env });
20639
- try {
20640
- const parsed = JSON.parse(output);
20641
- if (!Array.isArray(parsed)) throw new Error("result is not an array");
20642
- return parsed;
20643
- } catch (error) {
20644
- const message = error instanceof Error ? error.message : String(error);
20645
- throw new Error(`[supertask] Invalid pm2 jlist output: ${message}`);
20646
- }
20647
- }
20648
- function gatewayEntryFromProcess(processInfo) {
20649
- const args = processInfo?.pm2_env?.args ?? processInfo?.args;
20650
- const candidates = Array.isArray(args) ? [...args].reverse() : typeof args === "string" ? [args] : [];
20651
- const savedCwd = processInfo?.pm2_env?.pm_cwd ?? processInfo?.pm_cwd;
20652
- for (const candidate of candidates) {
20653
- const path = typeof savedCwd === "string" ? runtimePath(candidate, savedCwd) : candidate;
20654
- if (existsSync6(path)) return resolve3(path);
20655
- }
20656
- return null;
20657
- }
20658
- function gatewayEnvironmentFromProcess(processInfo) {
20659
- const saved = processInfo?.pm2_env?.env ?? processInfo?.env;
20660
- if (!saved) return { ...process.env };
20661
- const env = {};
20662
- for (const [key, value] of Object.entries(saved)) {
20663
- if (typeof value === "string") env[key] = value;
20664
- }
20665
- return env;
20666
- }
20667
- function gatewayRuntimeFromProcess(processInfo) {
20668
- const gatewayEntry = gatewayEntryFromProcess(processInfo);
20669
- if (!gatewayEntry) return null;
20670
- const savedBunPath = processInfo?.pm2_env?.pm_exec_path ?? processInfo?.pm_exec_path;
20671
- const savedCwd = processInfo?.pm2_env?.pm_cwd ?? processInfo?.pm_cwd;
20672
- const savedEnv = gatewayEnvironmentFromProcess(processInfo);
20673
- if (typeof savedBunPath !== "string" || typeof savedCwd !== "string") return null;
20674
- try {
20675
- accessSync(savedBunPath, constants.X_OK);
20676
- if (!statSync3(savedCwd).isDirectory()) return null;
20677
- if (spawnSync2(savedBunPath, ["--version"], {
20678
- stdio: "ignore",
20679
- env: savedEnv,
20680
- timeout: pm2CommandTimeoutMs(savedEnv),
20681
- killSignal: "SIGKILL"
20682
- }).status !== 0) return null;
20683
- } catch {
20684
- return null;
20685
- }
20686
- const killTimeout = processInfo?.pm2_env?.kill_timeout ?? processInfo?.kill_timeout;
20687
- return {
20688
- gatewayEntry,
20689
- bunPath: savedBunPath,
20690
- cwd: resolve3(savedCwd),
20691
- env: savedEnv,
20692
- killTimeoutMs: Number.isInteger(killTimeout) && killTimeout >= 5e3 ? killTimeout : void 0
20693
- };
20694
- }
20695
- function hasRestorableSavedGatewayRuntime(processInfo) {
20696
- return gatewayRuntimeFromProcess(processInfo) !== null;
20697
- }
20698
- function databasePath(env = process.env, cwd = process.cwd()) {
20699
- return env.SUPERTASK_DB_PATH ? runtimePath(env.SUPERTASK_DB_PATH, cwd) : join5(runtimeHome(env), ".local/share/opencode/tasks.db");
20700
- }
20701
- function isGatewayReady(expectedPid, path = databasePath()) {
20702
- if (!existsSync6(path)) return false;
20703
- let database = null;
20704
- try {
20705
- database = new Database5(path, { readonly: true });
20706
- const row = database.query(
20707
- "SELECT pid, heartbeat_at, ready_at FROM gateway_lock WHERE id = 1"
20708
- ).get();
20709
- if (!row || row.ready_at == null) return false;
20710
- if (expectedPid !== void 0 && row.pid !== expectedPid) return false;
20711
- const ageMs = Date.now() - row.heartbeat_at;
20712
- return ageMs >= -5e3 && ageMs < GATEWAY_LOCK_STALE_MS;
20713
- } catch {
20714
- return false;
20715
- } finally {
20716
- database?.close();
20717
- }
20718
- }
20719
- function gatewayVersionFromLock(expectedPid, path) {
20720
- if (!existsSync6(path)) return void 0;
20721
- let database = null;
20722
- try {
20723
- database = new Database5(path, { readonly: true });
20724
- const row = database.query(
20725
- "SELECT pid, version FROM gateway_lock WHERE id = 1"
20726
- ).get();
20727
- if (!row || row.pid !== expectedPid) return void 0;
20728
- return row.version;
20729
- } catch {
20730
- return void 0;
20731
- } finally {
20732
- database?.close();
20733
- }
20447
+ return pending;
20734
20448
  }
20735
- function managementLockPath(env = process.env, cwd = process.cwd()) {
20736
- const override = env.SUPERTASK_PM2_MANAGEMENT_LOCK;
20737
- if (override) return runtimePath(override, cwd);
20738
- return join5(runtimeScope({ env, cwd }).pm2Home, "supertask-gateway.manage.sqlite");
20739
- }
20740
- function canonicalManagementLockPath(env = process.env, cwd = process.cwd()) {
20741
- const home = runtimeHome(env);
20742
- const pm2Home = env.PM2_HOME ? runtimePath(env.PM2_HOME, cwd) : join5(home, ".pm2");
20743
- return join5(pm2Home, "supertask-gateway.manage.sqlite");
20744
- }
20745
- var __dirname, PROCESS_NAME, MAC_LAUNCH_AGENT_LABEL, GATEWAY_LOCK_STALE_MS;
20746
- var init_pm2 = __esm({
20747
- "src/daemon/pm2.ts"() {
20449
+ var CACHE_TTL_MS, DIAGNOSTIC_TIMEOUT_MS, cached, pending, activeProcessGroups;
20450
+ var init_gateway_diagnostic = __esm({
20451
+ "src/web/gateway-diagnostic.ts"() {
20748
20452
  "use strict";
20749
- init_config();
20750
- init_package_version();
20751
- init_management_lock();
20752
- init_package_version();
20753
- __dirname = dirname6(fileURLToPath5(import.meta.url));
20754
- PROCESS_NAME = "supertask-gateway";
20755
- MAC_LAUNCH_AGENT_LABEL = "com.supertask.pm2-resurrect";
20756
- GATEWAY_LOCK_STALE_MS = 3e4;
20453
+ CACHE_TTL_MS = 5e3;
20454
+ DIAGNOSTIC_TIMEOUT_MS = 2e4;
20455
+ cached = null;
20456
+ pending = null;
20457
+ activeProcessGroups = /* @__PURE__ */ new Set();
20458
+ process.once("exit", () => {
20459
+ for (const pid of activeProcessGroups) killDiagnosticGroup(pid);
20460
+ });
20757
20461
  }
20758
20462
  });
20759
20463
 
@@ -21422,7 +21126,7 @@ var init_ui = __esm({
21422
21126
  "details.enabledYes": "\u5DF2\u542F\u7528",
21423
21127
  "details.enabledNo": "\u5DF2\u505C\u7528",
21424
21128
  "dialog.cancelTask": "\u53D6\u6D88\u4EFB\u52A1 #{id}\uFF1F",
21425
- "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",
21129
+ "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",
21426
21130
  "dialog.retryTask": "\u91CD\u8BD5\u4EFB\u52A1 #{id}\uFF1F",
21427
21131
  "dialog.retryTaskBody": "\u4EFB\u52A1\u5C06\u56DE\u5230\u5F85\u6267\u884C\u72B6\u6001\uFF0C\u5E76\u91CD\u7F6E\u81EA\u52A8\u91CD\u8BD5\u9884\u7B97\u3002",
21428
21132
  "dialog.deleteTask": "\u5220\u9664\u4EFB\u52A1 #{id}\uFF1F",
@@ -21727,7 +21431,7 @@ var init_ui = __esm({
21727
21431
  "details.enabledYes": "Enabled",
21728
21432
  "details.enabledNo": "Disabled",
21729
21433
  "dialog.cancelTask": "Cancel task #{id}?",
21730
- "dialog.cancelTaskBody": "A running task will terminate its process tree on the next worker poll.",
21434
+ "dialog.cancelTaskBody": "A running task will terminate its managed process group on the next worker poll.",
21731
21435
  "dialog.retryTask": "Retry task #{id}?",
21732
21436
  "dialog.retryTaskBody": "The task returns to pending and its automatic retry budget is reset.",
21733
21437
  "dialog.deleteTask": "Delete task #{id}?",
@@ -22149,14 +21853,14 @@ __export(web_exports, {
22149
21853
  });
22150
21854
  import {
22151
21855
  existsSync as existsSync7,
22152
- mkdirSync as mkdirSync4,
22153
- readFileSync as readFileSync4,
21856
+ mkdirSync as mkdirSync3,
21857
+ readFileSync as readFileSync3,
22154
21858
  readdirSync,
22155
21859
  renameSync as renameSync2,
22156
- statSync as statSync4,
22157
- writeFileSync as writeFileSync3
21860
+ statSync as statSync3,
21861
+ writeFileSync as writeFileSync2
22158
21862
  } from "fs";
22159
- import { homedir as homedir4 } from "os";
21863
+ import { homedir as homedir3 } from "os";
22160
21864
  import { basename as basename3, dirname as dirname7, join as join6 } from "path";
22161
21865
  function setDashboardRuntimeConfig(config) {
22162
21866
  runtimeConfig = config === null ? null : structuredClone(config);
@@ -22182,10 +21886,13 @@ function resolveDashboardConfigState(runtimeAvailable, restartRequired, managedR
22182
21886
  function isSafeDashboardRestartTarget(diagnostic, currentPid) {
22183
21887
  return diagnostic.pm2Installed && diagnostic.processFound && diagnostic.status === "online" && diagnostic.pid === currentPid && diagnostic.ready && diagnostic.scopeMatches;
22184
21888
  }
22185
- function canRestartCurrentGateway() {
21889
+ async function canRestartCurrentGateway(fresh = false) {
22186
21890
  if (runtimeConfig === null) return false;
22187
21891
  try {
22188
- return isSafeDashboardRestartTarget(getGatewayDiagnostic(), process.pid);
21892
+ return isSafeDashboardRestartTarget(
21893
+ await getDashboardGatewayDiagnostic({ fresh }),
21894
+ process.pid
21895
+ );
22189
21896
  } catch {
22190
21897
  return false;
22191
21898
  }
@@ -22204,7 +21911,7 @@ function listChildDirectories(path) {
22204
21911
  if (entry.isDirectory()) return [{ name: entry.name, path: entryPath, hidden: entry.name.startsWith(".") }];
22205
21912
  if (!entry.isSymbolicLink()) return [];
22206
21913
  try {
22207
- return statSync4(entryPath).isDirectory() ? [{ name: entry.name, path: entryPath, hidden: entry.name.startsWith(".") }] : [];
21914
+ return statSync3(entryPath).isDirectory() ? [{ name: entry.name, path: entryPath, hidden: entry.name.startsWith(".") }] : [];
22208
21915
  } catch {
22209
21916
  return [];
22210
21917
  }
@@ -22442,7 +22149,7 @@ function readCurrentConfig() {
22442
22149
  const configPath = getConfigPath();
22443
22150
  if (!existsSync7(configPath)) return {};
22444
22151
  try {
22445
- return JSON.parse(readFileSync4(configPath, "utf-8"));
22152
+ return JSON.parse(readFileSync3(configPath, "utf-8"));
22446
22153
  } catch {
22447
22154
  return {};
22448
22155
  }
@@ -22450,9 +22157,9 @@ function readCurrentConfig() {
22450
22157
  function writeConfig(cfg) {
22451
22158
  const configPath = getConfigPath();
22452
22159
  const dir = dirname7(configPath);
22453
- if (!existsSync7(dir)) mkdirSync4(dir, { recursive: true });
22160
+ if (!existsSync7(dir)) mkdirSync3(dir, { recursive: true });
22454
22161
  const tempPath = `${configPath}.${process.pid}.tmp`;
22455
- writeFileSync3(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
22162
+ writeFileSync2(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
22456
22163
  renameSync2(tempPath, configPath);
22457
22164
  }
22458
22165
  function statCard(value, label, tone, cardIcon, delay = "") {
@@ -22504,7 +22211,7 @@ function pagination(locale, basePath, page, pages, total, suffix = "") {
22504
22211
  const next = page < pages ? `<a class="btn" href="${basePath}?page=${page + 1}${suffix}">${t(locale, "pagination.next")}${icon("chevronRight")}</a>` : "";
22505
22212
  return `<div class="pagination">${previous}<span class="summary">${t(locale, "pagination.summary", { page, pages, total })}</span>${next}</div>`;
22506
22213
  }
22507
- var app, LEGACY_PROJECT_FILTER, TASK_STATUSES, SESSION_ID_PATTERN, runtimeConfig, restartScheduled, dashboardApp, web_default;
22214
+ var app, LEGACY_PROJECT_FILTER, TASK_STATUSES, SESSION_ID_PATTERN, LOOPBACK_HOST_PATTERN, runtimeConfig, restartScheduled, dashboardApp, web_default;
22508
22215
  var init_web = __esm({
22509
22216
  "src/web/index.tsx"() {
22510
22217
  "use strict";
@@ -22521,7 +22228,7 @@ var init_web = __esm({
22521
22228
  init_config();
22522
22229
  init_health();
22523
22230
  init_job_templates();
22524
- init_pm2();
22231
+ init_gateway_diagnostic();
22525
22232
  init_ui();
22526
22233
  app = new Hono2();
22527
22234
  LEGACY_PROJECT_FILTER = "__supertask_legacy__";
@@ -22534,9 +22241,18 @@ var init_web = __esm({
22534
22241
  "cancelled"
22535
22242
  ]);
22536
22243
  SESSION_ID_PATTERN = /^ses_[A-Za-z0-9_]+$/;
22244
+ LOOPBACK_HOST_PATTERN = /^(?:localhost|127\.0\.0\.1|\[::1\])(?::([1-9]\d{0,4}))?$/i;
22537
22245
  runtimeConfig = null;
22538
22246
  restartScheduled = false;
22539
22247
  app.use("*", async (c, next) => {
22248
+ const requestHostname = new URL(c.req.url).hostname;
22249
+ const hostHeader = c.req.header("Host");
22250
+ const loopbackHosts = ["localhost", "127.0.0.1", "[::1]"];
22251
+ const hostMatch = hostHeader?.match(LOOPBACK_HOST_PATTERN) ?? null;
22252
+ const hostPort = hostMatch?.[1] === void 0 ? null : Number(hostMatch[1]);
22253
+ if (!loopbackHosts.includes(requestHostname) || hostHeader !== void 0 && (!hostMatch || hostPort !== null && hostPort > 65535)) {
22254
+ return c.json({ error: "invalid dashboard host" }, 421);
22255
+ }
22540
22256
  await next();
22541
22257
  c.header("X-Content-Type-Options", "nosniff");
22542
22258
  c.header("X-Frame-Options", "DENY");
@@ -22568,13 +22284,13 @@ var init_web = __esm({
22568
22284
  return c.json(health, health.status === "ok" ? 200 : 503);
22569
22285
  });
22570
22286
  app.get("/api/filesystem/directories", (c) => {
22571
- const requestedPath = c.req.query("path")?.trim() || homedir4();
22287
+ const requestedPath = c.req.query("path")?.trim() || homedir3();
22572
22288
  try {
22573
22289
  validateTaskWorkingDirectory(requestedPath);
22574
22290
  return c.json({
22575
22291
  path: requestedPath,
22576
22292
  parent: dirname7(requestedPath),
22577
- home: homedir4(),
22293
+ home: homedir3(),
22578
22294
  directories: listChildDirectories(requestedPath)
22579
22295
  });
22580
22296
  } catch (error) {
@@ -22908,7 +22624,7 @@ var init_web = __esm({
22908
22624
  const config = loadConfig();
22909
22625
  const activeConfig = runtimeConfig ?? config;
22910
22626
  const restartRequired = runtimeConfig !== null && !configsEqual(config, runtimeConfig);
22911
- const managedRestart = canRestartCurrentGateway();
22627
+ const managedRestart = await canRestartCurrentGateway();
22912
22628
  const configState = resolveDashboardConfigState(
22913
22629
  runtimeConfig !== null,
22914
22630
  restartRequired,
@@ -23028,9 +22744,9 @@ var init_web = __esm({
23028
22744
  if (!isValidSessionId(run.sessionId)) return c.json({ error: "session unavailable" }, 409);
23029
22745
  return c.json({ command: sessionCommand(run.sessionId) });
23030
22746
  });
23031
- app.get("/api/gateway/status", (c) => {
22747
+ app.get("/api/gateway/status", async (c) => {
23032
22748
  const savedConfig = loadConfig();
23033
- const managed = canRestartCurrentGateway();
22749
+ const managed = await canRestartCurrentGateway();
23034
22750
  return c.json({
23035
22751
  pid: process.pid,
23036
22752
  managed,
@@ -23155,7 +22871,7 @@ var init_web = __esm({
23155
22871
  return c.json({
23156
22872
  success: true,
23157
22873
  restartRequired: runtimeConfig === null || !configsEqual(savedConfig, runtimeConfig),
23158
- managed: canRestartCurrentGateway()
22874
+ managed: await canRestartCurrentGateway()
23159
22875
  });
23160
22876
  } catch (error) {
23161
22877
  return c.json({
@@ -23171,9 +22887,10 @@ var init_web = __esm({
23171
22887
  return c.json({ error: "confirmation must be RESTART" }, 400);
23172
22888
  }
23173
22889
  if (restartScheduled) return c.json({ error: "restart already scheduled" }, 409);
23174
- if (!canRestartCurrentGateway()) {
22890
+ if (!await canRestartCurrentGateway(true)) {
23175
22891
  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);
23176
22892
  }
22893
+ if (restartScheduled) return c.json({ error: "restart already scheduled" }, 409);
23177
22894
  restartScheduled = true;
23178
22895
  const previousPid = process.pid;
23179
22896
  setTimeout(() => {
@@ -23237,7 +22954,7 @@ function runCommandContext(executable, args, cwd) {
23237
22954
  function assertWorkerProcessIsolationSupported(platform = process.platform) {
23238
22955
  if (platform === "win32") {
23239
22956
  throw new Error(
23240
- "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"
22957
+ "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"
23241
22958
  );
23242
22959
  }
23243
22960
  }
@@ -23248,17 +22965,24 @@ var WorkerEngine = class {
23248
22965
  pollTimer = null;
23249
22966
  heartbeatTimer = null;
23250
22967
  pollCyclePromise = null;
22968
+ shutdownDeadlineMs = null;
22969
+ settlementRetryWakeups = /* @__PURE__ */ new Set();
23251
22970
  cfg;
23252
22971
  opencodeBin;
23253
22972
  maxOutputChars;
22973
+ settlementRetryDelaysMs;
22974
+ settlementRetryIntervalMs;
23254
22975
  constructor(cfg, options = {}) {
23255
22976
  this.cfg = cfg.worker;
23256
22977
  this.opencodeBin = options.opencodeBin ?? process.env.SUPERTASK_OPENCODE_BIN ?? "opencode";
23257
22978
  this.maxOutputChars = options.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
22979
+ this.settlementRetryDelaysMs = options.settlementRetryDelaysMs ?? [250, 1e3, 4e3];
22980
+ this.settlementRetryIntervalMs = options.settlementRetryIntervalMs ?? 5e3;
23258
22981
  }
23259
22982
  start() {
23260
22983
  assertWorkerProcessIsolationSupported();
23261
22984
  this.stopped = false;
22985
+ this.shutdownDeadlineMs = null;
23262
22986
  markGatewayActivity("worker");
23263
22987
  this.poll();
23264
22988
  this.heartbeatTimer = setInterval(() => {
@@ -23267,6 +22991,8 @@ var WorkerEngine = class {
23267
22991
  }
23268
22992
  async stop(gracePeriodMs = 0) {
23269
22993
  this.stopped = true;
22994
+ this.shutdownDeadlineMs = Date.now() + Math.max(0, gracePeriodMs);
22995
+ for (const wake of [...this.settlementRetryWakeups]) wake();
23270
22996
  if (this.pollTimer) {
23271
22997
  clearTimeout(this.pollTimer);
23272
22998
  this.pollTimer = null;
@@ -23336,21 +23062,18 @@ var WorkerEngine = class {
23336
23062
  if (databaseRunningCount >= this.cfg.maxConcurrency) break;
23337
23063
  let task;
23338
23064
  try {
23339
- task = await TaskService.next({ excludedBatchIds: [...this.activeBatchIds] });
23065
+ task = await TaskService.claimNext({ excludedBatchIds: [...this.activeBatchIds] });
23340
23066
  } catch (err) {
23341
23067
  this.logError("task claim failed", err);
23342
23068
  throw err;
23343
23069
  }
23344
23070
  if (!task) break;
23345
- if (this.stopped) break;
23346
- if (!await TaskService.start(task.id)) continue;
23347
- const batchId = normalizeTaskBatchId(task.batchId);
23348
- if (batchId) this.activeBatchIds.add(batchId);
23349
23071
  if (this.stopped) {
23350
23072
  await TaskService.resetRunningToPending([task.id]);
23351
- this.releaseBatch(task);
23352
23073
  break;
23353
23074
  }
23075
+ const batchId = normalizeTaskBatchId(task.batchId);
23076
+ if (batchId) this.activeBatchIds.add(batchId);
23354
23077
  let runId = null;
23355
23078
  try {
23356
23079
  const launchIdentity = `gateway-${process.pid}:launch:${randomUUID()}`;
@@ -23483,8 +23206,8 @@ var WorkerEngine = class {
23483
23206
  }
23484
23207
  });
23485
23208
  let spawnError = null;
23486
- const spawned = new Promise((resolve4, reject) => {
23487
- child.once("spawn", resolve4);
23209
+ const spawned = new Promise((resolve3, reject) => {
23210
+ child.once("spawn", resolve3);
23488
23211
  child.once("error", (error) => {
23489
23212
  spawnError = error;
23490
23213
  reject(error);
@@ -23532,10 +23255,10 @@ var WorkerEngine = class {
23532
23255
  return;
23533
23256
  }
23534
23257
  try {
23535
- await new Promise((resolve4, reject) => {
23258
+ await new Promise((resolve3, reject) => {
23536
23259
  child.stdin.end("START\n", (error) => {
23537
23260
  if (error) reject(error);
23538
- else resolve4();
23261
+ else resolve3();
23539
23262
  });
23540
23263
  });
23541
23264
  } catch (error) {
@@ -23561,7 +23284,7 @@ var WorkerEngine = class {
23561
23284
  if (!entry.guardianDrained) {
23562
23285
  const pid = entry.child.pid;
23563
23286
  const presence = pid == null ? spawnError == null ? "unknown" : "not-running" : inspectSpawnedProcessTreePresence(pid);
23564
- const message = "guardian \u672A\u63D0\u4F9B\u8FDB\u7A0B\u6811\u6392\u7A7A\u8BC1\u660E";
23287
+ const message = "guardian \u672A\u63D0\u4F9B\u53D7\u7BA1\u8FDB\u7A0B\u7EC4\u6392\u7A7A\u8BC1\u660E";
23565
23288
  if (presence !== "not-running") {
23566
23289
  if (entry.timeoutTimer) {
23567
23290
  clearTimeout(entry.timeoutTimer);
@@ -23597,17 +23320,48 @@ var WorkerEngine = class {
23597
23320
  clearTimeout(entry.timeoutTimer);
23598
23321
  entry.timeoutTimer = null;
23599
23322
  }
23600
- const settlement = this.commitEntry(entry, code, failure).then(() => true).catch((error) => {
23601
- markGatewayFailure("worker", error);
23602
- this.logError("task settlement failed", error, entry.task.id);
23603
- return false;
23604
- }).finally(() => {
23323
+ const settlement = this.commitEntryWithRetry(entry, code, failure).finally(() => {
23605
23324
  this.runningTasks.delete(entry.task.id);
23606
23325
  this.releaseBatch(entry.task);
23607
23326
  });
23608
23327
  entry.settlementPromise = settlement;
23609
23328
  return settlement;
23610
23329
  }
23330
+ async commitEntryWithRetry(entry, code, failure) {
23331
+ for (let attempt = 0; ; attempt += 1) {
23332
+ try {
23333
+ await this.commitEntry(entry, code, failure);
23334
+ return true;
23335
+ } catch (error) {
23336
+ markGatewayFailure("worker", error);
23337
+ const shortRetryDelayMs = this.settlementRetryDelaysMs[attempt];
23338
+ let retryDelayMs = shortRetryDelayMs ?? this.settlementRetryIntervalMs;
23339
+ if (this.stopped) {
23340
+ const remainingMs = Math.max(0, (this.shutdownDeadlineMs ?? 0) - Date.now());
23341
+ if (remainingMs === 0) return false;
23342
+ retryDelayMs = Math.min(retryDelayMs, remainingMs);
23343
+ }
23344
+ this.logError(
23345
+ `task settlement failed; retrying in ${retryDelayMs}ms`,
23346
+ error,
23347
+ entry.task.id
23348
+ );
23349
+ await this.waitForSettlementRetry(retryDelayMs);
23350
+ }
23351
+ }
23352
+ }
23353
+ waitForSettlementRetry(delayMs) {
23354
+ return new Promise((resolve3) => {
23355
+ let timer;
23356
+ const finish = () => {
23357
+ clearTimeout(timer);
23358
+ this.settlementRetryWakeups.delete(finish);
23359
+ resolve3();
23360
+ };
23361
+ timer = setTimeout(finish, delayMs);
23362
+ this.settlementRetryWakeups.add(finish);
23363
+ });
23364
+ }
23611
23365
  async commitEntry(entry, code, failure) {
23612
23366
  const termination = entry.termination;
23613
23367
  if (termination?.kind === "shutdown") return;
@@ -24174,7 +23928,29 @@ init_db2();
24174
23928
  init_task_service();
24175
23929
  init_health();
24176
23930
  init_process_control();
24177
- init_package_version();
23931
+
23932
+ // src/core/package-version.ts
23933
+ import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
23934
+ import { dirname as dirname4, join as join4 } from "path";
23935
+ import { fileURLToPath as fileURLToPath4 } from "url";
23936
+ function getPackageVersion() {
23937
+ let directory = dirname4(fileURLToPath4(import.meta.url));
23938
+ for (let depth = 0; depth < 5; depth += 1) {
23939
+ const packagePath = join4(directory, "package.json");
23940
+ if (existsSync4(packagePath)) {
23941
+ try {
23942
+ const pkg = JSON.parse(readFileSync2(packagePath, "utf8"));
23943
+ if (typeof pkg.version === "string") return pkg.version;
23944
+ } catch {
23945
+ return "0.0.0";
23946
+ }
23947
+ }
23948
+ directory = dirname4(directory);
23949
+ }
23950
+ return "0.0.0";
23951
+ }
23952
+
23953
+ // src/gateway/index.ts
24178
23954
  var STALE_THRESHOLD_MS = 3e4;
24179
23955
  async function runGatewayShutdownStep(failures, step, operation) {
24180
23956
  try {