opencode-supertask 0.1.21 → 0.1.23

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.
@@ -6543,6 +6543,23 @@ var init_task_service = __esm({
6543
6543
  ).returning();
6544
6544
  return result.length;
6545
6545
  }
6546
+ static async resetOrphanRunningToPending() {
6547
+ const result = await db.update(tasks2).set({
6548
+ status: "pending",
6549
+ startedAt: null,
6550
+ finishedAt: null
6551
+ }).where(
6552
+ and(
6553
+ eq(tasks2.status, "running"),
6554
+ sql`NOT EXISTS (
6555
+ SELECT 1 FROM ${taskRuns2}
6556
+ WHERE ${taskRuns2.taskId} = ${tasks2.id}
6557
+ AND ${taskRuns2.status} = 'running'
6558
+ )`
6559
+ )
6560
+ ).returning();
6561
+ return result.length;
6562
+ }
6546
6563
  static async cancel(id, scope = {}) {
6547
6564
  const conditions = [
6548
6565
  eq(tasks2.id, id),
@@ -6878,6 +6895,80 @@ var init_health = __esm({
6878
6895
  }
6879
6896
  });
6880
6897
 
6898
+ // src/core/process-control.ts
6899
+ import { spawnSync } from "child_process";
6900
+ import { basename } from "path";
6901
+ function isSafePid(pid) {
6902
+ return Number.isInteger(pid) && pid > 1 && pid !== process.pid;
6903
+ }
6904
+ function isProcessAlive(pid) {
6905
+ if (!Number.isInteger(pid) || pid <= 0) return false;
6906
+ try {
6907
+ process.kill(pid, 0);
6908
+ return true;
6909
+ } catch (error) {
6910
+ return error instanceof Error && "code" in error && error.code === "EPERM";
6911
+ }
6912
+ }
6913
+ function inspectUnixProcess(pid) {
6914
+ const result = spawnSync("ps", ["-o", "pgid=", "-o", "command=", "-p", String(pid)], {
6915
+ encoding: "utf8",
6916
+ stdio: ["ignore", "pipe", "ignore"]
6917
+ });
6918
+ if (result.status !== 0) return null;
6919
+ const match2 = result.stdout.trim().match(/^(\d+)\s+(.+)$/s);
6920
+ if (!match2) return null;
6921
+ return { processGroupId: Number(match2[1]), command: match2[2] };
6922
+ }
6923
+ function commandMatches(command, expectedExecutable) {
6924
+ const expectedName = basename(expectedExecutable).trim().toLowerCase();
6925
+ if (!expectedName) return false;
6926
+ return command.toLowerCase().includes(expectedName);
6927
+ }
6928
+ function signalSpawnedProcessTree(pid, signal) {
6929
+ if (!isSafePid(pid)) return false;
6930
+ if (process.platform !== "win32") {
6931
+ try {
6932
+ process.kill(-pid, signal);
6933
+ return true;
6934
+ } catch {
6935
+ }
6936
+ }
6937
+ try {
6938
+ process.kill(pid, signal);
6939
+ return true;
6940
+ } catch {
6941
+ return false;
6942
+ }
6943
+ }
6944
+ function signalRecordedProcessTree(pid, signal, expectedExecutable) {
6945
+ if (!isSafePid(pid)) return false;
6946
+ if (process.platform === "win32") {
6947
+ const list = spawnSync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
6948
+ encoding: "utf8",
6949
+ stdio: ["ignore", "pipe", "ignore"]
6950
+ });
6951
+ if (list.status !== 0 || !commandMatches(list.stdout, expectedExecutable)) return false;
6952
+ const args = ["/PID", String(pid), "/T"];
6953
+ if (signal === "SIGKILL") args.push("/F");
6954
+ return spawnSync("taskkill", args, { stdio: "ignore" }).status === 0;
6955
+ }
6956
+ const info = inspectUnixProcess(pid);
6957
+ if (!info || !commandMatches(info.command, expectedExecutable)) return false;
6958
+ try {
6959
+ if (info.processGroupId === pid) process.kill(-pid, signal);
6960
+ else process.kill(pid, signal);
6961
+ return true;
6962
+ } catch {
6963
+ return false;
6964
+ }
6965
+ }
6966
+ var init_process_control = __esm({
6967
+ "src/core/process-control.ts"() {
6968
+ "use strict";
6969
+ }
6970
+ });
6971
+
6881
6972
  // node_modules/cron-parser/dist/fields/types.js
6882
6973
  var require_types = __commonJS({
6883
6974
  "node_modules/cron-parser/dist/fields/types.js"(exports) {
@@ -14091,26 +14182,26 @@ var require_CronDate = __commonJS({
14091
14182
  * @param {CronDate | Date | number | string} [timestamp] - The timestamp to initialize the CronDate with.
14092
14183
  * @param {string} [tz] - The timezone to use for the CronDate.
14093
14184
  */
14094
- constructor(timestamp, tz) {
14185
+ constructor(timestamp2, tz) {
14095
14186
  const dateOpts = { zone: tz };
14096
- if (!timestamp) {
14187
+ if (!timestamp2) {
14097
14188
  this.#date = luxon_1.DateTime.local();
14098
- } else if (timestamp instanceof _CronDate) {
14099
- this.#date = timestamp.#date;
14100
- this.#dstStart = timestamp.#dstStart;
14101
- this.#dstEnd = timestamp.#dstEnd;
14102
- } else if (timestamp instanceof Date) {
14103
- this.#date = luxon_1.DateTime.fromJSDate(timestamp, dateOpts);
14104
- } else if (typeof timestamp === "number") {
14105
- this.#date = luxon_1.DateTime.fromMillis(timestamp, dateOpts);
14189
+ } else if (timestamp2 instanceof _CronDate) {
14190
+ this.#date = timestamp2.#date;
14191
+ this.#dstStart = timestamp2.#dstStart;
14192
+ this.#dstEnd = timestamp2.#dstEnd;
14193
+ } else if (timestamp2 instanceof Date) {
14194
+ this.#date = luxon_1.DateTime.fromJSDate(timestamp2, dateOpts);
14195
+ } else if (typeof timestamp2 === "number") {
14196
+ this.#date = luxon_1.DateTime.fromMillis(timestamp2, dateOpts);
14106
14197
  } else {
14107
- this.#date = luxon_1.DateTime.fromISO(timestamp, dateOpts);
14108
- this.#date.isValid || (this.#date = luxon_1.DateTime.fromRFC2822(timestamp, dateOpts));
14109
- this.#date.isValid || (this.#date = luxon_1.DateTime.fromSQL(timestamp, dateOpts));
14110
- this.#date.isValid || (this.#date = luxon_1.DateTime.fromFormat(timestamp, "EEE, d MMM yyyy HH:mm:ss", dateOpts));
14198
+ this.#date = luxon_1.DateTime.fromISO(timestamp2, dateOpts);
14199
+ this.#date.isValid || (this.#date = luxon_1.DateTime.fromRFC2822(timestamp2, dateOpts));
14200
+ this.#date.isValid || (this.#date = luxon_1.DateTime.fromSQL(timestamp2, dateOpts));
14201
+ this.#date.isValid || (this.#date = luxon_1.DateTime.fromFormat(timestamp2, "EEE, d MMM yyyy HH:mm:ss", dateOpts));
14111
14202
  }
14112
14203
  if (!this.#date.isValid) {
14113
- throw new Error(`CronDate: unhandled timestamp: ${timestamp}`);
14204
+ throw new Error(`CronDate: unhandled timestamp: ${timestamp2}`);
14114
14205
  }
14115
14206
  if (tz && tz !== this.#date.zoneName) {
14116
14207
  this.#date = this.#date.setZone(tz);
@@ -18502,14 +18593,346 @@ var init_html2 = __esm({
18502
18593
  }
18503
18594
  });
18504
18595
 
18596
+ // src/core/services/database-maintenance.service.ts
18597
+ import { Database as Database3 } from "bun:sqlite";
18598
+ import { randomUUID } from "crypto";
18599
+ import {
18600
+ chmodSync,
18601
+ constants,
18602
+ copyFileSync,
18603
+ existsSync as existsSync3,
18604
+ mkdirSync as mkdirSync2,
18605
+ renameSync,
18606
+ statSync,
18607
+ unlinkSync,
18608
+ writeFileSync
18609
+ } from "fs";
18610
+ import { tmpdir } from "os";
18611
+ import { basename as basename2, dirname as dirname2, resolve } from "path";
18612
+ function timestamp() {
18613
+ return (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
18614
+ }
18615
+ function normalizedPath(path) {
18616
+ return path === ":memory:" ? path : resolve(path);
18617
+ }
18618
+ function safeUnlink(path) {
18619
+ try {
18620
+ unlinkSync(path);
18621
+ } catch (error) {
18622
+ const code = error instanceof Error && "code" in error ? error.code : void 0;
18623
+ if (code !== "ENOENT") throw error;
18624
+ }
18625
+ }
18626
+ function errorMessage(error) {
18627
+ return error instanceof Error ? error.message : String(error);
18628
+ }
18629
+ var REQUIRED_TABLES, DatabaseMaintenanceConflictError, DatabaseMaintenanceService;
18630
+ var init_database_maintenance_service = __esm({
18631
+ "src/core/services/database-maintenance.service.ts"() {
18632
+ "use strict";
18633
+ init_db2();
18634
+ init_process_control();
18635
+ REQUIRED_TABLES = ["gateway_lock", "tasks", "task_runs", "task_templates"];
18636
+ DatabaseMaintenanceConflictError = class extends Error {
18637
+ constructor(message) {
18638
+ super(message);
18639
+ this.name = "DatabaseMaintenanceConflictError";
18640
+ }
18641
+ };
18642
+ DatabaseMaintenanceService = class {
18643
+ static check() {
18644
+ return this.inspect(getSqlite(), DB_FILE_PATH);
18645
+ }
18646
+ static backup(outputPath) {
18647
+ const sqlite2 = getSqlite();
18648
+ try {
18649
+ sqlite2.query("PRAGMA wal_checkpoint(PASSIVE)").get();
18650
+ } catch {
18651
+ }
18652
+ return this.writeSnapshot(sqlite2, outputPath ?? this.createBackupPath("backup"));
18653
+ }
18654
+ static clear(options = {}) {
18655
+ const sqlite2 = getSqlite();
18656
+ this.assertGatewaySafe(sqlite2, options.allowCurrentGateway ?? false);
18657
+ this.assertNoRunningWork(sqlite2);
18658
+ sqlite2.exec("BEGIN IMMEDIATE");
18659
+ let backup = null;
18660
+ try {
18661
+ this.assertGatewaySafe(sqlite2, options.allowCurrentGateway ?? false);
18662
+ this.assertNoRunningWork(sqlite2);
18663
+ const before = this.readCounts(sqlite2);
18664
+ backup = this.writeSnapshot(sqlite2, this.createBackupPath("pre-clear"));
18665
+ sqlite2.exec("DELETE FROM task_runs");
18666
+ sqlite2.exec("DELETE FROM tasks");
18667
+ sqlite2.exec("DELETE FROM task_templates");
18668
+ sqlite2.exec("COMMIT");
18669
+ return {
18670
+ backupPath: backup.path,
18671
+ deleted: before,
18672
+ check: this.inspect(sqlite2, DB_FILE_PATH)
18673
+ };
18674
+ } catch (error) {
18675
+ if (sqlite2.inTransaction) sqlite2.exec("ROLLBACK");
18676
+ if (error instanceof DatabaseMaintenanceConflictError) throw error;
18677
+ const backupHint = backup ? `\uFF1B\u4E8B\u52A1\u5DF2\u56DE\u6EDA\uFF0C\u5907\u4EFD\u4FDD\u7559\u5728 ${backup.path}` : "";
18678
+ throw new Error(`\u6E05\u7A7A\u6570\u636E\u5E93\u5931\u8D25${backupHint}\uFF1A${errorMessage(error)}`);
18679
+ }
18680
+ }
18681
+ static restore(sourcePath) {
18682
+ if (DB_FILE_PATH === ":memory:") {
18683
+ throw new Error("\u5185\u5B58\u6570\u636E\u5E93\u4E0D\u652F\u6301 restore");
18684
+ }
18685
+ const source = normalizedPath(sourcePath);
18686
+ const livePath = normalizedPath(DB_FILE_PATH);
18687
+ if (source === livePath) throw new Error("\u6062\u590D\u6765\u6E90\u4E0D\u80FD\u662F\u5F53\u524D\u6570\u636E\u5E93\u6587\u4EF6");
18688
+ const current = getSqlite();
18689
+ this.assertGatewaySafe(current, false);
18690
+ this.assertNoRunningWork(current);
18691
+ if (!existsSync3(source) || !statSync(source).isFile()) {
18692
+ throw new Error(`\u5907\u4EFD\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${source}`);
18693
+ }
18694
+ let sourceCheck;
18695
+ try {
18696
+ sourceCheck = this.inspectFile(source);
18697
+ } catch (error) {
18698
+ throw new Error(`\u5907\u4EFD\u6587\u4EF6\u65E0\u6548\uFF1A${errorMessage(error)}`);
18699
+ }
18700
+ if (!sourceCheck.ok) {
18701
+ throw new Error(`\u5907\u4EFD\u6587\u4EF6\u6821\u9A8C\u5931\u8D25\uFF1A${sourceCheck.integrityMessages.join("; ") || "schema/foreign key error"}`);
18702
+ }
18703
+ const stagePath = `${livePath}.restore-${process.pid}-${randomUUID()}.tmp`;
18704
+ const rollbackPath = `${livePath}.rollback-${process.pid}-${randomUUID()}.tmp`;
18705
+ let safetyBackup = null;
18706
+ let liveMoved = false;
18707
+ try {
18708
+ copyFileSync(source, stagePath, constants.COPYFILE_EXCL);
18709
+ chmodSync(stagePath, 384);
18710
+ const staged = new Database3(stagePath);
18711
+ try {
18712
+ const stagedCheck = this.inspect(staged, stagePath);
18713
+ if (!stagedCheck.ok) throw new Error("\u6682\u5B58\u6062\u590D\u6587\u4EF6\u6821\u9A8C\u5931\u8D25");
18714
+ const now = Date.now();
18715
+ staged.query(
18716
+ "INSERT OR REPLACE INTO gateway_lock (id, pid, acquired_at, heartbeat_at, ready_at) VALUES (1, ?, ?, ?, NULL)"
18717
+ ).run(process.pid, now, now);
18718
+ } finally {
18719
+ staged.close();
18720
+ }
18721
+ const sqlite2 = getSqlite();
18722
+ sqlite2.exec("BEGIN IMMEDIATE");
18723
+ try {
18724
+ this.assertGatewaySafe(sqlite2, false);
18725
+ this.assertNoRunningWork(sqlite2);
18726
+ safetyBackup = this.writeSnapshot(sqlite2, this.createBackupPath("pre-restore"));
18727
+ const now = Date.now();
18728
+ sqlite2.query(
18729
+ "INSERT OR REPLACE INTO gateway_lock (id, pid, acquired_at, heartbeat_at, ready_at) VALUES (1, ?, ?, ?, NULL)"
18730
+ ).run(process.pid, now, now);
18731
+ sqlite2.exec("COMMIT");
18732
+ } catch (error) {
18733
+ if (sqlite2.inTransaction) sqlite2.exec("ROLLBACK");
18734
+ throw error;
18735
+ }
18736
+ const checkpoint = sqlite2.query("PRAGMA wal_checkpoint(TRUNCATE)").get();
18737
+ if (checkpoint && checkpoint.busy !== 0) {
18738
+ throw new DatabaseMaintenanceConflictError("\u6570\u636E\u5E93\u4ECD\u88AB\u5176\u4ED6\u8FDE\u63A5\u5360\u7528\uFF0C\u65E0\u6CD5\u5B89\u5168\u6062\u590D");
18739
+ }
18740
+ closeDb();
18741
+ safeUnlink(`${livePath}-wal`);
18742
+ safeUnlink(`${livePath}-shm`);
18743
+ renameSync(livePath, rollbackPath);
18744
+ liveMoved = true;
18745
+ renameSync(stagePath, livePath);
18746
+ const restored = getSqlite();
18747
+ restored.exec("BEGIN IMMEDIATE");
18748
+ let recoveredRunningTasks = 0;
18749
+ let closedRunningRuns = 0;
18750
+ try {
18751
+ recoveredRunningTasks = this.scalar(
18752
+ restored,
18753
+ "SELECT COUNT(*) AS count FROM tasks WHERE status = 'running'"
18754
+ );
18755
+ closedRunningRuns = this.scalar(
18756
+ restored,
18757
+ "SELECT COUNT(*) AS count FROM task_runs WHERE status = 'running'"
18758
+ );
18759
+ const finishedAt = Math.floor(Date.now() / 1e3);
18760
+ restored.query(`
18761
+ UPDATE task_runs
18762
+ SET status = 'failed', finished_at = ?,
18763
+ log = CASE
18764
+ WHEN log IS NULL OR log = '' THEN '\u6570\u636E\u5E93\u6062\u590D\u65F6\u5173\u95ED\u9057\u7559\u8FD0\u884C\u8BB0\u5F55'
18765
+ ELSE log || '
18766
+ \u6570\u636E\u5E93\u6062\u590D\u65F6\u5173\u95ED\u9057\u7559\u8FD0\u884C\u8BB0\u5F55'
18767
+ END
18768
+ WHERE status = 'running'
18769
+ `).run(finishedAt);
18770
+ restored.exec(`
18771
+ UPDATE tasks
18772
+ SET status = 'pending', started_at = NULL, finished_at = NULL
18773
+ WHERE status = 'running'
18774
+ `);
18775
+ restored.query("DELETE FROM gateway_lock WHERE pid = ?").run(process.pid);
18776
+ restored.exec("COMMIT");
18777
+ } catch (error) {
18778
+ if (restored.inTransaction) restored.exec("ROLLBACK");
18779
+ throw error;
18780
+ }
18781
+ const check = this.inspect(restored, livePath);
18782
+ if (!check.ok) throw new Error("\u6062\u590D\u540E\u7684\u6570\u636E\u5E93\u672A\u901A\u8FC7\u5B8C\u6574\u6027\u6821\u9A8C");
18783
+ safeUnlink(rollbackPath);
18784
+ return {
18785
+ sourcePath: source,
18786
+ safetyBackupPath: safetyBackup.path,
18787
+ recoveredRunningTasks,
18788
+ closedRunningRuns,
18789
+ check
18790
+ };
18791
+ } catch (error) {
18792
+ closeDb();
18793
+ safeUnlink(`${livePath}-wal`);
18794
+ safeUnlink(`${livePath}-shm`);
18795
+ safeUnlink(stagePath);
18796
+ safeUnlink(`${stagePath}-journal`);
18797
+ safeUnlink(`${stagePath}-wal`);
18798
+ safeUnlink(`${stagePath}-shm`);
18799
+ if (liveMoved && existsSync3(rollbackPath)) {
18800
+ safeUnlink(livePath);
18801
+ renameSync(rollbackPath, livePath);
18802
+ try {
18803
+ const original = getSqlite();
18804
+ original.query("DELETE FROM gateway_lock WHERE pid = ?").run(process.pid);
18805
+ } catch {
18806
+ }
18807
+ } else if (existsSync3(rollbackPath)) {
18808
+ safeUnlink(rollbackPath);
18809
+ } else {
18810
+ try {
18811
+ getSqlite().query("DELETE FROM gateway_lock WHERE pid = ?").run(process.pid);
18812
+ } catch {
18813
+ }
18814
+ }
18815
+ const backupHint = safetyBackup ? `\uFF1B\u5F53\u524D\u5E93\u5B89\u5168\u5907\u4EFD\uFF1A${safetyBackup.path}` : "";
18816
+ throw new Error(`\u6062\u590D\u6570\u636E\u5E93\u5931\u8D25\uFF0C\u5DF2\u5C1D\u8BD5\u56DE\u6EDA${backupHint}\uFF1A${errorMessage(error)}`);
18817
+ }
18818
+ }
18819
+ static inspectFile(path) {
18820
+ let sqlite2;
18821
+ try {
18822
+ sqlite2 = new Database3(path, { readonly: true, strict: true });
18823
+ } catch (error) {
18824
+ throw new Error(`\u65E0\u6CD5\u6253\u5F00\u5907\u4EFD\u6587\u4EF6\uFF1A${errorMessage(error)}`);
18825
+ }
18826
+ try {
18827
+ return this.inspect(sqlite2, path);
18828
+ } finally {
18829
+ sqlite2.close();
18830
+ }
18831
+ }
18832
+ static inspect(sqlite2, path) {
18833
+ const tableRows = sqlite2.query(
18834
+ "SELECT name FROM sqlite_master WHERE type = 'table'"
18835
+ ).all();
18836
+ const tableNames = new Set(tableRows.map((row) => row.name));
18837
+ const missingTables = REQUIRED_TABLES.filter((table) => !tableNames.has(table));
18838
+ const integrityRows = sqlite2.query("PRAGMA integrity_check").all();
18839
+ const integrityMessages = integrityRows.map((row) => row.integrity_check);
18840
+ const foreignKeyViolations = sqlite2.query("PRAGMA foreign_key_check").all().length;
18841
+ const journal = sqlite2.query("PRAGMA journal_mode").get();
18842
+ const counts = missingTables.length === 0 ? this.readCounts(sqlite2) : { tasks: 0, taskRuns: 0, taskTemplates: 0 };
18843
+ const runningTasks = missingTables.includes("tasks") ? 0 : this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM tasks WHERE status = 'running'");
18844
+ const runningRuns = missingTables.includes("task_runs") ? 0 : this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM task_runs WHERE status = 'running'");
18845
+ const sizeBytes = path === ":memory:" || !existsSync3(path) ? sqlite2.serialize().byteLength : statSync(path).size;
18846
+ return {
18847
+ ok: integrityMessages.length === 1 && integrityMessages[0] === "ok" && foreignKeyViolations === 0 && missingTables.length === 0,
18848
+ path: normalizedPath(path),
18849
+ sizeBytes,
18850
+ journalMode: journal?.journal_mode ?? "unknown",
18851
+ integrityMessages,
18852
+ foreignKeyViolations,
18853
+ missingTables,
18854
+ counts,
18855
+ runningTasks,
18856
+ runningRuns
18857
+ };
18858
+ }
18859
+ static writeSnapshot(sqlite2, outputPath) {
18860
+ const liveCheck = this.inspect(sqlite2, DB_FILE_PATH);
18861
+ if (!liveCheck.ok) throw new Error("\u5F53\u524D\u6570\u636E\u5E93\u672A\u901A\u8FC7\u5B8C\u6574\u6027\u6821\u9A8C\uFF0C\u5DF2\u62D2\u7EDD\u521B\u5EFA\u5907\u4EFD");
18862
+ const output = normalizedPath(outputPath);
18863
+ if (DB_FILE_PATH !== ":memory:" && output === normalizedPath(DB_FILE_PATH)) {
18864
+ throw new Error("\u5907\u4EFD\u8DEF\u5F84\u4E0D\u80FD\u8986\u76D6\u5F53\u524D\u6570\u636E\u5E93");
18865
+ }
18866
+ if (existsSync3(output)) throw new Error(`\u5907\u4EFD\u6587\u4EF6\u5DF2\u5B58\u5728\uFF1A${output}`);
18867
+ mkdirSync2(dirname2(output), { recursive: true });
18868
+ const temporary = `${output}.tmp-${process.pid}-${randomUUID()}`;
18869
+ try {
18870
+ writeFileSync(temporary, sqlite2.serialize(), { flag: "wx", mode: 384 });
18871
+ const standalone = new Database3(temporary, { readwrite: true, create: false });
18872
+ try {
18873
+ standalone.query("PRAGMA journal_mode = DELETE").get();
18874
+ } finally {
18875
+ standalone.close();
18876
+ }
18877
+ safeUnlink(`${temporary}-wal`);
18878
+ safeUnlink(`${temporary}-shm`);
18879
+ const check = this.inspectFile(temporary);
18880
+ if (!check.ok) throw new Error("\u65B0\u5907\u4EFD\u672A\u901A\u8FC7\u5B8C\u6574\u6027\u6821\u9A8C");
18881
+ renameSync(temporary, output);
18882
+ const finalCheck = { ...check, path: output, sizeBytes: statSync(output).size };
18883
+ return { path: output, sizeBytes: finalCheck.sizeBytes, check: finalCheck };
18884
+ } catch (error) {
18885
+ safeUnlink(temporary);
18886
+ safeUnlink(`${temporary}-wal`);
18887
+ safeUnlink(`${temporary}-shm`);
18888
+ throw error;
18889
+ }
18890
+ }
18891
+ static createBackupPath(kind) {
18892
+ const directory = DB_FILE_PATH === ":memory:" ? tmpdir() : dirname2(normalizedPath(DB_FILE_PATH));
18893
+ const base = DB_FILE_PATH === ":memory:" ? "supertask-memory" : basename2(DB_FILE_PATH, ".db");
18894
+ return resolve(directory, `${base}.${kind}-${timestamp()}-${randomUUID().slice(0, 8)}.db`);
18895
+ }
18896
+ static readCounts(sqlite2) {
18897
+ return {
18898
+ tasks: this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM tasks"),
18899
+ taskRuns: this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM task_runs"),
18900
+ taskTemplates: this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM task_templates")
18901
+ };
18902
+ }
18903
+ static scalar(sqlite2, statement) {
18904
+ const row = sqlite2.query(statement).get();
18905
+ return Number(row?.count ?? 0);
18906
+ }
18907
+ static assertGatewaySafe(sqlite2, allowCurrentGateway) {
18908
+ const lock = sqlite2.query("SELECT pid FROM gateway_lock WHERE id = 1").get();
18909
+ if (!lock || !isProcessAlive(lock.pid)) return;
18910
+ if (allowCurrentGateway && lock.pid === process.pid) return;
18911
+ throw new DatabaseMaintenanceConflictError(
18912
+ `Gateway PID ${lock.pid} \u4ECD\u5728\u8FD0\u884C\uFF0C\u8BF7\u5148\u6267\u884C pm2 stop supertask-gateway`
18913
+ );
18914
+ }
18915
+ static assertNoRunningWork(sqlite2) {
18916
+ const runningTasks = this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM tasks WHERE status = 'running'");
18917
+ const runningRuns = this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM task_runs WHERE status = 'running'");
18918
+ if (runningTasks > 0 || runningRuns > 0) {
18919
+ throw new DatabaseMaintenanceConflictError(
18920
+ `\u5B58\u5728\u8FD0\u884C\u4E2D\u4EFB\u52A1\uFF08tasks=${runningTasks}, task_runs=${runningRuns}\uFF09\uFF0C\u5DF2\u62D2\u7EDD\u5371\u9669\u64CD\u4F5C`
18921
+ );
18922
+ }
18923
+ }
18924
+ };
18925
+ }
18926
+ });
18927
+
18505
18928
  // src/web/index.tsx
18506
18929
  var web_exports = {};
18507
18930
  __export(web_exports, {
18508
18931
  dashboardApp: () => dashboardApp,
18509
18932
  default: () => web_default
18510
18933
  });
18511
- import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync, mkdirSync as mkdirSync2, renameSync } from "fs";
18512
- import { dirname as dirname2 } from "path";
18934
+ import { existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, renameSync as renameSync2 } from "fs";
18935
+ import { dirname as dirname3 } from "path";
18513
18936
  function parsePositiveInteger(value) {
18514
18937
  if (!/^\d+$/.test(value)) return null;
18515
18938
  const parsed = Number(value);
@@ -18559,7 +18982,7 @@ function esc(s) {
18559
18982
  }
18560
18983
  function readCurrentConfig() {
18561
18984
  const configPath = getConfigPath();
18562
- if (!existsSync3(configPath)) return {};
18985
+ if (!existsSync4(configPath)) return {};
18563
18986
  try {
18564
18987
  return JSON.parse(readFileSync2(configPath, "utf-8"));
18565
18988
  } catch {
@@ -18568,11 +18991,11 @@ function readCurrentConfig() {
18568
18991
  }
18569
18992
  function writeConfig(cfg) {
18570
18993
  const configPath = getConfigPath();
18571
- const dir = dirname2(configPath);
18572
- if (!existsSync3(dir)) mkdirSync2(dir, { recursive: true });
18994
+ const dir = dirname3(configPath);
18995
+ if (!existsSync4(dir)) mkdirSync3(dir, { recursive: true });
18573
18996
  const tempPath = `${configPath}.${process.pid}.tmp`;
18574
- writeFileSync(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
18575
- renameSync(tempPath, configPath);
18997
+ writeFileSync2(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
18998
+ renameSync2(tempPath, configPath);
18576
18999
  }
18577
19000
  function renderLayout(title, activeTab, body) {
18578
19001
  return `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>${title} - SuperTask</title>${SHARED_STYLES}
@@ -18589,12 +19012,12 @@ async function triggerTmpl(id){if(!confirm('\u7ACB\u5373\u89E6\u53D1\u4E00\u6B21
18589
19012
  function toggleLog(id){const el=document.getElementById('log-'+id);el.style.display=el.style.display==='none'?'block':'none';}
18590
19013
 
18591
19014
  async function clearDatabase(){
18592
- if(!confirm('\u786E\u5B9A\u6E05\u7A7A\u6240\u6709\u4EFB\u52A1\u6570\u636E\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\uFF01'))return;
18593
- if(!confirm('\u518D\u6B21\u786E\u8BA4\uFF1A\u5C06\u5220\u9664\u6240\u6709\u4EFB\u52A1\u3001\u6267\u884C\u8BB0\u5F55\u548C\u8C03\u5EA6\u6A21\u677F\u3002'))return;
19015
+ if(!confirm('\u786E\u5B9A\u6E05\u7A7A\u6240\u6709\u4EFB\u52A1\u6570\u636E\uFF1F\u7CFB\u7EDF\u4F1A\u5148\u81EA\u52A8\u521B\u5EFA\u5907\u4EFD\u3002'))return;
19016
+ if(!confirm('\u518D\u6B21\u786E\u8BA4\uFF1A\u5C06\u4E8B\u52A1\u6027\u5220\u9664\u6240\u6709\u4EFB\u52A1\u3001\u6267\u884C\u8BB0\u5F55\u548C\u8C03\u5EA6\u6A21\u677F\u3002'))return;
18594
19017
  try{
18595
- const r=await fetch('/api/database/clear',{method:'POST'});
19018
+ const r=await fetch('/api/database/clear',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({confirmation:'CLEAR'})});
18596
19019
  const d=await r.json();
18597
- if(d.success){alert('\u6570\u636E\u5E93\u5DF2\u6E05\u7A7A');location.reload();}
19020
+ if(d.success){alert('\u6570\u636E\u5E93\u5DF2\u6E05\u7A7A\uFF0C\u81EA\u52A8\u5907\u4EFD\uFF1A'+d.backupPath);location.reload();}
18598
19021
  else{alert('\u6E05\u7A7A\u5931\u8D25: '+d.error);}
18599
19022
  }catch(e){alert('\u6E05\u7A7A\u5931\u8D25: '+e.message);}
18600
19023
  }
@@ -18655,6 +19078,7 @@ var init_web = __esm({
18655
19078
  init_task_service();
18656
19079
  init_task_run_service();
18657
19080
  init_task_template_service();
19081
+ init_database_maintenance_service();
18658
19082
  init_drizzle_orm();
18659
19083
  init_db2();
18660
19084
  init_config();
@@ -18963,7 +19387,7 @@ var init_web = __esm({
18963
19387
  const stats = await TaskService.stats({});
18964
19388
  const runningRuns = await TaskRunService.getAllRunningRuns();
18965
19389
  const templates = await TaskTemplateService.list(100);
18966
- const configExists = existsSync3(configPath);
19390
+ const configExists = existsSync4(configPath);
18967
19391
  let runRows = "";
18968
19392
  if (runningRuns.length > 0) {
18969
19393
  for (const run of runningRuns) {
@@ -19037,7 +19461,7 @@ var init_web = __esm({
19037
19461
 
19038
19462
  <div class="card mt16" style="border-color:var(--red)">
19039
19463
  <h3 style="margin:0 0 12px;font-size:14px;color:var(--red)">\u5371\u9669\u64CD\u4F5C</h3>
19040
- <p class="sm mu" style="margin:0 0 12px">\u6E05\u7A7A\u6240\u6709\u4EFB\u52A1\u6570\u636E\uFF08tasks + task_runs + task_templates\uFF09\uFF0C\u4E0D\u53EF\u6062\u590D\u3002</p>
19464
+ <p class="sm mu" style="margin:0 0 12px">\u81EA\u52A8\u5907\u4EFD\u540E\u4E8B\u52A1\u6027\u6E05\u7A7A tasks\u3001task_runs \u548C task_templates\uFF1B\u5B58\u5728\u8FD0\u884C\u4E2D\u4EFB\u52A1\u65F6\u4F1A\u62D2\u7EDD\u6267\u884C\u3002</p>
19041
19465
  <button class="btn btn-danger" style="border-color:var(--red);color:var(--red);padding:6px 16px" onclick="clearDatabase()">\u6E05\u7A7A\u6570\u636E\u5E93</button>
19042
19466
  </div>`;
19043
19467
  return c.html(renderLayout("\u7CFB\u7EDF\u72B6\u6001", "system", body));
@@ -19142,14 +19566,16 @@ var init_web = __esm({
19142
19566
  }
19143
19567
  });
19144
19568
  app.post("/api/database/clear", async (c) => {
19569
+ const body = await c.req.json().catch(() => ({}));
19570
+ if (body.confirmation !== "CLEAR") {
19571
+ return c.json({ success: false, error: "confirmation must be CLEAR" }, 400);
19572
+ }
19145
19573
  try {
19146
- const { tasks: tasks3, taskRuns: taskRuns4, taskTemplates: taskTemplates4 } = schema_exports;
19147
- await db.delete(taskRuns4);
19148
- await db.delete(taskTemplates4);
19149
- await db.delete(tasks3);
19150
- return c.json({ success: true });
19574
+ const result = DatabaseMaintenanceService.clear({ allowCurrentGateway: true });
19575
+ return c.json({ success: true, ...result });
19151
19576
  } catch (err) {
19152
- return c.json({ success: false, error: err instanceof Error ? err.message : String(err) }, 500);
19577
+ const status = err instanceof DatabaseMaintenanceConflictError ? 409 : 500;
19578
+ return c.json({ success: false, error: err instanceof Error ? err.message : String(err) }, status);
19153
19579
  }
19154
19580
  });
19155
19581
  dashboardApp = app;
@@ -19168,69 +19594,8 @@ init_config();
19168
19594
  init_task_service();
19169
19595
  init_task_run_service();
19170
19596
  init_health();
19597
+ init_process_control();
19171
19598
  import { spawn } from "child_process";
19172
-
19173
- // src/core/process-control.ts
19174
- import { spawnSync } from "child_process";
19175
- import { basename } from "path";
19176
- function isSafePid(pid) {
19177
- return Number.isInteger(pid) && pid > 1 && pid !== process.pid;
19178
- }
19179
- function inspectUnixProcess(pid) {
19180
- const result = spawnSync("ps", ["-o", "pgid=", "-o", "command=", "-p", String(pid)], {
19181
- encoding: "utf8",
19182
- stdio: ["ignore", "pipe", "ignore"]
19183
- });
19184
- if (result.status !== 0) return null;
19185
- const match2 = result.stdout.trim().match(/^(\d+)\s+(.+)$/s);
19186
- if (!match2) return null;
19187
- return { processGroupId: Number(match2[1]), command: match2[2] };
19188
- }
19189
- function commandMatches(command, expectedExecutable) {
19190
- const expectedName = basename(expectedExecutable).trim().toLowerCase();
19191
- if (!expectedName) return false;
19192
- return command.toLowerCase().includes(expectedName);
19193
- }
19194
- function signalSpawnedProcessTree(pid, signal) {
19195
- if (!isSafePid(pid)) return false;
19196
- if (process.platform !== "win32") {
19197
- try {
19198
- process.kill(-pid, signal);
19199
- return true;
19200
- } catch {
19201
- }
19202
- }
19203
- try {
19204
- process.kill(pid, signal);
19205
- return true;
19206
- } catch {
19207
- return false;
19208
- }
19209
- }
19210
- function signalRecordedProcessTree(pid, signal, expectedExecutable) {
19211
- if (!isSafePid(pid)) return false;
19212
- if (process.platform === "win32") {
19213
- const list = spawnSync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
19214
- encoding: "utf8",
19215
- stdio: ["ignore", "pipe", "ignore"]
19216
- });
19217
- if (list.status !== 0 || !commandMatches(list.stdout, expectedExecutable)) return false;
19218
- const args = ["/PID", String(pid), "/T"];
19219
- if (signal === "SIGKILL") args.push("/F");
19220
- return spawnSync("taskkill", args, { stdio: "ignore" }).status === 0;
19221
- }
19222
- const info = inspectUnixProcess(pid);
19223
- if (!info || !commandMatches(info.command, expectedExecutable)) return false;
19224
- try {
19225
- if (info.processGroupId === pid) process.kill(-pid, signal);
19226
- else process.kill(pid, signal);
19227
- return true;
19228
- } catch {
19229
- return false;
19230
- }
19231
- }
19232
-
19233
- // src/worker/index.ts
19234
19599
  var DEFAULT_MAX_OUTPUT_CHARS = 64 * 1024;
19235
19600
  var FORBIDDEN_AGENT = "supertask-runner";
19236
19601
  var WorkerEngine = class {
@@ -19493,14 +19858,14 @@ ${output}` : ""}`;
19493
19858
  if (entry.child.exitCode !== null || entry.child.signalCode !== null) {
19494
19859
  return Promise.resolve();
19495
19860
  }
19496
- return new Promise((resolve) => {
19861
+ return new Promise((resolve2) => {
19497
19862
  const timeout = setTimeout(() => {
19498
19863
  this.signalEntry(entry, "SIGKILL");
19499
- resolve();
19864
+ resolve2();
19500
19865
  }, 5e3);
19501
19866
  entry.child.once("close", () => {
19502
19867
  clearTimeout(timeout);
19503
- resolve();
19868
+ resolve2();
19504
19869
  });
19505
19870
  this.signalEntry(entry, "SIGTERM");
19506
19871
  });
@@ -19532,6 +19897,7 @@ ${output}` : ""}`;
19532
19897
  init_task_run_service();
19533
19898
  init_task_service();
19534
19899
  init_backoff();
19900
+ init_process_control();
19535
19901
  async function checkHeartbeats(heartbeatTimeoutMs) {
19536
19902
  const staleRuns = await TaskRunService.getStaleRuns(heartbeatTimeoutMs);
19537
19903
  if (staleRuns.length === 0) return;
@@ -19833,6 +20199,7 @@ init_db2();
19833
20199
  init_task_service();
19834
20200
  init_task_run_service();
19835
20201
  init_health();
20202
+ init_process_control();
19836
20203
  var STALE_THRESHOLD_MS = 3e4;
19837
20204
  function acquireLock() {
19838
20205
  const now = Date.now();
@@ -19841,7 +20208,8 @@ function acquireLock() {
19841
20208
  sqlite.exec("BEGIN IMMEDIATE");
19842
20209
  const existing = sqlite.prepare("SELECT id, pid, heartbeat_at FROM gateway_lock WHERE id = 1").get();
19843
20210
  if (existing) {
19844
- if (now - existing.heartbeat_at < STALE_THRESHOLD_MS) {
20211
+ const lockHolderAlive = existing.pid !== pid && isProcessAlive(existing.pid);
20212
+ if (now - existing.heartbeat_at < STALE_THRESHOLD_MS && lockHolderAlive) {
19845
20213
  sqlite.exec("ROLLBACK");
19846
20214
  console.error(JSON.stringify({
19847
20215
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -19911,6 +20279,15 @@ async function main() {
19911
20279
  const worker = new WorkerEngine(cfg);
19912
20280
  const watchdog = new Watchdog(cfg);
19913
20281
  const scheduler = new Scheduler(cfg);
20282
+ const recoveredOrphans = await TaskService.resetOrphanRunningToPending();
20283
+ if (recoveredOrphans > 0) {
20284
+ console.log(JSON.stringify({
20285
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
20286
+ level: "warn",
20287
+ msg: "reset orphan running tasks to pending",
20288
+ count: recoveredOrphans
20289
+ }));
20290
+ }
19914
20291
  initializeGatewayHealth({
19915
20292
  workerPollIntervalMs: cfg.worker.pollIntervalMs,
19916
20293
  schedulerEnabled: cfg.scheduler.enabled,
@@ -19981,6 +20358,8 @@ if (import.meta.main) {
19981
20358
  main();
19982
20359
  }
19983
20360
  export {
19984
- main
20361
+ acquireLock,
20362
+ main,
20363
+ releaseLock
19985
20364
  };
19986
20365
  //# sourceMappingURL=index.js.map