opencode-supertask 0.1.20 → 0.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -2353,11 +2353,11 @@ Expecting one of '${allowedValues.join("', '")}'`);
2353
2353
  };
2354
2354
  this._checkNumberOfArguments();
2355
2355
  const processedArgs = [];
2356
- this.registeredArguments.forEach((declaredArg, index) => {
2356
+ this.registeredArguments.forEach((declaredArg, index2) => {
2357
2357
  let value = declaredArg.defaultValue;
2358
2358
  if (declaredArg.variadic) {
2359
- if (index < this.args.length) {
2360
- value = this.args.slice(index);
2359
+ if (index2 < this.args.length) {
2360
+ value = this.args.slice(index2);
2361
2361
  if (declaredArg.parseArg) {
2362
2362
  value = value.reduce((processed, v) => {
2363
2363
  return myParseArg(declaredArg, v, processed);
@@ -2366,13 +2366,13 @@ Expecting one of '${allowedValues.join("', '")}'`);
2366
2366
  } else if (value === void 0) {
2367
2367
  value = [];
2368
2368
  }
2369
- } else if (index < this.args.length) {
2370
- value = this.args[index];
2369
+ } else if (index2 < this.args.length) {
2370
+ value = this.args[index2];
2371
2371
  if (declaredArg.parseArg) {
2372
2372
  value = myParseArg(declaredArg, value, declaredArg.defaultValue);
2373
2373
  }
2374
2374
  }
2375
- processedArgs[index] = value;
2375
+ processedArgs[index2] = value;
2376
2376
  });
2377
2377
  this.processedArgs = processedArgs;
2378
2378
  }
@@ -2658,10 +2658,10 @@ Expecting one of '${allowedValues.join("', '")}'`);
2658
2658
  }
2659
2659
  }
2660
2660
  if (/^--[^=]+=/.test(arg)) {
2661
- const index = arg.indexOf("=");
2662
- const option = this._findOption(arg.slice(0, index));
2661
+ const index2 = arg.indexOf("=");
2662
+ const option = this._findOption(arg.slice(0, index2));
2663
2663
  if (option && (option.required || option.optional)) {
2664
- this.emit(`option:${option.name()}`, arg.slice(index + 1));
2664
+ this.emit(`option:${option.name()}`, arg.slice(index2 + 1));
2665
2665
  continue;
2666
2666
  }
2667
2667
  }
@@ -4506,7 +4506,7 @@ var init_sql = __esm({
4506
4506
  return new SQL([new StringChunk(str)]);
4507
4507
  }
4508
4508
  sql2.raw = raw2;
4509
- function join4(chunks, separator) {
4509
+ function join5(chunks, separator) {
4510
4510
  const result = [];
4511
4511
  for (const [i, chunk] of chunks.entries()) {
4512
4512
  if (i > 0 && separator !== void 0) {
@@ -4516,7 +4516,7 @@ var init_sql = __esm({
4516
4516
  }
4517
4517
  return new SQL(result);
4518
4518
  }
4519
- sql2.join = join4;
4519
+ sql2.join = join5;
4520
4520
  function identifier(value) {
4521
4521
  return new Name(value);
4522
4522
  }
@@ -4660,8 +4660,8 @@ function haveSameKeys(left, right) {
4660
4660
  if (leftKeys.length !== rightKeys.length) {
4661
4661
  return false;
4662
4662
  }
4663
- for (const [index, key] of leftKeys.entries()) {
4664
- if (key !== rightKeys[index]) {
4663
+ for (const [index2, key] of leftKeys.entries()) {
4664
+ if (key !== rightKeys[index2]) {
4665
4665
  return false;
4666
4666
  }
4667
4667
  }
@@ -6522,8 +6522,8 @@ var init_dialect = __esm({
6522
6522
  }
6523
6523
  const joinsArray = [];
6524
6524
  if (joins) {
6525
- for (const [index, joinMeta] of joins.entries()) {
6526
- if (index === 0) {
6525
+ for (const [index2, joinMeta] of joins.entries()) {
6526
+ if (index2 === 0) {
6527
6527
  joinsArray.push(sql` `);
6528
6528
  }
6529
6529
  const table = joinMeta.table;
@@ -6540,7 +6540,7 @@ var init_dialect = __esm({
6540
6540
  sql`${sql.raw(joinMeta.joinType)} join ${table} on ${joinMeta.on}`
6541
6541
  );
6542
6542
  }
6543
- if (index < joins.length - 1) {
6543
+ if (index2 < joins.length - 1) {
6544
6544
  joinsArray.push(sql` `);
6545
6545
  }
6546
6546
  }
@@ -6553,9 +6553,9 @@ var init_dialect = __esm({
6553
6553
  buildOrderBy(orderBy) {
6554
6554
  const orderByList = [];
6555
6555
  if (orderBy) {
6556
- for (const [index, orderByValue] of orderBy.entries()) {
6556
+ for (const [index2, orderByValue] of orderBy.entries()) {
6557
6557
  orderByList.push(orderByValue);
6558
- if (index < orderBy.length - 1) {
6558
+ if (index2 < orderBy.length - 1) {
6559
6559
  orderByList.push(sql`, `);
6560
6560
  }
6561
6561
  }
@@ -6604,9 +6604,9 @@ var init_dialect = __esm({
6604
6604
  const havingSql = having ? sql` having ${having}` : void 0;
6605
6605
  const groupByList = [];
6606
6606
  if (groupBy) {
6607
- for (const [index, groupByValue] of groupBy.entries()) {
6607
+ for (const [index2, groupByValue] of groupBy.entries()) {
6608
6608
  groupByList.push(groupByValue);
6609
- if (index < groupBy.length - 1) {
6609
+ if (index2 < groupBy.length - 1) {
6610
6610
  groupByList.push(sql`, `);
6611
6611
  }
6612
6612
  }
@@ -7132,7 +7132,7 @@ var init_select2 = __esm({
7132
7132
  return (table, on) => {
7133
7133
  const baseTableName = this.tableName;
7134
7134
  const tableName = getTableLikeName(table);
7135
- if (typeof tableName === "string" && this.config.joins?.some((join4) => join4.alias === tableName)) {
7135
+ if (typeof tableName === "string" && this.config.joins?.some((join5) => join5.alias === tableName)) {
7136
7136
  throw new Error(`Alias "${tableName}" is already used in this query`);
7137
7137
  }
7138
7138
  if (!this.isPartialSelect) {
@@ -7974,7 +7974,7 @@ var init_update = __esm({
7974
7974
  createJoin(joinType) {
7975
7975
  return (table, on) => {
7976
7976
  const tableName = getTableLikeName(table);
7977
- if (typeof tableName === "string" && this.config.joins.some((join4) => join4.alias === tableName)) {
7977
+ if (typeof tableName === "string" && this.config.joins.some((join5) => join5.alias === tableName)) {
7978
7978
  throw new Error(`Alias "${tableName}" is already used in this query`);
7979
7979
  }
7980
7980
  if (typeof on === "function") {
@@ -8695,6 +8695,9 @@ var init_checks = __esm({
8695
8695
  });
8696
8696
 
8697
8697
  // node_modules/drizzle-orm/sqlite-core/indexes.js
8698
+ function index(name) {
8699
+ return new IndexBuilderOn(name, false);
8700
+ }
8698
8701
  var IndexBuilderOn, IndexBuilder, Index;
8699
8702
  var init_indexes = __esm({
8700
8703
  "node_modules/drizzle-orm/sqlite-core/indexes.js"() {
@@ -9274,10 +9277,6 @@ var init_migrator = __esm({
9274
9277
  });
9275
9278
 
9276
9279
  // node_modules/drizzle-orm/bun-sqlite/migrator.js
9277
- var migrator_exports = {};
9278
- __export(migrator_exports, {
9279
- migrate: () => migrate
9280
- });
9281
9280
  function migrate(db2, config) {
9282
9281
  const migrations = readMigrationFiles(config);
9283
9282
  db2.dialect.migrate(migrations, db2.session, config);
@@ -9327,12 +9326,17 @@ var init_schema = __esm({
9327
9326
  resultLog: text("result_log"),
9328
9327
  retryCount: integer("retry_count").default(0),
9329
9328
  maxRetries: integer("max_retries").default(3),
9329
+ retryBackoffMs: integer("retry_backoff_ms").default(3e4),
9330
9330
  // Gateway 扩展字段(毫秒)
9331
9331
  retryAfter: integer("retry_after"),
9332
9332
  timeoutMs: integer("timeout_ms"),
9333
9333
  templateId: integer("template_id"),
9334
9334
  scheduledAt: integer("scheduled_at")
9335
- });
9335
+ }, (table) => [
9336
+ index("tasks_queue_idx").on(table.status, table.retryAfter, table.urgency, table.importance, table.createdAt, table.id),
9337
+ index("tasks_batch_status_idx").on(table.batchId, table.status),
9338
+ index("tasks_template_status_idx").on(table.templateId, table.status)
9339
+ ]);
9336
9340
  TASK_CATEGORIES = [
9337
9341
  "translate",
9338
9342
  "generate",
@@ -9342,7 +9346,7 @@ var init_schema = __esm({
9342
9346
  ];
9343
9347
  taskRuns = sqliteTable("task_runs", {
9344
9348
  id: integer("id").primaryKey({ autoIncrement: true }),
9345
- taskId: integer("task_id").notNull().references(() => tasks.id),
9349
+ taskId: integer("task_id").notNull().references(() => tasks.id, { onDelete: "cascade" }),
9346
9350
  sessionId: text("session_id"),
9347
9351
  model: text("model"),
9348
9352
  status: text("status").default("running"),
@@ -9355,7 +9359,10 @@ var init_schema = __esm({
9355
9359
  heartbeatAt: integer("heartbeat_at"),
9356
9360
  workerPid: integer("worker_pid"),
9357
9361
  childPid: integer("child_pid")
9358
- });
9362
+ }, (table) => [
9363
+ index("task_runs_task_started_idx").on(table.taskId, table.startedAt, table.id),
9364
+ index("task_runs_status_heartbeat_idx").on(table.status, table.heartbeatAt)
9365
+ ]);
9359
9366
  taskTemplates = sqliteTable("task_templates", {
9360
9367
  id: integer("id").primaryKey({ autoIncrement: true }),
9361
9368
  name: text("name").notNull(),
@@ -9366,6 +9373,7 @@ var init_schema = __esm({
9366
9373
  category: text("category").default("general"),
9367
9374
  importance: integer("importance").default(3),
9368
9375
  urgency: integer("urgency").default(3),
9376
+ batchId: text("batch_id"),
9369
9377
  scheduleType: text("schedule_type").notNull(),
9370
9378
  cronExpr: text("cron_expr"),
9371
9379
  intervalMs: integer("interval_ms"),
@@ -9373,12 +9381,15 @@ var init_schema = __esm({
9373
9381
  maxInstances: integer("max_instances").default(1),
9374
9382
  maxRetries: integer("max_retries").default(3),
9375
9383
  retryBackoffMs: integer("retry_backoff_ms").default(3e4),
9384
+ timeoutMs: integer("timeout_ms"),
9376
9385
  lastRunAt: integer("last_run_at"),
9377
9386
  nextRunAt: integer("next_run_at"),
9378
9387
  enabled: integer("enabled", { mode: "boolean" }).default(true),
9379
9388
  createdAt: integer("created_at").default(0),
9380
9389
  updatedAt: integer("updated_at").default(0)
9381
- });
9390
+ }, (table) => [
9391
+ index("task_templates_due_idx").on(table.enabled, table.nextRunAt, table.id)
9392
+ ]);
9382
9393
  }
9383
9394
  });
9384
9395
 
@@ -9423,16 +9434,30 @@ function initDb() {
9423
9434
  id INTEGER PRIMARY KEY CHECK (id = 1),
9424
9435
  pid INTEGER NOT NULL,
9425
9436
  acquired_at INTEGER NOT NULL,
9426
- heartbeat_at INTEGER NOT NULL
9437
+ heartbeat_at INTEGER NOT NULL,
9438
+ ready_at INTEGER
9427
9439
  );
9428
9440
  `);
9441
+ const gatewayLockColumns = _sqlite.query("PRAGMA table_info(gateway_lock)").all();
9442
+ if (!gatewayLockColumns.some((column) => column.name === "ready_at")) {
9443
+ _sqlite.exec("ALTER TABLE gateway_lock ADD COLUMN ready_at INTEGER;");
9444
+ }
9429
9445
  _db = drizzle(_sqlite, { schema: schema_exports });
9430
9446
  if (!_migrationRan) {
9431
- _migrationRan = true;
9432
9447
  try {
9433
9448
  migrate(_db, { migrationsFolder: getMigrationsFolder() });
9449
+ _sqlite.exec("PRAGMA foreign_keys = ON;");
9450
+ const violations = _sqlite.query("PRAGMA foreign_key_check;").all();
9451
+ if (violations.length > 0) {
9452
+ throw new Error(`\u68C0\u6D4B\u5230 ${violations.length} \u6761\u5B64\u7ACB\u5173\u8054\u8BB0\u5F55\uFF0C\u8BF7\u5148\u4FEE\u590D\u6570\u636E\u518D\u542F\u52A8`);
9453
+ }
9454
+ _migrationRan = true;
9434
9455
  } catch (err) {
9435
9456
  const msg = err instanceof Error ? err.message : String(err);
9457
+ _migrationRan = false;
9458
+ _sqlite.close();
9459
+ _sqlite = null;
9460
+ _db = null;
9436
9461
  console.error(`[supertask] migration failed: ${msg}`);
9437
9462
  throw new Error(`[supertask] DB migration failed: ${msg}`);
9438
9463
  }
@@ -9452,6 +9477,7 @@ function closeDb() {
9452
9477
  _sqlite.close();
9453
9478
  _sqlite = null;
9454
9479
  _db = null;
9480
+ _migrationRan = false;
9455
9481
  }
9456
9482
  }
9457
9483
  var DEFAULT_DB_PATH, DB_FILE_PATH, _sqlite, _db, _migrationRan, db, sqlite;
@@ -9535,14 +9561,14 @@ var init_backoff = __esm({
9535
9561
  });
9536
9562
 
9537
9563
  // src/core/services/task.service.ts
9538
- var tasks2, TaskService;
9564
+ var tasks2, taskRuns2, TaskService;
9539
9565
  var init_task_service = __esm({
9540
9566
  "src/core/services/task.service.ts"() {
9541
9567
  "use strict";
9542
9568
  init_db2();
9543
9569
  init_drizzle_orm();
9544
9570
  init_backoff();
9545
- ({ tasks: tasks2 } = schema_exports);
9571
+ ({ tasks: tasks2, taskRuns: taskRuns2 } = schema_exports);
9546
9572
  TaskService = class {
9547
9573
  static buildScopeWhere(scope) {
9548
9574
  const conditions = [];
@@ -9552,9 +9578,27 @@ var init_task_service = __esm({
9552
9578
  return conditions;
9553
9579
  }
9554
9580
  static async add(data) {
9581
+ this.validateNewTask(data);
9555
9582
  const result = await db.insert(tasks2).values(data).returning();
9556
9583
  return result[0];
9557
9584
  }
9585
+ static validateNewTask(data) {
9586
+ if (!data.name.trim()) throw new Error("name \u4E0D\u80FD\u4E3A\u7A7A");
9587
+ if (!data.agent.trim()) throw new Error("agent \u4E0D\u80FD\u4E3A\u7A7A");
9588
+ if (!data.prompt.trim()) throw new Error("prompt \u4E0D\u80FD\u4E3A\u7A7A");
9589
+ this.validateInteger("importance", data.importance, 1, 5);
9590
+ this.validateInteger("urgency", data.urgency, 1, 5);
9591
+ this.validateInteger("maxRetries", data.maxRetries, 0, 1e3);
9592
+ this.validateInteger("retryBackoffMs", data.retryBackoffMs, 0, 864e5);
9593
+ this.validateInteger("timeoutMs", data.timeoutMs, 1e3, 6048e5);
9594
+ this.validateInteger("dependsOn", data.dependsOn, 1, Number.MAX_SAFE_INTEGER);
9595
+ }
9596
+ static validateInteger(name, value, min, max) {
9597
+ if (value === void 0 || value === null) return;
9598
+ if (!Number.isInteger(value) || value < min || value > max) {
9599
+ throw new Error(`${name} \u5FC5\u987B\u662F ${min} \u5230 ${max} \u4E4B\u95F4\u7684\u6574\u6570`);
9600
+ }
9601
+ }
9558
9602
  static async next(scope = {}) {
9559
9603
  const baseConditions = [...this.buildScopeWhere(scope)];
9560
9604
  const nowMs = Date.now();
@@ -9577,7 +9621,7 @@ var init_task_service = __esm({
9577
9621
  ),
9578
9622
  and(
9579
9623
  eq(tasks2.status, "failed"),
9580
- sql`${tasks2.retryCount} < ${tasks2.maxRetries}`,
9624
+ sql`${tasks2.retryCount} <= ${tasks2.maxRetries}`,
9581
9625
  retryAfterFilter
9582
9626
  )
9583
9627
  );
@@ -9591,7 +9635,8 @@ var init_task_service = __esm({
9591
9635
  const allTasks = await db.select().from(tasks2).where(and(...conditions)).orderBy(
9592
9636
  desc(tasks2.urgency),
9593
9637
  desc(tasks2.importance),
9594
- asc(tasks2.createdAt)
9638
+ asc(tasks2.createdAt),
9639
+ asc(tasks2.id)
9595
9640
  );
9596
9641
  for (const task of allTasks) {
9597
9642
  if (task.dependsOn) {
@@ -9612,7 +9657,7 @@ var init_task_service = __esm({
9612
9657
  eq(tasks2.status, "pending"),
9613
9658
  and(
9614
9659
  eq(tasks2.status, "failed"),
9615
- sql`${tasks2.retryCount} < ${tasks2.maxRetries}`
9660
+ sql`${tasks2.retryCount} <= ${tasks2.maxRetries}`
9616
9661
  )
9617
9662
  ),
9618
9663
  ...this.buildScopeWhere(scope)
@@ -9625,7 +9670,11 @@ var init_task_service = __esm({
9625
9670
  return result[0] || null;
9626
9671
  }
9627
9672
  static async done(id, log, scope = {}) {
9628
- const conditions = [eq(tasks2.id, id), ...this.buildScopeWhere(scope)];
9673
+ const conditions = [
9674
+ eq(tasks2.id, id),
9675
+ eq(tasks2.status, "running"),
9676
+ ...this.buildScopeWhere(scope)
9677
+ ];
9629
9678
  const result = await db.update(tasks2).set({
9630
9679
  status: "done",
9631
9680
  finishedAt: /* @__PURE__ */ new Date(),
@@ -9636,17 +9685,24 @@ var init_task_service = __esm({
9636
9685
  }
9637
9686
  static async fail(id, log, scope = {}, options) {
9638
9687
  const current = await this.getById(id, scope);
9639
- if (!current) return null;
9688
+ if (!current || current.status !== "running") return null;
9640
9689
  const newRetryCount = (current.retryCount ?? 0) + 1;
9641
9690
  const maxRetries = current.maxRetries ?? 3;
9642
- const isDeadLetter = options?.setDeadLetter ?? newRetryCount >= maxRetries;
9643
- const conditions = [eq(tasks2.id, id), ...this.buildScopeWhere(scope)];
9691
+ const isDeadLetter = options?.setDeadLetter ?? newRetryCount > maxRetries;
9692
+ const conditions = [
9693
+ eq(tasks2.id, id),
9694
+ eq(tasks2.status, "running"),
9695
+ ...this.buildScopeWhere(scope)
9696
+ ];
9644
9697
  const result = await db.update(tasks2).set({
9645
9698
  status: isDeadLetter ? "dead_letter" : "failed",
9646
9699
  finishedAt: /* @__PURE__ */ new Date(),
9647
9700
  resultLog: log,
9648
9701
  retryCount: newRetryCount,
9649
- retryAfter: isDeadLetter ? null : options?.retryAfterMs ?? Date.now() + computeBackoff(newRetryCount)
9702
+ retryAfter: isDeadLetter ? null : options?.retryAfterMs ?? Date.now() + computeBackoff(
9703
+ newRetryCount,
9704
+ current.retryBackoffMs ?? 3e4
9705
+ )
9650
9706
  }).where(and(...conditions)).returning();
9651
9707
  return result[0] || null;
9652
9708
  }
@@ -9678,9 +9734,38 @@ var init_task_service = __esm({
9678
9734
  ).returning();
9679
9735
  return result.length;
9680
9736
  }
9737
+ static async resetOrphanRunningToPending() {
9738
+ const result = await db.update(tasks2).set({
9739
+ status: "pending",
9740
+ startedAt: null,
9741
+ finishedAt: null
9742
+ }).where(
9743
+ and(
9744
+ eq(tasks2.status, "running"),
9745
+ sql`NOT EXISTS (
9746
+ SELECT 1 FROM ${taskRuns2}
9747
+ WHERE ${taskRuns2.taskId} = ${tasks2.id}
9748
+ AND ${taskRuns2.status} = 'running'
9749
+ )`
9750
+ )
9751
+ ).returning();
9752
+ return result.length;
9753
+ }
9681
9754
  static async cancel(id, scope = {}) {
9682
- const conditions = [eq(tasks2.id, id), ...this.buildScopeWhere(scope)];
9683
- const result = await db.update(tasks2).set({ status: "cancelled" }).where(and(...conditions)).returning();
9755
+ const conditions = [
9756
+ eq(tasks2.id, id),
9757
+ or(
9758
+ eq(tasks2.status, "pending"),
9759
+ eq(tasks2.status, "running"),
9760
+ eq(tasks2.status, "failed")
9761
+ ),
9762
+ ...this.buildScopeWhere(scope)
9763
+ ];
9764
+ const result = await db.update(tasks2).set({
9765
+ status: "cancelled",
9766
+ finishedAt: /* @__PURE__ */ new Date(),
9767
+ retryAfter: null
9768
+ }).where(and(...conditions)).returning();
9684
9769
  return result[0] || null;
9685
9770
  }
9686
9771
  static async retry(id, scope = {}) {
@@ -9692,7 +9777,8 @@ var init_task_service = __esm({
9692
9777
  status: "pending",
9693
9778
  startedAt: null,
9694
9779
  finishedAt: null,
9695
- retryAfter: null
9780
+ retryAfter: null,
9781
+ retryCount: 0
9696
9782
  }).where(and(...conditions)).returning();
9697
9783
  return result[0] || null;
9698
9784
  }
@@ -9706,7 +9792,8 @@ var init_task_service = __esm({
9706
9792
  status: "pending",
9707
9793
  startedAt: null,
9708
9794
  finishedAt: null,
9709
- retryAfter: null
9795
+ retryAfter: null,
9796
+ retryCount: 0
9710
9797
  }).where(and(...conditions)).returning();
9711
9798
  return result.length;
9712
9799
  }
@@ -9774,6 +9861,9 @@ var init_task_service = __esm({
9774
9861
  }
9775
9862
  static async delete(id, scope = {}) {
9776
9863
  const conditions = [eq(tasks2.id, id), ...this.buildScopeWhere(scope)];
9864
+ const existing = await db.select({ id: tasks2.id }).from(tasks2).where(and(...conditions)).limit(1);
9865
+ if (!existing[0]) return false;
9866
+ await db.delete(taskRuns2).where(eq(taskRuns2.taskId, id));
9777
9867
  const result = await db.delete(tasks2).where(and(...conditions)).returning();
9778
9868
  return result.length > 0;
9779
9869
  }
@@ -9992,7 +10082,7 @@ var require_CronField = __commonJS({
9992
10082
  if (!isValidRange) {
9993
10083
  throw new Error(`${this.constructor.name} Validation error, got value ${badValue} expected range ${this.min}-${this.max}${charsString}`);
9994
10084
  }
9995
- const duplicate = this.#values.find((value, index) => this.#values.indexOf(value) !== index);
10085
+ const duplicate = this.#values.find((value, index2) => this.#values.indexOf(value) !== index2);
9996
10086
  if (duplicate) {
9997
10087
  throw new Error(`${this.constructor.name} Validation error, duplicate values found: ${duplicate}`);
9998
10088
  }
@@ -17837,11 +17927,11 @@ var require_CronFieldCollection = __commonJS({
17837
17927
  throw new Error("Unexpected range end");
17838
17928
  }
17839
17929
  if (step * multiplier > range.end) {
17840
- const mapFn = (_, index) => {
17930
+ const mapFn = (_, index2) => {
17841
17931
  if (typeof range.start !== "number") {
17842
17932
  throw new Error("Unexpected range start");
17843
17933
  }
17844
- return index % step === 0 ? range.start + index : null;
17934
+ return index2 % step === 0 ? range.start + index2 : null;
17845
17935
  };
17846
17936
  if (typeof range.start !== "number") {
17847
17937
  throw new Error("Unexpected range start");
@@ -18733,9 +18823,9 @@ var require_CronExpressionParser = __commonJS({
18733
18823
  if (field === CronUnit.DayOfWeek && max % 7 === 0) {
18734
18824
  stack.push(0);
18735
18825
  }
18736
- for (let index = min; index <= max; index += repeatInterval) {
18737
- if (stack.indexOf(index) === -1) {
18738
- stack.push(index);
18826
+ for (let index2 = min; index2 <= max; index2 += repeatInterval) {
18827
+ if (stack.indexOf(index2) === -1) {
18828
+ stack.push(index2);
18739
18829
  }
18740
18830
  }
18741
18831
  return stack;
@@ -18858,8 +18948,8 @@ var require_CronFileParser = __commonJS({
18858
18948
  * @throws If file cannot be read
18859
18949
  */
18860
18950
  static parseFileSync(filePath) {
18861
- const { readFileSync: readFileSync4 } = __require("fs");
18862
- const data = readFileSync4(filePath, "utf8");
18951
+ const { readFileSync: readFileSync5 } = __require("fs");
18952
+ const data = readFileSync5(filePath, "utf8");
18863
18953
  return _CronFileParser.#parseContent(data);
18864
18954
  }
18865
18955
  /**
@@ -18958,11 +19048,6 @@ var require_dist = __commonJS({
18958
19048
  });
18959
19049
 
18960
19050
  // src/core/cron-parser.ts
18961
- var cron_parser_exports = {};
18962
- __export(cron_parser_exports, {
18963
- getNextCronRun: () => getNextCronRun,
18964
- isValidCronExpr: () => isValidCronExpr
18965
- });
18966
19051
  function getNextCronRun(expr, afterMs) {
18967
19052
  try {
18968
19053
  const fromDate = afterMs != null ? new Date(afterMs) : /* @__PURE__ */ new Date();
@@ -19000,6 +19085,7 @@ var init_task_template_service = __esm({
19000
19085
  ({ taskTemplates: taskTemplates2 } = schema_exports);
19001
19086
  TaskTemplateService = class {
19002
19087
  static async create(data) {
19088
+ this.validate(data);
19003
19089
  const now = Date.now();
19004
19090
  const result = await db.insert(taskTemplates2).values({ ...data, createdAt: now, updatedAt: now }).returning();
19005
19091
  const tmpl = result[0];
@@ -19015,8 +19101,38 @@ var init_task_template_service = __esm({
19015
19101
  }
19016
19102
  return tmpl;
19017
19103
  }
19104
+ static validate(data) {
19105
+ if (!data.name.trim()) throw new Error("name \u4E0D\u80FD\u4E3A\u7A7A");
19106
+ if (!data.agent.trim()) throw new Error("agent \u4E0D\u80FD\u4E3A\u7A7A");
19107
+ if (!data.prompt.trim()) throw new Error("prompt \u4E0D\u80FD\u4E3A\u7A7A");
19108
+ const scheduleType = data.scheduleType;
19109
+ if (!["cron", "delayed", "recurring"].includes(scheduleType)) {
19110
+ throw new Error("scheduleType \u5FC5\u987B\u662F cron\u3001delayed \u6216 recurring");
19111
+ }
19112
+ if (scheduleType === "cron" && (!data.cronExpr || !isValidCronExpr(data.cronExpr))) {
19113
+ throw new Error("cronExpr \u7F3A\u5931\u6216\u683C\u5F0F\u65E0\u6548");
19114
+ }
19115
+ if (scheduleType === "recurring" && (!Number.isInteger(data.intervalMs) || (data.intervalMs ?? 0) <= 0)) {
19116
+ throw new Error("intervalMs \u5FC5\u987B\u662F\u6B63\u6574\u6570");
19117
+ }
19118
+ if (scheduleType === "delayed" && (!Number.isInteger(data.runAt) || (data.runAt ?? 0) <= 0)) {
19119
+ throw new Error("runAt \u5FC5\u987B\u662F\u6B63\u6574\u6570\u65F6\u95F4\u6233");
19120
+ }
19121
+ this.validateInteger("importance", data.importance, 1, 5);
19122
+ this.validateInteger("urgency", data.urgency, 1, 5);
19123
+ this.validateInteger("maxInstances", data.maxInstances, 1, 1e3);
19124
+ this.validateInteger("maxRetries", data.maxRetries, 0, 1e3);
19125
+ this.validateInteger("retryBackoffMs", data.retryBackoffMs, 0, 864e5);
19126
+ this.validateInteger("timeoutMs", data.timeoutMs, 1e3, 6048e5);
19127
+ }
19128
+ static validateInteger(name, value, min, max) {
19129
+ if (value === void 0 || value === null) return;
19130
+ if (!Number.isInteger(value) || value < min || value > max) {
19131
+ throw new Error(`${name} \u5FC5\u987B\u662F ${min} \u5230 ${max} \u4E4B\u95F4\u7684\u6574\u6570`);
19132
+ }
19133
+ }
19018
19134
  static async list(limit = 50) {
19019
- return await db.select().from(taskTemplates2).orderBy(desc(taskTemplates2.createdAt)).limit(limit);
19135
+ return await db.select().from(taskTemplates2).orderBy(desc(taskTemplates2.createdAt), desc(taskTemplates2.id)).limit(limit);
19020
19136
  }
19021
19137
  static async getById(id) {
19022
19138
  const result = await db.select().from(taskTemplates2).where(eq(taskTemplates2.id, id));
@@ -19059,137 +19175,554 @@ var init_task_template_service = __esm({
19059
19175
  // src/gateway/config.ts
19060
19176
  var config_exports = {};
19061
19177
  __export(config_exports, {
19062
- CONFIG_PATH: () => CONFIG_PATH,
19063
- loadConfig: () => loadConfig
19178
+ DEFAULT_CONFIG: () => DEFAULT_CONFIG,
19179
+ DEFAULT_CONFIG_PATH: () => DEFAULT_CONFIG_PATH,
19180
+ getConfigPath: () => getConfigPath,
19181
+ loadConfig: () => loadConfig,
19182
+ validateConfig: () => validateConfig
19064
19183
  });
19065
- import { readFileSync, existsSync as existsSync2 } from "fs";
19184
+ import { existsSync as existsSync2, readFileSync } from "fs";
19066
19185
  import { homedir as homedir2 } from "os";
19067
19186
  import { join as join2 } from "path";
19068
- function deepMerge(base, override) {
19069
- const result = { ...base };
19070
- for (const key of Object.keys(override)) {
19071
- const val = override[key];
19072
- if (val !== null && typeof val === "object" && !Array.isArray(val) && key in result) {
19073
- const baseVal = result[key];
19074
- if (baseVal !== null && typeof baseVal === "object" && !Array.isArray(baseVal)) {
19075
- result[key] = deepMerge(baseVal, val);
19076
- continue;
19077
- }
19078
- }
19079
- if (val !== void 0) {
19080
- result[key] = val;
19081
- }
19187
+ function getConfigPath() {
19188
+ return process.env.SUPERTASK_CONFIG_PATH ?? DEFAULT_CONFIG_PATH;
19189
+ }
19190
+ function objectAt(value, path) {
19191
+ if (value === void 0) return {};
19192
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
19193
+ throw new Error(`${path} \u5FC5\u987B\u662F\u5BF9\u8C61`);
19082
19194
  }
19083
- return result;
19195
+ return value;
19196
+ }
19197
+ function integerAt(source, key, fallback, min, max, path) {
19198
+ const value = source[key];
19199
+ if (value === void 0) return fallback;
19200
+ if (typeof value !== "number" || !Number.isInteger(value) || value < min || value > max) {
19201
+ throw new Error(`${path}.${key} \u5FC5\u987B\u662F ${min} \u5230 ${max} \u4E4B\u95F4\u7684\u6574\u6570`);
19202
+ }
19203
+ return value;
19204
+ }
19205
+ function booleanAt(source, key, fallback, path) {
19206
+ const value = source[key];
19207
+ if (value === void 0) return fallback;
19208
+ if (typeof value !== "boolean") throw new Error(`${path}.${key} \u5FC5\u987B\u662F\u5E03\u5C14\u503C`);
19209
+ return value;
19084
19210
  }
19085
- function loadConfig() {
19086
- if (!existsSync2(CONFIG_PATH)) {
19087
- return DEFAULT_CONFIG;
19211
+ function validateConfig(input) {
19212
+ const root = objectAt(input, "config");
19213
+ const version2 = root.configVersion ?? 1;
19214
+ if (version2 !== 1 && version2 !== 2) {
19215
+ throw new Error("config.configVersion \u53EA\u652F\u6301 1 \u6216 2");
19216
+ }
19217
+ const worker = objectAt(root.worker, "worker");
19218
+ const scheduler = objectAt(root.scheduler, "scheduler");
19219
+ const watchdog = objectAt(root.watchdog, "watchdog");
19220
+ const dashboard = objectAt(root.dashboard, "dashboard");
19221
+ const heartbeatIntervalMs = integerAt(worker, "heartbeatIntervalMs", DEFAULT_CONFIG.worker.heartbeatIntervalMs, 1e3, 36e5, "worker");
19222
+ const heartbeatTimeoutMs = integerAt(watchdog, "heartbeatTimeoutMs", DEFAULT_CONFIG.watchdog.heartbeatTimeoutMs, 1e3, 864e5, "watchdog");
19223
+ let checkIntervalMs;
19224
+ let cleanupIntervalMs;
19225
+ if (version2 === 1 && watchdog.checkIntervalMs === void 0 && watchdog.cleanupIntervalMs !== void 0) {
19226
+ checkIntervalMs = integerAt(watchdog, "cleanupIntervalMs", DEFAULT_CONFIG.watchdog.checkIntervalMs, 1e3, 36e5, "watchdog");
19227
+ cleanupIntervalMs = DEFAULT_CONFIG.watchdog.cleanupIntervalMs;
19228
+ } else {
19229
+ checkIntervalMs = integerAt(watchdog, "checkIntervalMs", DEFAULT_CONFIG.watchdog.checkIntervalMs, 1e3, 36e5, "watchdog");
19230
+ cleanupIntervalMs = integerAt(watchdog, "cleanupIntervalMs", DEFAULT_CONFIG.watchdog.cleanupIntervalMs, 6e4, 6048e5, "watchdog");
19231
+ }
19232
+ if (heartbeatIntervalMs >= heartbeatTimeoutMs) {
19233
+ throw new Error("worker.heartbeatIntervalMs \u5FC5\u987B\u5C0F\u4E8E watchdog.heartbeatTimeoutMs");
19088
19234
  }
19235
+ if (checkIntervalMs > heartbeatTimeoutMs) {
19236
+ throw new Error("watchdog.checkIntervalMs \u4E0D\u80FD\u5927\u4E8E watchdog.heartbeatTimeoutMs");
19237
+ }
19238
+ return {
19239
+ configVersion: 2,
19240
+ worker: {
19241
+ maxConcurrency: integerAt(worker, "maxConcurrency", DEFAULT_CONFIG.worker.maxConcurrency, 1, 64, "worker"),
19242
+ pollIntervalMs: integerAt(worker, "pollIntervalMs", DEFAULT_CONFIG.worker.pollIntervalMs, 50, 6e4, "worker"),
19243
+ heartbeatIntervalMs,
19244
+ taskTimeoutMs: integerAt(worker, "taskTimeoutMs", DEFAULT_CONFIG.worker.taskTimeoutMs, 1e3, 6048e5, "worker"),
19245
+ shutdownGracePeriodMs: integerAt(worker, "shutdownGracePeriodMs", DEFAULT_CONFIG.worker.shutdownGracePeriodMs, 0, 36e5, "worker")
19246
+ },
19247
+ scheduler: {
19248
+ enabled: booleanAt(scheduler, "enabled", DEFAULT_CONFIG.scheduler.enabled, "scheduler"),
19249
+ checkIntervalMs: integerAt(scheduler, "checkIntervalMs", DEFAULT_CONFIG.scheduler.checkIntervalMs, 100, 6e4, "scheduler")
19250
+ },
19251
+ watchdog: {
19252
+ heartbeatTimeoutMs,
19253
+ checkIntervalMs,
19254
+ cleanupIntervalMs,
19255
+ retentionDays: integerAt(watchdog, "retentionDays", DEFAULT_CONFIG.watchdog.retentionDays, 1, 3650, "watchdog")
19256
+ },
19257
+ dashboard: {
19258
+ enabled: booleanAt(dashboard, "enabled", DEFAULT_CONFIG.dashboard.enabled, "dashboard"),
19259
+ port: integerAt(dashboard, "port", DEFAULT_CONFIG.dashboard.port, 1, 65535, "dashboard")
19260
+ }
19261
+ };
19262
+ }
19263
+ function loadConfig(path = getConfigPath()) {
19264
+ if (!existsSync2(path)) return validateConfig({ configVersion: 2 });
19089
19265
  try {
19090
- const raw2 = readFileSync(CONFIG_PATH, "utf-8");
19091
- const userConfig = JSON.parse(raw2);
19092
- return deepMerge(DEFAULT_CONFIG, userConfig);
19093
- } catch (err) {
19094
- const msg = err instanceof Error ? err.message : String(err);
19095
- console.error(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "warn", msg: "config load failed, using defaults", path: CONFIG_PATH, error: msg }));
19096
- return DEFAULT_CONFIG;
19266
+ return validateConfig(JSON.parse(readFileSync(path, "utf-8")));
19267
+ } catch (error) {
19268
+ const message = error instanceof Error ? error.message : String(error);
19269
+ throw new Error(`\u65E0\u6CD5\u8BFB\u53D6\u914D\u7F6E ${path}: ${message}`);
19097
19270
  }
19098
19271
  }
19099
- var DEFAULT_CONFIG, CONFIG_PATH;
19272
+ var DEFAULT_CONFIG, DEFAULT_CONFIG_PATH;
19100
19273
  var init_config = __esm({
19101
19274
  "src/gateway/config.ts"() {
19102
19275
  "use strict";
19103
19276
  DEFAULT_CONFIG = {
19277
+ configVersion: 2,
19104
19278
  worker: {
19105
19279
  maxConcurrency: 2,
19106
19280
  pollIntervalMs: 1e3,
19107
19281
  heartbeatIntervalMs: 3e4,
19108
- taskTimeoutMs: 18e5
19282
+ taskTimeoutMs: 18e5,
19283
+ shutdownGracePeriodMs: 3e4
19109
19284
  },
19110
19285
  scheduler: {
19111
19286
  enabled: true,
19112
- checkIntervalMs: 1e3,
19113
- catchUp: "next"
19287
+ checkIntervalMs: 1e3
19114
19288
  },
19115
19289
  watchdog: {
19116
19290
  heartbeatTimeoutMs: 6e5,
19117
- cleanupIntervalMs: 6e4,
19291
+ checkIntervalMs: 6e4,
19292
+ cleanupIntervalMs: 864e5,
19118
19293
  retentionDays: 30
19119
19294
  },
19120
19295
  dashboard: {
19121
19296
  enabled: true,
19122
19297
  port: 4680
19123
- },
19124
- logging: {
19125
- level: "info",
19126
- format: "json"
19127
19298
  }
19128
19299
  };
19129
- CONFIG_PATH = join2(homedir2(), ".config/opencode/supertask.json");
19300
+ DEFAULT_CONFIG_PATH = join2(homedir2(), ".config/opencode/supertask.json");
19301
+ }
19302
+ });
19303
+
19304
+ // src/daemon/pm2.ts
19305
+ var pm2_exports = {};
19306
+ __export(pm2_exports, {
19307
+ ensureGateway: () => ensureGateway,
19308
+ getPackageVersion: () => getPackageVersion,
19309
+ install: () => install,
19310
+ installMacLaunchAgent: () => installMacLaunchAgent,
19311
+ isGatewayReady: () => isGatewayReady,
19312
+ isGatewayRunning: () => isGatewayRunning,
19313
+ isPm2Installed: () => isPm2Installed,
19314
+ resolveGatewayEntry: () => resolveGatewayEntry,
19315
+ uninstall: () => uninstall,
19316
+ upgrade: () => upgrade
19317
+ });
19318
+ import { execSync, spawnSync } from "child_process";
19319
+ import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
19320
+ import { homedir as homedir3 } from "os";
19321
+ import { dirname as dirname2, isAbsolute, join as join3 } from "path";
19322
+ import { fileURLToPath as fileURLToPath2 } from "url";
19323
+ import { Database as Database3 } from "bun:sqlite";
19324
+ function getPackageVersion() {
19325
+ const envVersion = process.env.npm_package_version;
19326
+ if (envVersion) return envVersion;
19327
+ try {
19328
+ const pkgPath = join3(__dirname, "../../package.json");
19329
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
19330
+ return typeof pkg.version === "string" ? pkg.version : "0.0.0";
19331
+ } catch {
19332
+ return "0.0.0";
19333
+ }
19334
+ }
19335
+ function resolveGatewayEntry() {
19336
+ const override = process.env.SUPERTASK_GATEWAY_ENTRY;
19337
+ if (override) {
19338
+ if (!existsSync3(override)) throw new Error(`[supertask] Gateway entry not found: ${override}`);
19339
+ return override;
19340
+ }
19341
+ const candidates = [
19342
+ join3(__dirname, "../gateway/index.js"),
19343
+ join3(__dirname, "../gateway/index.ts")
19344
+ ];
19345
+ const entry = candidates.find((candidate) => existsSync3(candidate));
19346
+ if (!entry) throw new Error(`[supertask] Gateway entry not found. Checked: ${candidates.join(", ")}`);
19347
+ return entry;
19348
+ }
19349
+ function versionFile() {
19350
+ return process.env.SUPERTASK_VERSION_FILE ?? join3(homedir3(), ".local/share/opencode/supertask-gateway-version");
19351
+ }
19352
+ function getRunningVersion() {
19353
+ try {
19354
+ const path = versionFile();
19355
+ if (!existsSync3(path)) return null;
19356
+ return readFileSync2(path, "utf-8").trim() || null;
19357
+ } catch {
19358
+ return null;
19359
+ }
19360
+ }
19361
+ function writeRunningVersion(version2) {
19362
+ const path = versionFile();
19363
+ mkdirSync2(dirname2(path), { recursive: true });
19364
+ writeFileSync(path, version2, "utf-8");
19365
+ }
19366
+ function pm2Bin() {
19367
+ return process.env.SUPERTASK_PM2_BIN ?? (process.platform === "win32" ? "pm2.cmd" : "pm2");
19368
+ }
19369
+ function xmlEscape(value) {
19370
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
19371
+ }
19372
+ function resolvePm2Bin() {
19373
+ const configured = pm2Bin();
19374
+ if (isAbsolute(configured)) return configured;
19375
+ const result = spawnSync("which", [configured], { encoding: "utf8" });
19376
+ const resolved = result.status === 0 ? result.stdout.trim().split("\n")[0] : "";
19377
+ if (!resolved) throw new Error(`[supertask] \u65E0\u6CD5\u89E3\u6790 pm2 \u53EF\u6267\u884C\u6587\u4EF6: ${configured}`);
19378
+ return resolved;
19379
+ }
19380
+ function launchAgentPath() {
19381
+ return process.env.SUPERTASK_LAUNCH_AGENT_PATH ?? join3(homedir3(), "Library/LaunchAgents", `${MAC_LAUNCH_AGENT_LABEL}.plist`);
19382
+ }
19383
+ function launchctlBin() {
19384
+ return process.env.SUPERTASK_LAUNCHCTL_BIN ?? "launchctl";
19385
+ }
19386
+ function installMacLaunchAgent() {
19387
+ if (typeof process.getuid !== "function") {
19388
+ throw new Error("[supertask] \u5F53\u524D\u8FD0\u884C\u65F6\u65E0\u6CD5\u83B7\u53D6 macOS \u7528\u6237 ID");
19389
+ }
19390
+ const path = launchAgentPath();
19391
+ const home = homedir3();
19392
+ const pm2Home = process.env.PM2_HOME ?? join3(home, ".pm2");
19393
+ const environmentPath = process.env.PATH ?? "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin";
19394
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
19395
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
19396
+ <plist version="1.0">
19397
+ <dict>
19398
+ <key>Label</key>
19399
+ <string>${MAC_LAUNCH_AGENT_LABEL}</string>
19400
+ <key>ProgramArguments</key>
19401
+ <array>
19402
+ <string>${xmlEscape(resolvePm2Bin())}</string>
19403
+ <string>resurrect</string>
19404
+ </array>
19405
+ <key>RunAtLoad</key>
19406
+ <true/>
19407
+ <key>KeepAlive</key>
19408
+ <dict>
19409
+ <key>SuccessfulExit</key>
19410
+ <false/>
19411
+ </dict>
19412
+ <key>ThrottleInterval</key>
19413
+ <integer>10</integer>
19414
+ <key>EnvironmentVariables</key>
19415
+ <dict>
19416
+ <key>HOME</key>
19417
+ <string>${xmlEscape(home)}</string>
19418
+ <key>PATH</key>
19419
+ <string>${xmlEscape(environmentPath)}</string>
19420
+ <key>PM2_HOME</key>
19421
+ <string>${xmlEscape(pm2Home)}</string>
19422
+ </dict>
19423
+ <key>StandardErrorPath</key>
19424
+ <string>${xmlEscape(join3(pm2Home, "supertask-launchd-error.log"))}</string>
19425
+ <key>StandardOutPath</key>
19426
+ <string>${xmlEscape(join3(pm2Home, "supertask-launchd-output.log"))}</string>
19427
+ </dict>
19428
+ </plist>
19429
+ `;
19430
+ mkdirSync2(dirname2(path), { recursive: true });
19431
+ writeFileSync(path, plist, { mode: 384 });
19432
+ chmodSync(path, 384);
19433
+ const domain = `gui/${process.getuid()}`;
19434
+ spawnSync(launchctlBin(), ["bootout", `${domain}/${MAC_LAUNCH_AGENT_LABEL}`], {
19435
+ stdio: "ignore"
19436
+ });
19437
+ const loaded = spawnSync(launchctlBin(), ["bootstrap", domain, path], {
19438
+ encoding: "utf8"
19439
+ });
19440
+ if (loaded.status !== 0) {
19441
+ const output = `${loaded.stdout ?? ""}${loaded.stderr ?? ""}`.trim();
19442
+ throw new Error(`[supertask] macOS LaunchAgent \u52A0\u8F7D\u5931\u8D25: ${output || `\u9000\u51FA\u7801 ${loaded.status}`}`);
19443
+ }
19444
+ return path;
19445
+ }
19446
+ function isPm2Installed() {
19447
+ const result = spawnSync(pm2Bin(), ["--version"], {
19448
+ stdio: "ignore",
19449
+ shell: process.platform === "win32"
19450
+ });
19451
+ return result.status === 0;
19452
+ }
19453
+ function installPm2() {
19454
+ console.log("[supertask] Installing pm2...");
19455
+ try {
19456
+ execSync("npm install -g pm2", { stdio: "inherit" });
19457
+ return true;
19458
+ } catch {
19459
+ try {
19460
+ execSync("bun install -g pm2", { stdio: "inherit" });
19461
+ return true;
19462
+ } catch {
19463
+ return false;
19464
+ }
19465
+ }
19466
+ }
19467
+ function pm2Exec(args) {
19468
+ const result = spawnSync(pm2Bin(), args, {
19469
+ stdio: ["pipe", "pipe", "pipe"],
19470
+ encoding: "utf-8",
19471
+ env: process.env,
19472
+ shell: process.platform === "win32"
19473
+ });
19474
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
19475
+ if (result.error) return { ok: false, output: result.error.message };
19476
+ return { ok: result.status === 0, output };
19477
+ }
19478
+ function requirePm2(args, action) {
19479
+ const result = pm2Exec(args);
19480
+ if (!result.ok) throw new Error(`[supertask] ${action} failed: ${result.output || "unknown pm2 error"}`);
19481
+ return result.output;
19482
+ }
19483
+ function pm2JsonList() {
19484
+ const output = requirePm2(["jlist"], "pm2 jlist");
19485
+ try {
19486
+ const parsed = JSON.parse(output);
19487
+ if (!Array.isArray(parsed)) throw new Error("result is not an array");
19488
+ return parsed;
19489
+ } catch (error) {
19490
+ const message = error instanceof Error ? error.message : String(error);
19491
+ throw new Error(`[supertask] Invalid pm2 jlist output: ${message}`);
19492
+ }
19493
+ }
19494
+ function isGatewayRunning() {
19495
+ if (!isPm2Installed()) return false;
19496
+ const proc = pm2JsonList().find((item) => item.name === PROCESS_NAME);
19497
+ return proc?.pm2_env?.status === "online" && typeof proc.pid === "number" && isGatewayReady(proc.pid);
19498
+ }
19499
+ function databasePath() {
19500
+ return process.env.SUPERTASK_DB_PATH ?? join3(homedir3(), ".local/share/opencode/tasks.db");
19501
+ }
19502
+ function isGatewayReady(expectedPid) {
19503
+ const path = databasePath();
19504
+ if (!existsSync3(path)) return false;
19505
+ let database = null;
19506
+ try {
19507
+ database = new Database3(path, { readonly: true });
19508
+ const row = database.query(
19509
+ "SELECT pid, heartbeat_at, ready_at FROM gateway_lock WHERE id = 1"
19510
+ ).get();
19511
+ if (!row || row.ready_at == null) return false;
19512
+ if (expectedPid !== void 0 && row.pid !== expectedPid) return false;
19513
+ const ageMs = Date.now() - row.heartbeat_at;
19514
+ return ageMs >= -5e3 && ageMs < GATEWAY_LOCK_STALE_MS;
19515
+ } catch {
19516
+ return false;
19517
+ } finally {
19518
+ database?.close();
19519
+ }
19520
+ }
19521
+ function readyTimeoutMs() {
19522
+ const value = Number(process.env.SUPERTASK_GATEWAY_READY_TIMEOUT_MS ?? 3e4);
19523
+ return Number.isFinite(value) && value > 0 ? value : 3e4;
19524
+ }
19525
+ function waitForGatewayReady(pid) {
19526
+ const deadline = Date.now() + readyTimeoutMs();
19527
+ const sleeper = new Int32Array(new SharedArrayBuffer(4));
19528
+ while (Date.now() < deadline) {
19529
+ if (isGatewayReady(pid)) return true;
19530
+ Atomics.wait(sleeper, 0, 0, 100);
19531
+ }
19532
+ return isGatewayReady(pid);
19533
+ }
19534
+ function findBunPath() {
19535
+ const override = process.env.SUPERTASK_BUN_BIN;
19536
+ if (override) return override;
19537
+ try {
19538
+ const command = process.platform === "win32" ? "where bun" : "which bun";
19539
+ return execSync(command, { stdio: "pipe" }).toString().trim().split("\n")[0];
19540
+ } catch {
19541
+ return process.execPath;
19542
+ }
19543
+ }
19544
+ function pm2StartGateway(gatewayEntry = resolveGatewayEntry()) {
19545
+ const configuredKillTimeout = Number(process.env.SUPERTASK_PM2_KILL_TIMEOUT_MS);
19546
+ const killTimeoutMs = Number.isInteger(configuredKillTimeout) && configuredKillTimeout >= 5e3 ? configuredKillTimeout : loadConfig().worker.shutdownGracePeriodMs + 5e3;
19547
+ requirePm2([
19548
+ "start",
19549
+ findBunPath(),
19550
+ "--name",
19551
+ PROCESS_NAME,
19552
+ "--interpreter",
19553
+ "none",
19554
+ "--restart-delay",
19555
+ "5000",
19556
+ "--max-restarts",
19557
+ "30",
19558
+ "--kill-timeout",
19559
+ String(killTimeoutMs),
19560
+ "--",
19561
+ gatewayEntry
19562
+ ], "pm2 start");
19563
+ const started = pm2JsonList().find((item) => item.name === PROCESS_NAME);
19564
+ if (started?.pm2_env?.status !== "online") {
19565
+ throw new Error(`[supertask] Gateway did not become online (status: ${started?.pm2_env?.status ?? "missing"})`);
19566
+ }
19567
+ if (typeof started.pid !== "number" || !waitForGatewayReady(started.pid)) {
19568
+ throw new Error("[supertask] Gateway \u8FDB\u7A0B online\uFF0C\u4F46\u672A\u5728\u9650\u5B9A\u65F6\u95F4\u5185\u5C31\u7EEA\uFF1B\u8BF7\u67E5\u770B pm2 logs supertask-gateway");
19569
+ }
19570
+ }
19571
+ function savePm2State() {
19572
+ requirePm2(["save"], "pm2 save");
19573
+ }
19574
+ function install() {
19575
+ if (!isPm2Installed() && !installPm2()) {
19576
+ throw new Error("[supertask] Failed to install pm2. Please install it manually: npm install -g pm2");
19577
+ }
19578
+ const existing = pm2JsonList().find((item) => item.name === PROCESS_NAME);
19579
+ if (existing) requirePm2(["delete", PROCESS_NAME], "pm2 delete existing Gateway");
19580
+ const version2 = getPackageVersion();
19581
+ pm2StartGateway();
19582
+ writeRunningVersion(version2);
19583
+ savePm2State();
19584
+ if (process.platform === "darwin") {
19585
+ try {
19586
+ const path = installMacLaunchAgent();
19587
+ console.log(`[supertask] macOS LaunchAgent installed: ${path}`);
19588
+ } catch (error) {
19589
+ console.warn(error instanceof Error ? error.message : String(error));
19590
+ }
19591
+ } else {
19592
+ const startup = pm2Exec(["startup"]);
19593
+ if (startup.output) console.log(startup.output);
19594
+ if (!startup.ok) {
19595
+ console.warn("[supertask] pm2 startup \u672A\u5B8C\u6210\uFF1B\u8BF7\u6309 pm2 \u8F93\u51FA\u6267\u884C\u9700\u8981\u7BA1\u7406\u5458\u6743\u9650\u7684\u547D\u4EE4\uFF0C\u7136\u540E\u8FD0\u884C `pm2 save`\u3002");
19596
+ }
19597
+ }
19598
+ console.log("[supertask] Gateway installed and running.");
19599
+ console.log("[supertask] Manage with: pm2 status / pm2 logs supertask-gateway");
19600
+ }
19601
+ function uninstall() {
19602
+ if (!isPm2Installed()) throw new Error("[supertask] pm2 is not installed");
19603
+ const existing = pm2JsonList().find((item) => item.name === PROCESS_NAME);
19604
+ if (existing) requirePm2(["delete", PROCESS_NAME], "pm2 delete Gateway");
19605
+ savePm2State();
19606
+ console.log("[supertask] Gateway removed from pm2. Other pm2 startup entries were preserved.");
19607
+ }
19608
+ function upgrade(target) {
19609
+ if (!isPm2Installed()) {
19610
+ throw new Error("[supertask] pm2 is not installed. Run `supertask install` first.");
19611
+ }
19612
+ const before = getRunningVersion();
19613
+ const oldGatewayEntry = resolveGatewayEntry();
19614
+ const currentVersion = target?.version ?? getPackageVersion();
19615
+ const existing = pm2JsonList().find((item) => item.name === PROCESS_NAME);
19616
+ if (existing) requirePm2(["delete", PROCESS_NAME], "pm2 delete old Gateway");
19617
+ try {
19618
+ pm2StartGateway(target?.gatewayEntry ?? oldGatewayEntry);
19619
+ writeRunningVersion(currentVersion);
19620
+ savePm2State();
19621
+ return { before, after: currentVersion, restarted: true };
19622
+ } catch (error) {
19623
+ const failed = pm2JsonList().find((item) => item.name === PROCESS_NAME);
19624
+ if (failed) requirePm2(["delete", PROCESS_NAME], "pm2 delete failed Gateway");
19625
+ try {
19626
+ pm2StartGateway(oldGatewayEntry);
19627
+ if (before) writeRunningVersion(before);
19628
+ savePm2State();
19629
+ } catch (rollbackError) {
19630
+ const original = error instanceof Error ? error.message : String(error);
19631
+ const rollback = rollbackError instanceof Error ? rollbackError.message : String(rollbackError);
19632
+ throw new Error(`${original}; \u65E7 Gateway \u56DE\u6EDA\u4E5F\u5931\u8D25: ${rollback}`);
19633
+ }
19634
+ const message = error instanceof Error ? error.message : String(error);
19635
+ throw new Error(`${message}; \u5DF2\u56DE\u6EDA\u5230\u65E7 Gateway`);
19636
+ }
19637
+ }
19638
+ function ensureGateway() {
19639
+ if (!isPm2Installed()) {
19640
+ return { ok: false, reason: "pm2-not-installed" };
19641
+ }
19642
+ const currentVersion = getPackageVersion();
19643
+ const processList = pm2JsonList();
19644
+ const existing = processList.find((item) => item.name === PROCESS_NAME);
19645
+ if (existing?.pm2_env?.status === "online" && typeof existing.pid === "number" && isGatewayReady(existing.pid) && getRunningVersion() === currentVersion) {
19646
+ return { ok: true, action: "already-running" };
19647
+ }
19648
+ if (existing) requirePm2(["delete", PROCESS_NAME], "pm2 delete stale Gateway");
19649
+ pm2StartGateway();
19650
+ writeRunningVersion(currentVersion);
19651
+ savePm2State();
19652
+ return { ok: true, action: existing ? "restarted" : "started" };
19653
+ }
19654
+ var __dirname, PROCESS_NAME, MAC_LAUNCH_AGENT_LABEL, GATEWAY_LOCK_STALE_MS;
19655
+ var init_pm2 = __esm({
19656
+ "src/daemon/pm2.ts"() {
19657
+ "use strict";
19658
+ init_config();
19659
+ __dirname = dirname2(fileURLToPath2(import.meta.url));
19660
+ PROCESS_NAME = "supertask-gateway";
19661
+ MAC_LAUNCH_AGENT_LABEL = "com.supertask.pm2-resurrect";
19662
+ GATEWAY_LOCK_STALE_MS = 3e4;
19130
19663
  }
19131
19664
  });
19132
19665
 
19133
19666
  // src/core/services/task-run.service.ts
19134
- var taskRuns2, TaskRunService;
19667
+ var taskRuns3, TaskRunService;
19135
19668
  var init_task_run_service = __esm({
19136
19669
  "src/core/services/task-run.service.ts"() {
19137
19670
  "use strict";
19138
19671
  init_db2();
19139
19672
  init_drizzle_orm();
19140
- ({ taskRuns: taskRuns2 } = schema_exports);
19673
+ ({ taskRuns: taskRuns3 } = schema_exports);
19141
19674
  TaskRunService = class {
19142
19675
  static async create(data) {
19143
- const result = await db.insert(taskRuns2).values(data).returning();
19676
+ const result = await db.insert(taskRuns3).values(data).returning();
19144
19677
  return result[0];
19145
19678
  }
19146
19679
  static async updateSessionId(id, sessionId) {
19147
- const result = await db.update(taskRuns2).set({ sessionId }).where(eq(taskRuns2.id, id)).returning();
19680
+ const result = await db.update(taskRuns3).set({ sessionId }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
19148
19681
  return result[0] || null;
19149
19682
  }
19150
19683
  static async done(id, log) {
19151
- const result = await db.update(taskRuns2).set({
19684
+ const result = await db.update(taskRuns3).set({
19152
19685
  status: "done",
19153
19686
  finishedAt: /* @__PURE__ */ new Date(),
19154
19687
  log
19155
- }).where(eq(taskRuns2.id, id)).returning();
19688
+ }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
19156
19689
  return result[0] || null;
19157
19690
  }
19158
19691
  static async fail(id, log) {
19159
- const result = await db.update(taskRuns2).set({
19692
+ const result = await db.update(taskRuns3).set({
19160
19693
  status: "failed",
19161
19694
  finishedAt: /* @__PURE__ */ new Date(),
19162
19695
  log
19163
- }).where(eq(taskRuns2.id, id)).returning();
19696
+ }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
19164
19697
  return result[0] || null;
19165
19698
  }
19166
19699
  static async heartbeat(id) {
19167
- const result = await db.update(taskRuns2).set({ heartbeatAt: Date.now() }).where(and(eq(taskRuns2.id, id), eq(taskRuns2.status, "running"))).returning();
19700
+ const result = await db.update(taskRuns3).set({ heartbeatAt: Date.now() }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
19168
19701
  return result[0] || null;
19169
19702
  }
19170
19703
  static async updatePid(id, workerPid, childPid) {
19171
- const result = await db.update(taskRuns2).set({
19704
+ const result = await db.update(taskRuns3).set({
19172
19705
  workerPid,
19173
19706
  childPid,
19174
19707
  lockedAt: Date.now(),
19175
19708
  lockedBy: `gateway-${process.pid}`
19176
- }).where(eq(taskRuns2.id, id)).returning();
19709
+ }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
19177
19710
  return result[0] || null;
19178
19711
  }
19179
19712
  static async getById(id) {
19180
- const result = await db.select().from(taskRuns2).where(eq(taskRuns2.id, id));
19713
+ const result = await db.select().from(taskRuns3).where(eq(taskRuns3.id, id));
19181
19714
  return result[0] || null;
19182
19715
  }
19183
19716
  static async listByTaskId(taskId) {
19184
- return await db.select().from(taskRuns2).where(eq(taskRuns2.taskId, taskId)).orderBy(desc(taskRuns2.startedAt), desc(taskRuns2.id));
19717
+ return await db.select().from(taskRuns3).where(eq(taskRuns3.taskId, taskId)).orderBy(desc(taskRuns3.startedAt), desc(taskRuns3.id));
19185
19718
  }
19186
19719
  static async getLatestByTaskId(taskId) {
19187
- const result = await db.select().from(taskRuns2).where(eq(taskRuns2.taskId, taskId)).orderBy(desc(taskRuns2.startedAt), desc(taskRuns2.id)).limit(1);
19720
+ const result = await db.select().from(taskRuns3).where(eq(taskRuns3.taskId, taskId)).orderBy(desc(taskRuns3.startedAt), desc(taskRuns3.id)).limit(1);
19188
19721
  return result[0] || null;
19189
19722
  }
19190
19723
  static async getLatestByTaskIds(taskIds) {
19191
19724
  if (taskIds.length === 0) return /* @__PURE__ */ new Map();
19192
- const latestRuns = await db.select().from(taskRuns2).where(inArray(taskRuns2.taskId, taskIds)).orderBy(desc(taskRuns2.startedAt));
19725
+ const latestRuns = await db.select().from(taskRuns3).where(inArray(taskRuns3.taskId, taskIds)).orderBy(desc(taskRuns3.startedAt), desc(taskRuns3.id));
19193
19726
  const result = /* @__PURE__ */ new Map();
19194
19727
  for (const run of latestRuns) {
19195
19728
  if (!result.has(run.taskId)) {
@@ -19203,22 +19736,23 @@ var init_task_run_service = __esm({
19203
19736
  const cutoffSec = Math.floor(cutoffMs / 1e3);
19204
19737
  const { tasks: tasksTable } = schema_exports;
19205
19738
  const result = await db.select({
19206
- runId: taskRuns2.id,
19207
- taskId: taskRuns2.taskId,
19208
- childPid: taskRuns2.childPid,
19739
+ runId: taskRuns3.id,
19740
+ taskId: taskRuns3.taskId,
19741
+ childPid: taskRuns3.childPid,
19209
19742
  taskRetryCount: tasksTable.retryCount,
19210
- taskMaxRetries: tasksTable.maxRetries
19211
- }).from(taskRuns2).innerJoin(tasksTable, eq(taskRuns2.taskId, tasksTable.id)).where(
19743
+ taskMaxRetries: tasksTable.maxRetries,
19744
+ taskRetryBackoffMs: tasksTable.retryBackoffMs
19745
+ }).from(taskRuns3).innerJoin(tasksTable, eq(taskRuns3.taskId, tasksTable.id)).where(
19212
19746
  and(
19213
- eq(taskRuns2.status, "running"),
19747
+ eq(taskRuns3.status, "running"),
19214
19748
  or(
19215
19749
  and(
19216
- sql`${taskRuns2.heartbeatAt} IS NULL`,
19217
- sql`${taskRuns2.startedAt} < ${cutoffSec}`
19750
+ sql`${taskRuns3.heartbeatAt} IS NULL`,
19751
+ sql`${taskRuns3.startedAt} < ${cutoffSec}`
19218
19752
  ),
19219
19753
  and(
19220
- sql`${taskRuns2.heartbeatAt} IS NOT NULL`,
19221
- sql`${taskRuns2.heartbeatAt} < ${cutoffMs}`
19754
+ sql`${taskRuns3.heartbeatAt} IS NOT NULL`,
19755
+ sql`${taskRuns3.heartbeatAt} < ${cutoffMs}`
19222
19756
  )
19223
19757
  )
19224
19758
  )
@@ -19228,65 +19762,243 @@ var init_task_run_service = __esm({
19228
19762
  taskId: row.taskId,
19229
19763
  childPid: row.childPid,
19230
19764
  taskRetryCount: row.taskRetryCount ?? 0,
19231
- taskMaxRetries: row.taskMaxRetries ?? 3
19765
+ taskMaxRetries: row.taskMaxRetries ?? 3,
19766
+ taskRetryBackoffMs: row.taskRetryBackoffMs ?? 3e4
19232
19767
  }));
19233
19768
  }
19234
19769
  static async getRunningRunByTaskId(taskId) {
19235
- const result = await db.select().from(taskRuns2).where(and(eq(taskRuns2.taskId, taskId), eq(taskRuns2.status, "running"))).orderBy(desc(taskRuns2.startedAt), desc(taskRuns2.id)).limit(1);
19770
+ const result = await db.select().from(taskRuns3).where(and(eq(taskRuns3.taskId, taskId), eq(taskRuns3.status, "running"))).orderBy(desc(taskRuns3.startedAt), desc(taskRuns3.id)).limit(1);
19236
19771
  return result[0] || null;
19237
19772
  }
19238
19773
  static async deleteByTaskIds(taskIds) {
19239
19774
  if (taskIds.length === 0) return 0;
19240
- const result = await db.delete(taskRuns2).where(inArray(taskRuns2.taskId, taskIds)).returning();
19775
+ const result = await db.delete(taskRuns3).where(inArray(taskRuns3.taskId, taskIds)).returning();
19241
19776
  return result.length;
19242
19777
  }
19243
19778
  static async getAllRunningRuns() {
19244
- return await db.select().from(taskRuns2).where(eq(taskRuns2.status, "running"));
19779
+ return await db.select().from(taskRuns3).where(eq(taskRuns3.status, "running"));
19245
19780
  }
19246
19781
  };
19247
19782
  }
19248
19783
  });
19249
19784
 
19785
+ // src/gateway/health.ts
19786
+ function initializeGatewayHealth(config) {
19787
+ const now = Date.now();
19788
+ state = {
19789
+ startedAt: now,
19790
+ config,
19791
+ lastActivityAt: {
19792
+ worker: now,
19793
+ scheduler: now,
19794
+ watchdog: now
19795
+ }
19796
+ };
19797
+ }
19798
+ function markGatewayActivity(component) {
19799
+ if (state) state.lastActivityAt[component] = Date.now();
19800
+ }
19801
+ function resetGatewayHealth() {
19802
+ state = null;
19803
+ }
19804
+ function componentStatus(component, enabled, maxAgeMs, now) {
19805
+ const lastActivityAt = state?.lastActivityAt[component] ?? null;
19806
+ const ageMs = lastActivityAt == null ? null : Math.max(0, now - lastActivityAt);
19807
+ return {
19808
+ enabled,
19809
+ healthy: !enabled || ageMs != null && ageMs <= maxAgeMs,
19810
+ lastActivityAt,
19811
+ ageMs,
19812
+ maxAgeMs
19813
+ };
19814
+ }
19815
+ function getGatewayHealth(now = Date.now()) {
19816
+ const worker = componentStatus(
19817
+ "worker",
19818
+ true,
19819
+ Math.max((state?.config.workerPollIntervalMs ?? 1e3) * 5, 5e3),
19820
+ now
19821
+ );
19822
+ const scheduler = componentStatus(
19823
+ "scheduler",
19824
+ state?.config.schedulerEnabled ?? false,
19825
+ Math.max((state?.config.schedulerCheckIntervalMs ?? 1e3) * 5, 5e3),
19826
+ now
19827
+ );
19828
+ const watchdog = componentStatus(
19829
+ "watchdog",
19830
+ true,
19831
+ Math.max((state?.config.watchdogCheckIntervalMs ?? 6e4) * 3, 5e3),
19832
+ now
19833
+ );
19834
+ let lock = { pid: null, heartbeatAt: null, readyAt: null, ageMs: null, healthy: false };
19835
+ try {
19836
+ const row = sqlite.prepare(
19837
+ "SELECT pid, heartbeat_at, ready_at FROM gateway_lock WHERE id = 1"
19838
+ ).get();
19839
+ if (row) {
19840
+ const ageMs = Math.max(0, now - row.heartbeat_at);
19841
+ lock = {
19842
+ pid: row.pid,
19843
+ heartbeatAt: row.heartbeat_at,
19844
+ readyAt: row.ready_at,
19845
+ ageMs,
19846
+ healthy: row.pid === process.pid && row.ready_at != null && ageMs < LOCK_STALE_MS
19847
+ };
19848
+ }
19849
+ } catch {
19850
+ }
19851
+ const healthy = state != null && lock.healthy && worker.healthy && scheduler.healthy && watchdog.healthy;
19852
+ return {
19853
+ status: healthy ? "ok" : "degraded",
19854
+ pid: process.pid,
19855
+ uptimeMs: state ? Math.max(0, now - state.startedAt) : 0,
19856
+ lock,
19857
+ components: { worker, scheduler, watchdog }
19858
+ };
19859
+ }
19860
+ var LOCK_STALE_MS, state;
19861
+ var init_health = __esm({
19862
+ "src/gateway/health.ts"() {
19863
+ "use strict";
19864
+ init_db2();
19865
+ LOCK_STALE_MS = 3e4;
19866
+ state = null;
19867
+ }
19868
+ });
19869
+
19870
+ // src/core/process-control.ts
19871
+ import { spawnSync as spawnSync2 } from "child_process";
19872
+ import { basename } from "path";
19873
+ function isSafePid(pid) {
19874
+ return Number.isInteger(pid) && pid > 1 && pid !== process.pid;
19875
+ }
19876
+ function isProcessAlive(pid) {
19877
+ if (!Number.isInteger(pid) || pid <= 0) return false;
19878
+ try {
19879
+ process.kill(pid, 0);
19880
+ return true;
19881
+ } catch (error) {
19882
+ return error instanceof Error && "code" in error && error.code === "EPERM";
19883
+ }
19884
+ }
19885
+ function inspectUnixProcess(pid) {
19886
+ const result = spawnSync2("ps", ["-o", "pgid=", "-o", "command=", "-p", String(pid)], {
19887
+ encoding: "utf8",
19888
+ stdio: ["ignore", "pipe", "ignore"]
19889
+ });
19890
+ if (result.status !== 0) return null;
19891
+ const match2 = result.stdout.trim().match(/^(\d+)\s+(.+)$/s);
19892
+ if (!match2) return null;
19893
+ return { processGroupId: Number(match2[1]), command: match2[2] };
19894
+ }
19895
+ function commandMatches(command, expectedExecutable) {
19896
+ const expectedName = basename(expectedExecutable).trim().toLowerCase();
19897
+ if (!expectedName) return false;
19898
+ return command.toLowerCase().includes(expectedName);
19899
+ }
19900
+ function signalSpawnedProcessTree(pid, signal) {
19901
+ if (!isSafePid(pid)) return false;
19902
+ if (process.platform !== "win32") {
19903
+ try {
19904
+ process.kill(-pid, signal);
19905
+ return true;
19906
+ } catch {
19907
+ }
19908
+ }
19909
+ try {
19910
+ process.kill(pid, signal);
19911
+ return true;
19912
+ } catch {
19913
+ return false;
19914
+ }
19915
+ }
19916
+ function signalRecordedProcessTree(pid, signal, expectedExecutable) {
19917
+ if (!isSafePid(pid)) return false;
19918
+ if (process.platform === "win32") {
19919
+ const list = spawnSync2("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
19920
+ encoding: "utf8",
19921
+ stdio: ["ignore", "pipe", "ignore"]
19922
+ });
19923
+ if (list.status !== 0 || !commandMatches(list.stdout, expectedExecutable)) return false;
19924
+ const args = ["/PID", String(pid), "/T"];
19925
+ if (signal === "SIGKILL") args.push("/F");
19926
+ return spawnSync2("taskkill", args, { stdio: "ignore" }).status === 0;
19927
+ }
19928
+ const info = inspectUnixProcess(pid);
19929
+ if (!info || !commandMatches(info.command, expectedExecutable)) return false;
19930
+ try {
19931
+ if (info.processGroupId === pid) process.kill(-pid, signal);
19932
+ else process.kill(pid, signal);
19933
+ return true;
19934
+ } catch {
19935
+ return false;
19936
+ }
19937
+ }
19938
+ var init_process_control = __esm({
19939
+ "src/core/process-control.ts"() {
19940
+ "use strict";
19941
+ }
19942
+ });
19943
+
19250
19944
  // src/worker/index.ts
19251
19945
  import { spawn } from "child_process";
19252
- var WorkerEngine;
19946
+ var DEFAULT_MAX_OUTPUT_CHARS, FORBIDDEN_AGENT, WorkerEngine;
19253
19947
  var init_worker = __esm({
19254
19948
  "src/worker/index.ts"() {
19255
19949
  "use strict";
19256
19950
  init_task_service();
19257
19951
  init_task_run_service();
19952
+ init_health();
19953
+ init_process_control();
19954
+ DEFAULT_MAX_OUTPUT_CHARS = 64 * 1024;
19955
+ FORBIDDEN_AGENT = "supertask-runner";
19258
19956
  WorkerEngine = class {
19259
19957
  activeBatchIds = /* @__PURE__ */ new Set();
19260
19958
  runningTasks = /* @__PURE__ */ new Map();
19261
19959
  stopped = false;
19262
19960
  pollTimer = null;
19263
19961
  heartbeatTimer = null;
19962
+ pollCyclePromise = null;
19264
19963
  cfg;
19265
- constructor(cfg) {
19964
+ opencodeBin;
19965
+ maxOutputChars;
19966
+ constructor(cfg, options = {}) {
19266
19967
  this.cfg = cfg.worker;
19968
+ this.opencodeBin = options.opencodeBin ?? process.env.SUPERTASK_OPENCODE_BIN ?? "opencode";
19969
+ this.maxOutputChars = options.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
19267
19970
  }
19268
19971
  start() {
19269
19972
  this.stopped = false;
19973
+ markGatewayActivity("worker");
19270
19974
  this.poll();
19271
19975
  this.heartbeatTimer = setInterval(() => this.updateHeartbeats(), this.cfg.heartbeatIntervalMs);
19272
19976
  }
19273
- stop() {
19977
+ async stop(gracePeriodMs = 0) {
19274
19978
  this.stopped = true;
19275
19979
  if (this.pollTimer) {
19276
19980
  clearTimeout(this.pollTimer);
19277
19981
  this.pollTimer = null;
19278
19982
  }
19983
+ if (this.pollCyclePromise) await this.pollCyclePromise;
19984
+ if (gracePeriodMs > 0 && this.runningTasks.size > 0) {
19985
+ const deadline = Date.now() + gracePeriodMs;
19986
+ while (this.runningTasks.size > 0 && Date.now() < deadline) {
19987
+ await Bun.sleep(Math.min(50, deadline - Date.now()));
19988
+ }
19989
+ }
19279
19990
  if (this.heartbeatTimer) {
19280
19991
  clearInterval(this.heartbeatTimer);
19281
19992
  this.heartbeatTimer = null;
19282
19993
  }
19994
+ const interruptedTaskIds = [...this.runningTasks.keys()];
19283
19995
  const killPromises = [];
19284
- for (const [, entry] of this.runningTasks) {
19996
+ for (const entry of this.runningTasks.values()) {
19285
19997
  entry.shutdown = true;
19286
19998
  killPromises.push(this.killEntry(entry));
19287
19999
  }
19288
- return Promise.allSettled(killPromises).then(() => {
19289
- });
20000
+ await Promise.allSettled(killPromises);
20001
+ return interruptedTaskIds;
19290
20002
  }
19291
20003
  getRunningTaskIds() {
19292
20004
  return [...this.runningTasks.keys()];
@@ -19296,136 +20008,244 @@ var init_worker = __esm({
19296
20008
  }
19297
20009
  poll() {
19298
20010
  if (this.stopped) return;
19299
- this.tryDispatch().then(() => {
20011
+ markGatewayActivity("worker");
20012
+ this.pollCyclePromise = this.tryDispatch().finally(() => {
20013
+ this.pollCyclePromise = null;
19300
20014
  if (this.stopped) return;
19301
20015
  this.pollTimer = setTimeout(() => this.poll(), this.cfg.pollIntervalMs);
19302
20016
  });
19303
20017
  }
19304
20018
  async tryDispatch() {
20019
+ await this.reconcileCancelledTasks();
19305
20020
  while (!this.stopped && this.runningTasks.size < this.cfg.maxConcurrency) {
20021
+ let task;
20022
+ try {
20023
+ task = await TaskService.next({ excludedBatchIds: [...this.activeBatchIds] });
20024
+ } catch (err) {
20025
+ this.logError("task claim failed", err);
20026
+ break;
20027
+ }
20028
+ if (!task) break;
20029
+ if (this.stopped) break;
20030
+ if (!await TaskService.start(task.id)) continue;
20031
+ if (task.batchId) this.activeBatchIds.add(task.batchId);
20032
+ if (this.stopped) {
20033
+ await TaskService.resetRunningToPending([task.id]);
20034
+ this.releaseBatch(task);
20035
+ break;
20036
+ }
19306
20037
  try {
19307
- const excludedBatchIds = [...this.activeBatchIds];
19308
- const task = await TaskService.next({ excludedBatchIds });
19309
- if (!task) break;
19310
- if (!await TaskService.start(task.id)) continue;
19311
- if (task.batchId) {
19312
- this.activeBatchIds.add(task.batchId);
19313
- }
19314
20038
  const run = await TaskRunService.create({
19315
20039
  taskId: task.id,
19316
20040
  model: this.resolveModel(task.model),
19317
20041
  status: "running"
19318
20042
  });
19319
- const modelToUse = this.resolveModel(task.model);
19320
- const args = ["run", "--agent", "supertask-runner", "--format", "json"];
19321
- if (modelToUse) {
19322
- args.push("-m", modelToUse);
20043
+ if (this.stopped) {
20044
+ await TaskRunService.fail(run.id, "Gateway shutdown before spawn");
20045
+ await TaskService.resetRunningToPending([task.id]);
20046
+ this.releaseBatch(task);
20047
+ break;
19323
20048
  }
19324
- args.push(`\u6267\u884C\u4EFB\u52A1 ID: ${task.id}${modelToUse ? ` OVERRIDE_MODEL=${modelToUse}` : ""}`);
19325
- const cwd = task.cwd || process.cwd();
19326
- const child = spawn("opencode", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
19327
- await TaskRunService.updatePid(run.id, process.pid, child.pid ?? 0);
19328
- let output = "";
19329
- const handleData = (data) => {
19330
- const text2 = data.toString();
19331
- output += text2;
19332
- process.stdout.write(text2);
19333
- const match2 = text2.match(/"sessionID"\s*:\s*"(ses_[^"]+)"/);
19334
- if (match2) {
19335
- TaskRunService.updateSessionId(run.id, match2[1]).then(() => {
19336
- console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: "sessionId captured", taskId: task.id, sessionId: match2[1] }));
19337
- });
19338
- }
19339
- };
19340
- child.stdout?.on("data", handleData);
19341
- child.stderr?.on("data", handleData);
19342
- const entry = { task, runId: run.id, child, startedAt: Date.now(), shutdown: false };
19343
- this.runningTasks.set(task.id, entry);
19344
- child.on("close", async (code) => {
19345
- this.runningTasks.delete(task.id);
19346
- if (task.batchId) this.activeBatchIds.delete(task.batchId);
19347
- if (entry.shutdown) return;
19348
- const currentRun = await TaskRunService.getById(run.id);
19349
- if (!currentRun || currentRun.status !== "running") return;
19350
- if (code === 0) {
19351
- await TaskRunService.done(run.id);
19352
- await TaskService.done(task.id);
19353
- console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: "task done", taskId: task.id }));
19354
- } else {
19355
- const lastOutput = output.slice(-2e3);
19356
- await TaskRunService.fail(run.id, lastOutput);
19357
- const currentStatus = await TaskService.getById(task.id);
19358
- if (currentStatus?.status === "running") {
19359
- await TaskService.fail(task.id, "Worker\u6267\u884C\u5F02\u5E38\uFF1AOpencode \u8FDB\u7A0B\u975E\u6B63\u5E38\u9000\u51FA");
19360
- }
19361
- console.error(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "error", msg: "task failed", taskId: task.id, code }));
19362
- }
19363
- });
19364
- child.on("error", async (err) => {
19365
- this.runningTasks.delete(task.id);
19366
- if (task.batchId) this.activeBatchIds.delete(task.batchId);
19367
- if (entry.shutdown) return;
19368
- const currentRun = await TaskRunService.getById(run.id);
19369
- if (!currentRun || currentRun.status !== "running") return;
19370
- await TaskRunService.fail(run.id, err.message);
19371
- const currentStatus = await TaskService.getById(task.id);
19372
- if (currentStatus?.status === "running") {
19373
- await TaskService.fail(task.id, `spawn \u5F02\u5E38: ${err.message}`);
19374
- }
19375
- console.error(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "error", msg: "task spawn error", taskId: task.id, error: err.message }));
20049
+ if (task.agent === FORBIDDEN_AGENT) {
20050
+ const message = `\u7981\u6B62\u6267\u884C\u9012\u5F52 Agent: ${FORBIDDEN_AGENT}`;
20051
+ await TaskRunService.fail(run.id, message);
20052
+ await TaskService.fail(task.id, message, {}, { setDeadLetter: true });
20053
+ this.releaseBatch(task);
20054
+ continue;
20055
+ }
20056
+ this.spawnTask(task, run.id);
20057
+ } catch (err) {
20058
+ this.releaseBatch(task);
20059
+ const message = `Worker \u542F\u52A8\u4EFB\u52A1\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`;
20060
+ try {
20061
+ await TaskService.fail(task.id, message);
20062
+ } catch (failErr) {
20063
+ this.logError("failed to compensate task startup", failErr, task.id);
20064
+ }
20065
+ this.logError("task dispatch failed", err, task.id);
20066
+ }
20067
+ }
20068
+ }
20069
+ spawnTask(task, runId) {
20070
+ const model = this.resolveModel(task.model);
20071
+ const args = ["run", "--agent", task.agent, "--format", "json"];
20072
+ if (model) args.push("-m", model);
20073
+ args.push(task.prompt);
20074
+ const child = spawn(this.opencodeBin, args, {
20075
+ cwd: task.cwd || process.cwd(),
20076
+ stdio: ["ignore", "pipe", "pipe"],
20077
+ detached: process.platform !== "win32"
20078
+ });
20079
+ const entry = {
20080
+ task,
20081
+ runId,
20082
+ child,
20083
+ output: "",
20084
+ sessionId: null,
20085
+ timeoutTimer: null,
20086
+ shutdown: false,
20087
+ settled: false
20088
+ };
20089
+ this.runningTasks.set(task.id, entry);
20090
+ const handleData = (data) => {
20091
+ const text2 = data.toString();
20092
+ entry.output = (entry.output + text2).slice(-this.maxOutputChars);
20093
+ process.stdout.write(text2);
20094
+ const match2 = entry.output.match(/"sessionID"\s*:\s*"(ses_[^"]+)"/);
20095
+ if (match2?.[1] && match2[1] !== entry.sessionId) {
20096
+ entry.sessionId = match2[1];
20097
+ TaskRunService.updateSessionId(runId, match2[1]).catch((err) => {
20098
+ this.logError("sessionId update failed", err, task.id);
19376
20099
  });
20100
+ }
20101
+ };
20102
+ child.stdout?.on("data", handleData);
20103
+ child.stderr?.on("data", handleData);
20104
+ child.once("error", (err) => {
20105
+ void this.finishEntry(entry, null, `\u65E0\u6CD5\u542F\u52A8 opencode\uFF1A${err.message}`);
20106
+ });
20107
+ child.once("close", (code, signal) => {
20108
+ const failure = code === 0 ? void 0 : `opencode \u9000\u51FA\u7801 ${code ?? "null"}${signal ? `\uFF0C\u4FE1\u53F7 ${signal}` : ""}`;
20109
+ void this.finishEntry(entry, code, failure);
20110
+ });
20111
+ const timeoutMs = task.timeoutMs ?? this.cfg.taskTimeoutMs;
20112
+ if (timeoutMs > 0) {
20113
+ entry.timeoutTimer = setTimeout(() => {
20114
+ this.signalEntry(entry, "SIGKILL");
20115
+ void this.finishEntry(entry, null, `\u4EFB\u52A1\u8D85\u65F6\uFF08${timeoutMs}ms\uFF09`);
20116
+ }, timeoutMs);
20117
+ }
20118
+ TaskRunService.updatePid(runId, process.pid, child.pid ?? 0).catch((err) => {
20119
+ this.signalEntry(entry, "SIGKILL");
20120
+ void this.finishEntry(entry, null, `\u8BB0\u5F55 Worker PID \u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`);
20121
+ });
20122
+ }
20123
+ async finishEntry(entry, code, failure) {
20124
+ if (entry.settled) return;
20125
+ entry.settled = true;
20126
+ if (entry.timeoutTimer) {
20127
+ clearTimeout(entry.timeoutTimer);
20128
+ entry.timeoutTimer = null;
20129
+ }
20130
+ try {
20131
+ if (entry.shutdown) return;
20132
+ const currentRun = await TaskRunService.getById(entry.runId);
20133
+ if (!currentRun || currentRun.status !== "running") return;
20134
+ const output = entry.output.trim();
20135
+ const log = failure ? `${failure}${output ? `
20136
+ ${output}` : ""}` : output;
20137
+ if (code === 0 && !failure) {
20138
+ const completed = await TaskService.done(entry.task.id, log);
20139
+ if (completed) {
20140
+ await TaskRunService.done(entry.runId, log);
20141
+ console.log(JSON.stringify({
20142
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
20143
+ level: "info",
20144
+ msg: "task done",
20145
+ taskId: entry.task.id
20146
+ }));
20147
+ return;
20148
+ }
20149
+ await TaskRunService.fail(entry.runId, "\u4EFB\u52A1\u72B6\u6001\u5DF2\u88AB\u5176\u4ED6\u64CD\u4F5C\u6539\u53D8");
20150
+ return;
20151
+ }
20152
+ await TaskRunService.fail(entry.runId, log);
20153
+ const failed = await TaskService.fail(entry.task.id, log);
20154
+ if (!failed) {
20155
+ this.logError("task failure state transition rejected", failure ?? "unknown failure", entry.task.id);
20156
+ }
20157
+ console.error(JSON.stringify({
20158
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
20159
+ level: "error",
20160
+ msg: "task failed",
20161
+ taskId: entry.task.id,
20162
+ error: failure
20163
+ }));
20164
+ } finally {
20165
+ this.runningTasks.delete(entry.task.id);
20166
+ this.releaseBatch(entry.task);
20167
+ }
20168
+ }
20169
+ async reconcileCancelledTasks() {
20170
+ for (const entry of [...this.runningTasks.values()]) {
20171
+ try {
20172
+ const task = await TaskService.getById(entry.task.id);
20173
+ if (task?.status === "cancelled") await this.cancelEntry(entry);
19377
20174
  } catch (err) {
19378
- console.error(JSON.stringify({
19379
- ts: (/* @__PURE__ */ new Date()).toISOString(),
19380
- level: "error",
19381
- msg: "tryDispatch iteration failed",
19382
- error: err instanceof Error ? err.message : String(err)
19383
- }));
19384
- break;
20175
+ this.logError("cancel reconciliation failed", err, entry.task.id);
19385
20176
  }
19386
20177
  }
19387
20178
  }
20179
+ async cancelEntry(entry) {
20180
+ if (entry.settled) return;
20181
+ entry.settled = true;
20182
+ if (entry.timeoutTimer) {
20183
+ clearTimeout(entry.timeoutTimer);
20184
+ entry.timeoutTimer = null;
20185
+ }
20186
+ try {
20187
+ await this.killEntry(entry);
20188
+ const output = entry.output.trim();
20189
+ const log = `\u4EFB\u52A1\u5DF2\u53D6\u6D88${output ? `
20190
+ ${output}` : ""}`;
20191
+ await TaskRunService.fail(entry.runId, log);
20192
+ console.log(JSON.stringify({
20193
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
20194
+ level: "info",
20195
+ msg: "running task cancelled",
20196
+ taskId: entry.task.id
20197
+ }));
20198
+ } finally {
20199
+ this.runningTasks.delete(entry.task.id);
20200
+ this.releaseBatch(entry.task);
20201
+ }
20202
+ }
19388
20203
  async updateHeartbeats() {
19389
- for (const [, entry] of this.runningTasks) {
20204
+ for (const entry of this.runningTasks.values()) {
19390
20205
  try {
19391
20206
  await TaskRunService.heartbeat(entry.runId);
19392
- } catch {
20207
+ } catch (err) {
20208
+ this.logError("heartbeat update failed", err, entry.task.id);
19393
20209
  }
19394
20210
  }
19395
20211
  }
19396
20212
  killEntry(entry) {
19397
- if (entry.child.exitCode !== null) {
20213
+ if (entry.child.exitCode !== null || entry.child.signalCode !== null) {
19398
20214
  return Promise.resolve();
19399
20215
  }
19400
20216
  return new Promise((resolve) => {
19401
20217
  const timeout = setTimeout(() => {
19402
- try {
19403
- if (entry.child.pid) process.kill(entry.child.pid, "SIGKILL");
19404
- } catch {
19405
- }
20218
+ this.signalEntry(entry, "SIGKILL");
19406
20219
  resolve();
19407
20220
  }, 5e3);
19408
- entry.child.on("close", () => {
20221
+ entry.child.once("close", () => {
19409
20222
  clearTimeout(timeout);
19410
20223
  resolve();
19411
20224
  });
19412
- try {
19413
- if (entry.child.pid) {
19414
- entry.child.kill("SIGTERM");
19415
- } else {
19416
- clearTimeout(timeout);
19417
- resolve();
19418
- }
19419
- } catch {
19420
- clearTimeout(timeout);
19421
- resolve();
19422
- }
20225
+ this.signalEntry(entry, "SIGTERM");
19423
20226
  });
19424
20227
  }
20228
+ signalEntry(entry, signal) {
20229
+ const pid = entry.child.pid;
20230
+ if (!pid) return;
20231
+ signalSpawnedProcessTree(pid, signal);
20232
+ }
20233
+ releaseBatch(task) {
20234
+ if (task.batchId) this.activeBatchIds.delete(task.batchId);
20235
+ }
19425
20236
  resolveModel(taskModel) {
19426
20237
  if (!taskModel || taskModel === "default") return null;
19427
20238
  return taskModel;
19428
20239
  }
20240
+ logError(message, error, taskId) {
20241
+ console.error(JSON.stringify({
20242
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
20243
+ level: "error",
20244
+ msg: message,
20245
+ taskId,
20246
+ error: error instanceof Error ? error.message : String(error)
20247
+ }));
20248
+ }
19429
20249
  };
19430
20250
  }
19431
20251
  });
@@ -19437,15 +20257,23 @@ async function checkHeartbeats(heartbeatTimeoutMs) {
19437
20257
  for (const run of staleRuns) {
19438
20258
  try {
19439
20259
  if (run.childPid != null && run.childPid > 0) {
19440
- try {
19441
- process.kill(run.childPid, "SIGKILL");
19442
- } catch {
20260
+ const expectedExecutable = process.env.SUPERTASK_OPENCODE_BIN ?? "opencode";
20261
+ const killed = signalRecordedProcessTree(run.childPid, "SIGKILL", expectedExecutable);
20262
+ if (!killed) {
20263
+ console.warn(JSON.stringify({
20264
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
20265
+ level: "warn",
20266
+ msg: "stale child process was not killed because identity validation failed",
20267
+ taskId: run.taskId,
20268
+ runId: run.runId,
20269
+ childPid: run.childPid
20270
+ }));
19443
20271
  }
19444
20272
  }
19445
20273
  await TaskRunService.fail(run.runId, `\u5FC3\u8DF3\u8D85\u65F6 (${heartbeatTimeoutMs / 1e3}s)\uFF0CWatchdog kill`);
19446
20274
  const newRetryCount = run.taskRetryCount + 1;
19447
20275
  const maxRetries = run.taskMaxRetries;
19448
- if (newRetryCount >= maxRetries) {
20276
+ if (newRetryCount > maxRetries) {
19449
20277
  await TaskService.markDeadLetter(run.taskId, newRetryCount);
19450
20278
  console.log(JSON.stringify({
19451
20279
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -19457,7 +20285,7 @@ async function checkHeartbeats(heartbeatTimeoutMs) {
19457
20285
  maxRetries
19458
20286
  }));
19459
20287
  } else {
19460
- const backoffMs = computeBackoff(newRetryCount);
20288
+ const backoffMs = computeBackoff(newRetryCount, run.taskRetryBackoffMs);
19461
20289
  const retryAfter = Date.now() + backoffMs;
19462
20290
  await TaskService.markPendingForRetry(run.taskId, retryAfter, newRetryCount);
19463
20291
  console.log(JSON.stringify({
@@ -19488,6 +20316,7 @@ var init_heartbeat = __esm({
19488
20316
  init_task_run_service();
19489
20317
  init_task_service();
19490
20318
  init_backoff();
20319
+ init_process_control();
19491
20320
  }
19492
20321
  });
19493
20322
 
@@ -19532,6 +20361,7 @@ var init_watchdog = __esm({
19532
20361
  "use strict";
19533
20362
  init_heartbeat();
19534
20363
  init_cleanup();
20364
+ init_health();
19535
20365
  Watchdog = class {
19536
20366
  constructor(cfg) {
19537
20367
  this.cfg = cfg;
@@ -19542,13 +20372,14 @@ var init_watchdog = __esm({
19542
20372
  cleanupTimer = null;
19543
20373
  start() {
19544
20374
  this.stopped = false;
20375
+ markGatewayActivity("watchdog");
19545
20376
  this.heartbeatTimer = setInterval(
19546
20377
  () => this.runHeartbeatCheck(),
19547
- this.cfg.watchdog.cleanupIntervalMs
20378
+ this.cfg.watchdog.checkIntervalMs
19548
20379
  );
19549
20380
  this.cleanupTimer = setInterval(
19550
20381
  () => this.runCleanup(),
19551
- this.cfg.watchdog.cleanupIntervalMs * 24 * 60
20382
+ this.cfg.watchdog.cleanupIntervalMs
19552
20383
  );
19553
20384
  }
19554
20385
  stop() {
@@ -19564,6 +20395,7 @@ var init_watchdog = __esm({
19564
20395
  }
19565
20396
  async runHeartbeatCheck() {
19566
20397
  if (this.stopped) return;
20398
+ markGatewayActivity("watchdog");
19567
20399
  try {
19568
20400
  await checkHeartbeats(this.cfg.watchdog.heartbeatTimeoutMs);
19569
20401
  } catch (err) {
@@ -19596,7 +20428,7 @@ var init_watchdog = __esm({
19596
20428
  async function cloneTaskFromTemplate(templateId) {
19597
20429
  const rows = await db.select().from(taskTemplates3).where(eq(taskTemplates3.id, templateId)).limit(1);
19598
20430
  const tmpl = rows[0];
19599
- if (!tmpl) return null;
20431
+ if (!tmpl || !tmpl.enabled) return null;
19600
20432
  const activeCount = await db.select({ count: sql`count(*)` }).from(schema_exports.tasks).where(
19601
20433
  and(
19602
20434
  eq(schema_exports.tasks.templateId, templateId),
@@ -19605,7 +20437,8 @@ async function cloneTaskFromTemplate(templateId) {
19605
20437
  ).then((r) => r[0].count);
19606
20438
  if (activeCount >= (tmpl.maxInstances ?? 1)) return null;
19607
20439
  const nowMs = Date.now();
19608
- const nextRunAt = TaskTemplateService.calculateNextRunAt(
20440
+ const isDelayed = tmpl.scheduleType === "delayed";
20441
+ const nextRunAt = isDelayed ? null : TaskTemplateService.calculateNextRunAt(
19609
20442
  tmpl.scheduleType,
19610
20443
  tmpl,
19611
20444
  nowMs
@@ -19619,13 +20452,17 @@ async function cloneTaskFromTemplate(templateId) {
19619
20452
  category: tmpl.category ?? "general",
19620
20453
  importance: tmpl.importance ?? 3,
19621
20454
  urgency: tmpl.urgency ?? 3,
20455
+ batchId: tmpl.batchId,
19622
20456
  maxRetries: tmpl.maxRetries ?? 3,
20457
+ retryBackoffMs: tmpl.retryBackoffMs ?? 3e4,
20458
+ timeoutMs: tmpl.timeoutMs,
19623
20459
  templateId: tmpl.id,
19624
20460
  scheduledAt: nowMs
19625
20461
  });
19626
20462
  await db.update(taskTemplates3).set({
19627
20463
  lastRunAt: nowMs,
19628
20464
  nextRunAt,
20465
+ enabled: isDelayed ? false : tmpl.enabled,
19629
20466
  updatedAt: nowMs
19630
20467
  }).where(eq(taskTemplates3.id, templateId));
19631
20468
  return task;
@@ -19638,7 +20475,7 @@ async function getDueTemplates() {
19638
20475
  sql`${taskTemplates3.nextRunAt} IS NOT NULL`,
19639
20476
  sql`${taskTemplates3.nextRunAt} <= ${nowMs}`
19640
20477
  )
19641
- );
20478
+ ).orderBy(asc(taskTemplates3.nextRunAt), asc(taskTemplates3.id));
19642
20479
  }
19643
20480
  async function initializeNextRunAt(templateId) {
19644
20481
  const rows = await db.select().from(taskTemplates3).where(eq(taskTemplates3.id, templateId)).limit(1);
@@ -19672,6 +20509,7 @@ var init_scheduler = __esm({
19672
20509
  init_job_templates();
19673
20510
  init_db2();
19674
20511
  init_drizzle_orm();
20512
+ init_health();
19675
20513
  Scheduler = class {
19676
20514
  constructor(cfg) {
19677
20515
  this.cfg = cfg;
@@ -19683,6 +20521,7 @@ var init_scheduler = __esm({
19683
20521
  async start() {
19684
20522
  if (!this.cfg.scheduler.enabled) return;
19685
20523
  this.stopped = false;
20524
+ markGatewayActivity("scheduler");
19686
20525
  await this.initializeTemplates();
19687
20526
  this.timer = setInterval(() => this.tick(), this.cfg.scheduler.checkIntervalMs);
19688
20527
  }
@@ -19695,6 +20534,7 @@ var init_scheduler = __esm({
19695
20534
  }
19696
20535
  async tick() {
19697
20536
  if (this.stopped || this.ticking) return;
20537
+ markGatewayActivity("scheduler");
19698
20538
  this.ticking = true;
19699
20539
  try {
19700
20540
  const dueTemplates = await getDueTemplates();
@@ -19749,13 +20589,13 @@ var init_compose = __esm({
19749
20589
  "use strict";
19750
20590
  compose = (middleware, onError, onNotFound) => {
19751
20591
  return (context, next) => {
19752
- let index = -1;
20592
+ let index2 = -1;
19753
20593
  return dispatch(0);
19754
20594
  async function dispatch(i) {
19755
- if (i <= index) {
20595
+ if (i <= index2) {
19756
20596
  throw new Error("next() called multiple times");
19757
20597
  }
19758
- index = i;
20598
+ index2 = i;
19759
20599
  let res;
19760
20600
  let isError = false;
19761
20601
  let handler;
@@ -19870,8 +20710,8 @@ var init_body = __esm({
19870
20710
  handleParsingNestedValues = (form, key, value) => {
19871
20711
  let nestedForm = form;
19872
20712
  const keys = key.split(".");
19873
- keys.forEach((key2, index) => {
19874
- if (index === keys.length - 1) {
20713
+ keys.forEach((key2, index2) => {
20714
+ if (index2 === keys.length - 1) {
19875
20715
  nestedForm[key2] = value;
19876
20716
  } else {
19877
20717
  if (!nestedForm[key2] || typeof nestedForm[key2] !== "object" || Array.isArray(nestedForm[key2]) || nestedForm[key2] instanceof File) {
@@ -19903,8 +20743,8 @@ var init_url = __esm({
19903
20743
  };
19904
20744
  extractGroupsFromPath = (path) => {
19905
20745
  const groups = [];
19906
- path = path.replace(/\{[^}]+\}/g, (match2, index) => {
19907
- const mark = `@${index}`;
20746
+ path = path.replace(/\{[^}]+\}/g, (match2, index2) => {
20747
+ const mark = `@${index2}`;
19908
20748
  groups.push([mark, match2]);
19909
20749
  return mark;
19910
20750
  });
@@ -20423,10 +21263,10 @@ var init_html = __esm({
20423
21263
  return;
20424
21264
  }
20425
21265
  let escape;
20426
- let index;
21266
+ let index2;
20427
21267
  let lastIndex = 0;
20428
- for (index = match2; index < str.length; index++) {
20429
- switch (str.charCodeAt(index)) {
21268
+ for (index2 = match2; index2 < str.length; index2++) {
21269
+ switch (str.charCodeAt(index2)) {
20430
21270
  case 34:
20431
21271
  escape = "&quot;";
20432
21272
  break;
@@ -20445,10 +21285,10 @@ var init_html = __esm({
20445
21285
  default:
20446
21286
  continue;
20447
21287
  }
20448
- buffer[0] += str.substring(lastIndex, index) + escape;
20449
- lastIndex = index + 1;
21288
+ buffer[0] += str.substring(lastIndex, index2) + escape;
21289
+ lastIndex = index2 + 1;
20450
21290
  }
20451
- buffer[0] += str.substring(lastIndex, index);
21291
+ buffer[0] += str.substring(lastIndex, index2);
20452
21292
  };
20453
21293
  resolveCallbackSync = (str) => {
20454
21294
  const callbacks = str.callbacks;
@@ -21324,8 +22164,8 @@ function match(method, path) {
21324
22164
  if (!match3) {
21325
22165
  return [[], emptyParam];
21326
22166
  }
21327
- const index = match3.indexOf("", 1);
21328
- return [matcher[1][index], match3];
22167
+ const index2 = match3.indexOf("", 1);
22168
+ return [matcher[1][index2], match3];
21329
22169
  });
21330
22170
  this.match = match2;
21331
22171
  return match2(method, path);
@@ -21372,7 +22212,7 @@ var init_node = __esm({
21372
22212
  #index;
21373
22213
  #varIndex;
21374
22214
  #children = /* @__PURE__ */ Object.create(null);
21375
- insert(tokens, index, paramMap, context, pathErrorCheckOnly) {
22215
+ insert(tokens, index2, paramMap, context, pathErrorCheckOnly) {
21376
22216
  if (tokens.length === 0) {
21377
22217
  if (this.#index !== void 0) {
21378
22218
  throw PATH_ERROR;
@@ -21380,7 +22220,7 @@ var init_node = __esm({
21380
22220
  if (pathErrorCheckOnly) {
21381
22221
  return;
21382
22222
  }
21383
- this.#index = index;
22223
+ this.#index = index2;
21384
22224
  return;
21385
22225
  }
21386
22226
  const [token, ...restTokens] = tokens;
@@ -21430,7 +22270,7 @@ var init_node = __esm({
21430
22270
  node = this.#children[token] = new _Node();
21431
22271
  }
21432
22272
  }
21433
- node.insert(restTokens, index, paramMap, context, pathErrorCheckOnly);
22273
+ node.insert(restTokens, index2, paramMap, context, pathErrorCheckOnly);
21434
22274
  }
21435
22275
  buildRegExpStr() {
21436
22276
  const childKeys = Object.keys(this.#children).sort(compareKey);
@@ -21462,7 +22302,7 @@ var init_trie = __esm({
21462
22302
  Trie = class {
21463
22303
  #context = { varIndex: 0 };
21464
22304
  #root = new Node();
21465
- insert(path, index, pathErrorCheckOnly) {
22305
+ insert(path, index2, pathErrorCheckOnly) {
21466
22306
  const paramAssoc = [];
21467
22307
  const groups = [];
21468
22308
  for (let i = 0; ; ) {
@@ -21488,7 +22328,7 @@ var init_trie = __esm({
21488
22328
  }
21489
22329
  }
21490
22330
  }
21491
- this.#root.insert(tokens, index, paramAssoc, this.#context, pathErrorCheckOnly);
22331
+ this.#root.insert(tokens, index2, paramAssoc, this.#context, pathErrorCheckOnly);
21492
22332
  return paramAssoc;
21493
22333
  }
21494
22334
  buildRegExp() {
@@ -22082,8 +22922,19 @@ __export(web_exports, {
22082
22922
  dashboardApp: () => dashboardApp,
22083
22923
  default: () => web_default
22084
22924
  });
22085
- import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
22086
- import { dirname as dirname2 } from "path";
22925
+ import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, renameSync } from "fs";
22926
+ import { dirname as dirname3 } from "path";
22927
+ function parsePositiveInteger(value) {
22928
+ if (!/^\d+$/.test(value)) return null;
22929
+ const parsed = Number(value);
22930
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
22931
+ }
22932
+ function parseTaskStatus(value) {
22933
+ return TASK_STATUSES.has(value) ? value : null;
22934
+ }
22935
+ function safeStatus(value) {
22936
+ return value && TASK_STATUSES.has(value) ? value : "unknown";
22937
+ }
22087
22938
  function formatDuration(startAt, endAt) {
22088
22939
  if (!startAt) return "-";
22089
22940
  const start = new Date(startAt).getTime();
@@ -22121,17 +22972,21 @@ function esc(s) {
22121
22972
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
22122
22973
  }
22123
22974
  function readCurrentConfig() {
22124
- if (!existsSync3(CONFIG_PATH)) return {};
22975
+ const configPath = getConfigPath();
22976
+ if (!existsSync4(configPath)) return {};
22125
22977
  try {
22126
- return JSON.parse(readFileSync2(CONFIG_PATH, "utf-8"));
22978
+ return JSON.parse(readFileSync3(configPath, "utf-8"));
22127
22979
  } catch {
22128
22980
  return {};
22129
22981
  }
22130
22982
  }
22131
22983
  function writeConfig(cfg) {
22132
- const dir = dirname2(CONFIG_PATH);
22133
- if (!existsSync3(dir)) mkdirSync2(dir, { recursive: true });
22134
- writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2) + "\n");
22984
+ const configPath = getConfigPath();
22985
+ const dir = dirname3(configPath);
22986
+ if (!existsSync4(dir)) mkdirSync3(dir, { recursive: true });
22987
+ const tempPath = `${configPath}.${process.pid}.tmp`;
22988
+ writeFileSync2(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
22989
+ renameSync(tempPath, configPath);
22135
22990
  }
22136
22991
  function renderLayout(title, activeTab, body) {
22137
22992
  return `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>${title} - SuperTask</title>${SHARED_STYLES}
@@ -22170,11 +23025,11 @@ async function saveConfig(){
22170
23025
  scheduler:{
22171
23026
  enabled:form.se.checked,
22172
23027
  checkIntervalMs:Number(form.si.value),
22173
- catchUp:form.cu.value,
22174
23028
  },
22175
23029
  watchdog:{
22176
23030
  heartbeatTimeoutMs:Number(form.wt.value)*1000,
22177
- cleanupIntervalMs:Number(form.wc.value)*1000,
23031
+ checkIntervalMs:Number(form.wci.value)*1000,
23032
+ cleanupIntervalMs:Number(form.wcl.value)*3600000,
22178
23033
  retentionDays:Number(form.rd.value),
22179
23034
  }
22180
23035
  };
@@ -22205,7 +23060,7 @@ async function saveConfig(){
22205
23060
  <dialog id="dd"><div class="dh"><h3 style="margin:0">\u8BE6\u60C5</h3><button class="cb" onclick="document.getElementById('dd').close()">&times;</button></div><div class="db"><pre id="dc"></pre></div></dialog>
22206
23061
  </body></html>`;
22207
23062
  }
22208
- var app, SHARED_STYLES, dashboardApp, web_default;
23063
+ var app, TASK_STATUSES, SHARED_STYLES, dashboardApp, web_default;
22209
23064
  var init_web = __esm({
22210
23065
  "src/web/index.tsx"() {
22211
23066
  "use strict";
@@ -22217,7 +23072,47 @@ var init_web = __esm({
22217
23072
  init_drizzle_orm();
22218
23073
  init_db2();
22219
23074
  init_config();
23075
+ init_health();
22220
23076
  app = new Hono2();
23077
+ TASK_STATUSES = /* @__PURE__ */ new Set([
23078
+ "pending",
23079
+ "running",
23080
+ "done",
23081
+ "failed",
23082
+ "dead_letter",
23083
+ "cancelled"
23084
+ ]);
23085
+ app.use("*", async (c, next) => {
23086
+ await next();
23087
+ c.header("X-Content-Type-Options", "nosniff");
23088
+ c.header("X-Frame-Options", "DENY");
23089
+ c.header("Referrer-Policy", "no-referrer");
23090
+ });
23091
+ app.use("/api/*", async (c, next) => {
23092
+ if (["GET", "HEAD", "OPTIONS"].includes(c.req.method)) return next();
23093
+ const fetchSite = c.req.header("Sec-Fetch-Site");
23094
+ if (fetchSite && !["same-origin", "none"].includes(fetchSite)) {
23095
+ return c.json({ error: "cross-site request rejected" }, 403);
23096
+ }
23097
+ const origin = c.req.header("Origin");
23098
+ if (origin) {
23099
+ try {
23100
+ const originUrl = new URL(origin);
23101
+ const requestUrl = new URL(c.req.url);
23102
+ const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(originUrl.hostname);
23103
+ if (!loopback || originUrl.origin !== requestUrl.origin) {
23104
+ return c.json({ error: "cross-site request rejected" }, 403);
23105
+ }
23106
+ } catch {
23107
+ return c.json({ error: "invalid origin" }, 403);
23108
+ }
23109
+ }
23110
+ return next();
23111
+ });
23112
+ app.get("/health", (c) => {
23113
+ const health = getGatewayHealth();
23114
+ return c.json(health, health.status === "ok" ? 200 : 503);
23115
+ });
22221
23116
  SHARED_STYLES = html`
22222
23117
  <style>
22223
23118
  :root { --bg:#0d1117; --card:#161b22; --border:#30363d; --t1:#c9d1d9; --t2:#8b949e; --green:#238636; --red:#da3633; --yellow:#d29922; --blue:#1f6feb; --purple:#8957e5; }
@@ -22294,12 +23189,16 @@ var init_web = __esm({
22294
23189
  </style>
22295
23190
  `;
22296
23191
  app.get("/", async (c) => {
22297
- const page = Number(c.req.query("page") || "1");
23192
+ const pageParam = c.req.query("page") || "1";
23193
+ const page = parsePositiveInteger(pageParam);
23194
+ if (page === null) return c.text("invalid page", 400);
22298
23195
  const statusFilter = c.req.query("status") || "";
23196
+ const parsedStatus = statusFilter ? parseTaskStatus(statusFilter) : null;
23197
+ if (statusFilter && !parsedStatus) return c.text("invalid status", 400);
22299
23198
  const limit = 50;
22300
23199
  const offset = (page - 1) * limit;
22301
23200
  const [tasks3, statsData] = await Promise.all([
22302
- TaskService.list({ limit, offset, ...statusFilter ? { status: statusFilter } : {} }),
23201
+ TaskService.list({ limit, offset, ...parsedStatus ? { status: parsedStatus } : {} }),
22303
23202
  TaskService.stats({})
22304
23203
  ]);
22305
23204
  const taskIds = tasks3.map((t) => t.id);
@@ -22322,12 +23221,13 @@ var init_web = __esm({
22322
23221
  </div>`;
22323
23222
  let rows = "";
22324
23223
  for (const task of tasks3) {
22325
- const st = (task.status ?? "").toUpperCase();
23224
+ const status = safeStatus(task.status);
23225
+ const st = status.toUpperCase();
22326
23226
  rows += `<tr>
22327
23227
  <td class="mu">#${task.id}</td>
22328
23228
  <td><div style="font-weight:500">${esc(task.name)}</div><div class="mu sm el">${esc(task.prompt.substring(0, 120))}</div></td>
22329
23229
  <td><span class="tag">${esc(task.agent)}</span></td>
22330
- <td><span class="badge b-${task.status}">${st}</span></td>
23230
+ <td><span class="badge b-${status}">${st}</span></td>
22331
23231
  <td class="sm ${task.status === "running" ? "" : "mu"}">${formatDuration(task.startedAt, task.finishedAt)}</td>
22332
23232
  <td class="mu sm">${(task.retryCount ?? 0) > 0 ? task.retryCount : "-"}</td>
22333
23233
  <td>
@@ -22359,21 +23259,13 @@ var init_web = __esm({
22359
23259
  });
22360
23260
  app.get("/templates", async (c) => {
22361
23261
  const templates = await TaskTemplateService.list(100);
22362
- for (const tmpl of templates) {
22363
- if (tmpl.enabled && tmpl.scheduleType === "cron" && tmpl.cronExpr) {
22364
- try {
22365
- const { getNextCronRun: getNextCronRun2 } = await Promise.resolve().then(() => (init_cron_parser(), cron_parser_exports));
22366
- tmpl.nextRunAt = getNextCronRun2(tmpl.cronExpr, Date.now());
22367
- } catch {
22368
- }
22369
- }
22370
- }
22371
23262
  const enabled = templates.filter((t) => t.enabled).length;
22372
23263
  const disabled = templates.length - enabled;
22373
23264
  let rows = "";
22374
23265
  for (const t of templates) {
22375
- const typeLabel = t.scheduleType === "cron" ? "Cron" : t.scheduleType === "recurring" ? "\u5FAA\u73AF" : "\u5B9A\u65F6";
22376
- const typeClass = "tag t-" + t.scheduleType;
23266
+ const scheduleType = ["cron", "recurring", "delayed"].includes(t.scheduleType) ? t.scheduleType : "unknown";
23267
+ const typeLabel = scheduleType === "cron" ? "Cron" : scheduleType === "recurring" ? "\u5FAA\u73AF" : scheduleType === "delayed" ? "\u5B9A\u65F6" : "\u672A\u77E5";
23268
+ const typeClass = "tag t-" + scheduleType;
22377
23269
  let rule = "-";
22378
23270
  if (t.scheduleType === "cron") rule = t.cronExpr || "-";
22379
23271
  else if (t.scheduleType === "recurring") rule = t.intervalMs ? `${Math.floor(t.intervalMs / 6e4)}\u5206\u949F` : "-";
@@ -22385,7 +23277,7 @@ var init_web = __esm({
22385
23277
  <td><div style="font-weight:500">${esc(t.name)}</div><div class="mu sm el">${esc(t.prompt.substring(0, 100))}</div>
22386
23278
  <div class="sm" style="margin-top:2px"><span class="tag">${esc(t.agent)}</span>${t.model && t.model !== "default" ? ` <span class="tag">${esc(t.model)}</span>` : ""}</div></td>
22387
23279
  <td><span class="${typeClass}">${typeLabel}</span></td>
22388
- <td class="m sm">${rule}</td>
23280
+ <td class="m sm">${esc(rule)}</td>
22389
23281
  <td>${statusBadge}</td>
22390
23282
  <td class="sm">${t.lastRunAt ? timeAgo(t.lastRunAt) : "-"}</td>
22391
23283
  <td class="sm">${t.nextRunAt ? timeUntil(t.nextRunAt) : "-"}</td>
@@ -22413,7 +23305,8 @@ var init_web = __esm({
22413
23305
  return c.html(renderLayout("\u5B9A\u65F6\u4EFB\u52A1", "templates", body));
22414
23306
  });
22415
23307
  app.get("/runs", async (c) => {
22416
- const page = Number(c.req.query("page") || "1");
23308
+ const page = parsePositiveInteger(c.req.query("page") || "1");
23309
+ if (page === null) return c.text("invalid page", 400);
22417
23310
  const limit = 50;
22418
23311
  const offset = (page - 1) * limit;
22419
23312
  const { taskRuns: tr, tasks: tk } = schema_exports;
@@ -22431,13 +23324,14 @@ var init_web = __esm({
22431
23324
  childPid: tr.childPid,
22432
23325
  taskName: tk.name,
22433
23326
  taskAgent: tk.agent
22434
- }).from(tr).innerJoin(tk, eq(tr.taskId, tk.id)).orderBy(desc(tr.startedAt)).limit(limit).offset(offset);
23327
+ }).from(tr).innerJoin(tk, eq(tr.taskId, tk.id)).orderBy(desc(tr.startedAt), desc(tr.id)).limit(limit).offset(offset);
22435
23328
  const totalResult = await db.select({ count: sql`count(*)` }).from(tr);
22436
23329
  const total = Number(totalResult[0]?.count ?? 0);
22437
23330
  const totalPages = Math.ceil(total / limit);
22438
23331
  let rows = "";
22439
23332
  const logsHtml = [];
22440
23333
  for (const run of runs) {
23334
+ const status = safeStatus(run.status);
22441
23335
  const shortSession = run.sessionId ? run.sessionId.slice(4, 7) + "***" + run.sessionId.slice(-3) : "-";
22442
23336
  const logBtn = run.log ? `<button class="btn btn-sm" onclick="toggleLog(${run.id})">\u65E5\u5FD7</button>` : "";
22443
23337
  rows += `<tr>
@@ -22445,15 +23339,15 @@ var init_web = __esm({
22445
23339
  <td><div style="font-weight:500">${esc(run.taskName)} <span class="mu">(#${run.taskId})</span></div>
22446
23340
  ${run.model ? `<div class="sm"><span class="tag">${esc(run.model)}</span></div>` : ""}</td>
22447
23341
  <td><span class="tag">${esc(run.taskAgent)}</span></td>
22448
- <td><span class="badge b-${run.status}">${(run.status ?? "").toUpperCase()}</span></td>
23342
+ <td><span class="badge b-${status}">${status.toUpperCase()}</span></td>
22449
23343
  <td class="sm">${formatDuration(run.startedAt, run.finishedAt)}</td>
22450
23344
  <td class="sm mu">${run.heartbeatAt ? timeAgo(run.heartbeatAt) : "-"}</td>
22451
23345
  <td><button class="btn btn-sm" onclick="showRunDetail(${run.id})">\u8BE6\u60C5</button>${logBtn}</td>
22452
23346
  </tr>`;
22453
23347
  if (run.log) {
22454
23348
  logsHtml.push(`<div id="log-${run.id}" style="display:none" class="mt8">
22455
- <div class="panel"><div class="ph"><h3>Run #${run.id} \u65E5\u5FD7 \u2014 ${run.taskName}</h3></div>
22456
- <div class="log-box">${run.log.replace(/</g, "&lt;")}</div></div></div>`);
23349
+ <div class="panel"><div class="ph"><h3>Run #${run.id} \u65E5\u5FD7 \u2014 ${esc(run.taskName)}</h3></div>
23350
+ <div class="log-box">${esc(run.log)}</div></div></div>`);
22457
23351
  }
22458
23352
  }
22459
23353
  let paging = `<div class="pn">`;
@@ -22479,17 +23373,18 @@ var init_web = __esm({
22479
23373
  });
22480
23374
  app.get("/system", async (c) => {
22481
23375
  const config = loadConfig();
23376
+ const configPath = getConfigPath();
22482
23377
  const stats = await TaskService.stats({});
22483
23378
  const runningRuns = await TaskRunService.getAllRunningRuns();
22484
23379
  const templates = await TaskTemplateService.list(100);
22485
- const configExists = existsSync3(CONFIG_PATH);
23380
+ const configExists = existsSync4(configPath);
22486
23381
  let runRows = "";
22487
23382
  if (runningRuns.length > 0) {
22488
23383
  for (const run of runningRuns) {
22489
23384
  const shortS = run.sessionId ? run.sessionId.slice(4, 7) + "***" + run.sessionId.slice(-3) : "-";
22490
23385
  runRows += `<tr>
22491
23386
  <td class="mu">#${run.id}</td><td>#${run.taskId}</td>
22492
- <td class="m sm">${shortS}</td>
23387
+ <td class="m sm">${esc(shortS)}</td>
22493
23388
  <td class="sm">${esc(run.model) || "-"}</td>
22494
23389
  <td class="sm">${formatDate(run.startedAt)}</td>
22495
23390
  <td class="sm">${run.heartbeatAt ? timeAgo(run.heartbeatAt) : "-"}</td>
@@ -22514,19 +23409,14 @@ var init_web = __esm({
22514
23409
  <h3 style="margin:0 0 12px;font-size:14px">Scheduler \u914D\u7F6E</h3>
22515
23410
  <div class="form-row"><label>\u542F\u7528\u8C03\u5EA6</label><input type="checkbox" name="se" ${config.scheduler.enabled ? "checked" : ""}></div>
22516
23411
  <div class="form-row"><label>\u68C0\u67E5\u95F4\u9694(ms)</label><input type="number" name="si" value="${config.scheduler.checkIntervalMs}" min="100" style="width:100px"></div>
22517
- <div class="form-row"><label>\u8FFD\u8D76\u6A21\u5F0F</label><select name="cu" style="width:100px">
22518
- <option value="next" ${config.scheduler.catchUp === "next" ? "selected" : ""}>next</option>
22519
- <option value="all" ${config.scheduler.catchUp === "all" ? "selected" : ""}>all</option>
22520
- <option value="latest" ${config.scheduler.catchUp === "latest" ? "selected" : ""}>latest</option>
22521
- </select></div>
22522
23412
  <div class="ir"><span class="ik">\u6D3B\u8DC3\u6A21\u677F</span><span class="iv">${templates.filter((t) => t.enabled).length} / ${templates.length}</span></div>
22523
23413
  </div>
22524
23414
  <div class="card">
22525
23415
  <h3 style="margin:0 0 12px;font-size:14px">Watchdog \u914D\u7F6E</h3>
22526
23416
  <div class="form-row"><label>\u5FC3\u8DF3\u8D85\u65F6(\u79D2)</label><input type="number" name="wt" value="${config.watchdog.heartbeatTimeoutMs / 1e3}" min="10" style="width:100px"></div>
22527
- <div class="form-row"><label>\u6E05\u7406\u95F4\u9694(\u79D2)</label><input type="number" name="wc" value="${config.watchdog.cleanupIntervalMs / 1e3}" min="10" style="width:100px"></div>
23417
+ <div class="form-row"><label>\u68C0\u67E5\u95F4\u9694(\u79D2)</label><input type="number" name="wci" value="${config.watchdog.checkIntervalMs / 1e3}" min="1" style="width:100px"></div>
23418
+ <div class="form-row"><label>\u6E05\u7406\u95F4\u9694(\u5C0F\u65F6)</label><input type="number" name="wcl" value="${config.watchdog.cleanupIntervalMs / 36e5}" min="1" style="width:100px"></div>
22528
23419
  <div class="form-row"><label>\u6570\u636E\u4FDD\u7559(\u5929)</label><input type="number" name="rd" value="${config.watchdog.retentionDays}" min="1" style="width:100px"></div>
22529
- <div class="ir"><span class="ik">\u65E5\u5FD7\u683C\u5F0F</span><span class="iv">${config.logging.format}</span></div>
22530
23420
  </div>
22531
23421
  </div>
22532
23422
  <div style="text-align:center;margin-bottom:24px">
@@ -22555,7 +23445,7 @@ var init_web = __esm({
22555
23445
 
22556
23446
  <div class="card mt16">
22557
23447
  <h3 style="margin:0 0 12px;font-size:14px">\u914D\u7F6E\u6587\u4EF6</h3>
22558
- <div class="ir"><span class="ik">\u8DEF\u5F84</span><span class="iv m sm">${CONFIG_PATH}</span></div>
23448
+ <div class="ir"><span class="ik">\u8DEF\u5F84</span><span class="iv m sm">${esc(configPath)}</span></div>
22559
23449
  <div class="ir"><span class="ik">\u6587\u4EF6\u5B58\u5728</span><span class="iv">${configFileStatus}</span></div>
22560
23450
  </div>
22561
23451
 
@@ -22567,46 +23457,61 @@ var init_web = __esm({
22567
23457
  return c.html(renderLayout("\u7CFB\u7EDF\u72B6\u6001", "system", body));
22568
23458
  });
22569
23459
  app.get("/api/tasks/:id", async (c) => {
22570
- const id = Number(c.req.param("id"));
23460
+ const id = parsePositiveInteger(c.req.param("id"));
23461
+ if (id === null) return c.json({ error: "invalid id" }, 400);
22571
23462
  const task = await TaskService.getById(id);
22572
23463
  if (!task) return c.json({ error: "not found" }, 404);
22573
23464
  const runs = await TaskRunService.listByTaskId(id);
22574
23465
  return c.json({ ...task, _runs: runs });
22575
23466
  });
22576
23467
  app.get("/api/runs/:id", async (c) => {
22577
- const id = Number(c.req.param("id"));
23468
+ const id = parsePositiveInteger(c.req.param("id"));
23469
+ if (id === null) return c.json({ error: "invalid id" }, 400);
22578
23470
  const run = await TaskRunService.getById(id);
22579
23471
  if (!run) return c.json({ error: "not found" }, 404);
22580
23472
  return c.json(run);
22581
23473
  });
22582
23474
  app.get("/api/templates/:id", async (c) => {
22583
- const id = Number(c.req.param("id"));
23475
+ const id = parsePositiveInteger(c.req.param("id"));
23476
+ if (id === null) return c.json({ error: "invalid id" }, 400);
22584
23477
  const tmpl = await TaskTemplateService.getById(id);
22585
23478
  if (!tmpl) return c.json({ error: "not found" }, 404);
22586
23479
  return c.json(tmpl);
22587
23480
  });
22588
23481
  app.post("/api/tasks/:id/retry", async (c) => {
22589
- await TaskService.retry(Number(c.req.param("id")));
22590
- return c.json({ success: true });
23482
+ const id = parsePositiveInteger(c.req.param("id"));
23483
+ if (id === null) return c.json({ error: "invalid id" }, 400);
23484
+ const task = await TaskService.retry(id);
23485
+ if (task) return c.json({ success: true });
23486
+ return await TaskService.getById(id) ? c.json({ error: "task status does not allow retry" }, 409) : c.json({ error: "not found" }, 404);
22591
23487
  });
22592
23488
  app.delete("/api/tasks/:id", async (c) => {
22593
- await TaskService.delete(Number(c.req.param("id")));
22594
- return c.json({ success: true });
23489
+ const id = parsePositiveInteger(c.req.param("id"));
23490
+ if (id === null) return c.json({ error: "invalid id" }, 400);
23491
+ const deleted = await TaskService.delete(id);
23492
+ return deleted ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
22595
23493
  });
22596
23494
  app.post("/api/templates/:id/enable", async (c) => {
22597
- const result = await TaskTemplateService.enable(Number(c.req.param("id")));
22598
- return c.json({ success: !!result });
23495
+ const id = parsePositiveInteger(c.req.param("id"));
23496
+ if (id === null) return c.json({ error: "invalid id" }, 400);
23497
+ const result = await TaskTemplateService.enable(id);
23498
+ return result ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
22599
23499
  });
22600
23500
  app.post("/api/templates/:id/disable", async (c) => {
22601
- const result = await TaskTemplateService.disable(Number(c.req.param("id")));
22602
- return c.json({ success: !!result });
23501
+ const id = parsePositiveInteger(c.req.param("id"));
23502
+ if (id === null) return c.json({ error: "invalid id" }, 400);
23503
+ const result = await TaskTemplateService.disable(id);
23504
+ return result ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
22603
23505
  });
22604
23506
  app.delete("/api/templates/:id", async (c) => {
22605
- const ok = await TaskTemplateService.delete(Number(c.req.param("id")));
22606
- return c.json({ success: ok });
23507
+ const id = parsePositiveInteger(c.req.param("id"));
23508
+ if (id === null) return c.json({ error: "invalid id" }, 400);
23509
+ const ok = await TaskTemplateService.delete(id);
23510
+ return ok ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
22607
23511
  });
22608
23512
  app.post("/api/templates/:id/trigger", async (c) => {
22609
- const id = Number(c.req.param("id"));
23513
+ const id = parsePositiveInteger(c.req.param("id"));
23514
+ if (id === null) return c.json({ error: "invalid id" }, 400);
22610
23515
  const tmpl = await TaskTemplateService.getById(id);
22611
23516
  if (!tmpl) return c.json({ error: "not found" }, 404);
22612
23517
  const task = await TaskService.add({
@@ -22618,7 +23523,10 @@ var init_web = __esm({
22618
23523
  category: tmpl.category,
22619
23524
  importance: tmpl.importance,
22620
23525
  urgency: tmpl.urgency,
23526
+ batchId: tmpl.batchId,
22621
23527
  maxRetries: tmpl.maxRetries,
23528
+ retryBackoffMs: tmpl.retryBackoffMs,
23529
+ timeoutMs: tmpl.timeoutMs,
22622
23530
  templateId: tmpl.id
22623
23531
  });
22624
23532
  return c.json({ success: true, taskId: task.id });
@@ -22633,22 +23541,29 @@ var init_web = __esm({
22633
23541
  const bW = body.worker ?? {};
22634
23542
  const bS = body.scheduler ?? {};
22635
23543
  const bD = body.watchdog ?? {};
22636
- const merged = { ...current, ...body, worker: { ...curW, ...bW }, scheduler: { ...curS, ...bS }, watchdog: { ...curD, ...bD } };
22637
- writeConfig(merged);
23544
+ const merged = {
23545
+ ...current,
23546
+ ...body,
23547
+ configVersion: 2,
23548
+ worker: { ...curW, ...bW },
23549
+ scheduler: { ...curS, ...bS },
23550
+ watchdog: { ...curD, ...bD }
23551
+ };
23552
+ writeConfig(validateConfig(merged));
22638
23553
  return c.json({ success: true });
22639
23554
  } catch (err) {
22640
- return c.json({ success: false, error: err instanceof Error ? err.message : String(err) });
23555
+ return c.json({ success: false, error: err instanceof Error ? err.message : String(err) }, 400);
22641
23556
  }
22642
23557
  });
22643
23558
  app.post("/api/database/clear", async (c) => {
22644
23559
  try {
22645
- const { tasks: tasks3, taskRuns: taskRuns3, taskTemplates: taskTemplates4 } = schema_exports;
22646
- await db.delete(taskRuns3);
23560
+ const { tasks: tasks3, taskRuns: taskRuns4, taskTemplates: taskTemplates4 } = schema_exports;
23561
+ await db.delete(taskRuns4);
22647
23562
  await db.delete(taskTemplates4);
22648
23563
  await db.delete(tasks3);
22649
23564
  return c.json({ success: true });
22650
23565
  } catch (err) {
22651
- return c.json({ success: false, error: err instanceof Error ? err.message : String(err) });
23566
+ return c.json({ success: false, error: err instanceof Error ? err.message : String(err) }, 500);
22652
23567
  }
22653
23568
  });
22654
23569
  dashboardApp = app;
@@ -22662,7 +23577,9 @@ var init_web = __esm({
22662
23577
  // src/gateway/index.ts
22663
23578
  var gateway_exports = {};
22664
23579
  __export(gateway_exports, {
22665
- main: () => main
23580
+ acquireLock: () => acquireLock,
23581
+ main: () => main,
23582
+ releaseLock: () => releaseLock
22666
23583
  });
22667
23584
  function acquireLock() {
22668
23585
  const now = Date.now();
@@ -22671,7 +23588,8 @@ function acquireLock() {
22671
23588
  sqlite.exec("BEGIN IMMEDIATE");
22672
23589
  const existing = sqlite.prepare("SELECT id, pid, heartbeat_at FROM gateway_lock WHERE id = 1").get();
22673
23590
  if (existing) {
22674
- if (now - existing.heartbeat_at < STALE_THRESHOLD_MS) {
23591
+ const lockHolderAlive = existing.pid !== pid && isProcessAlive(existing.pid);
23592
+ if (now - existing.heartbeat_at < STALE_THRESHOLD_MS && lockHolderAlive) {
22675
23593
  sqlite.exec("ROLLBACK");
22676
23594
  console.error(JSON.stringify({
22677
23595
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -22684,7 +23602,7 @@ function acquireLock() {
22684
23602
  sqlite.exec("DELETE FROM gateway_lock WHERE id = 1");
22685
23603
  }
22686
23604
  sqlite.exec(
22687
- "INSERT INTO gateway_lock (id, pid, acquired_at, heartbeat_at) VALUES (1, ?, ?, ?)",
23605
+ "INSERT INTO gateway_lock (id, pid, acquired_at, heartbeat_at, ready_at) VALUES (1, ?, ?, ?, NULL)",
22688
23606
  [pid, now, now]
22689
23607
  );
22690
23608
  sqlite.exec("COMMIT");
@@ -22718,6 +23636,18 @@ function updateLockHeartbeat() {
22718
23636
  } catch {
22719
23637
  }
22720
23638
  }
23639
+ function markGatewayReady() {
23640
+ sqlite.exec(
23641
+ "UPDATE gateway_lock SET heartbeat_at = ?, ready_at = ? WHERE pid = ?",
23642
+ [Date.now(), Date.now(), process.pid]
23643
+ );
23644
+ }
23645
+ function markGatewayNotReady() {
23646
+ try {
23647
+ sqlite.exec("UPDATE gateway_lock SET ready_at = NULL WHERE pid = ?", [process.pid]);
23648
+ } catch {
23649
+ }
23650
+ }
22721
23651
  async function main() {
22722
23652
  console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: "SuperTask Gateway starting", pid: process.pid }));
22723
23653
  if (!acquireLock()) {
@@ -22729,6 +23659,21 @@ async function main() {
22729
23659
  const worker = new WorkerEngine(cfg);
22730
23660
  const watchdog = new Watchdog(cfg);
22731
23661
  const scheduler = new Scheduler(cfg);
23662
+ const recoveredOrphans = await TaskService.resetOrphanRunningToPending();
23663
+ if (recoveredOrphans > 0) {
23664
+ console.log(JSON.stringify({
23665
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
23666
+ level: "warn",
23667
+ msg: "reset orphan running tasks to pending",
23668
+ count: recoveredOrphans
23669
+ }));
23670
+ }
23671
+ initializeGatewayHealth({
23672
+ workerPollIntervalMs: cfg.worker.pollIntervalMs,
23673
+ schedulerEnabled: cfg.scheduler.enabled,
23674
+ schedulerCheckIntervalMs: cfg.scheduler.checkIntervalMs,
23675
+ watchdogCheckIntervalMs: cfg.watchdog.checkIntervalMs
23676
+ });
22732
23677
  worker.start();
22733
23678
  watchdog.start();
22734
23679
  await scheduler.start();
@@ -22746,6 +23691,7 @@ async function main() {
22746
23691
  url: `http://localhost:${cfg.dashboard.port}`
22747
23692
  }));
22748
23693
  }
23694
+ markGatewayReady();
22749
23695
  console.log(JSON.stringify({
22750
23696
  ts: (/* @__PURE__ */ new Date()).toISOString(),
22751
23697
  level: "info",
@@ -22759,10 +23705,10 @@ async function main() {
22759
23705
  shuttingDown = true;
22760
23706
  console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: `received ${signal}, shutting down...` }));
22761
23707
  clearInterval(heartbeatTimer);
23708
+ markGatewayNotReady();
22762
23709
  scheduler.stop();
22763
23710
  watchdog.stop();
22764
- const runningIds = worker.getRunningTaskIds();
22765
- await worker.stop();
23711
+ const runningIds = await worker.stop(cfg.worker.shutdownGracePeriodMs);
22766
23712
  if (runningIds.length > 0) {
22767
23713
  const resetCount = await TaskService.resetRunningToPending(runningIds);
22768
23714
  console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: "reset running tasks to pending", count: resetCount }));
@@ -22772,6 +23718,7 @@ async function main() {
22772
23718
  await TaskRunService.fail(run.id, "Gateway shutdown");
22773
23719
  }
22774
23720
  releaseLock();
23721
+ resetGatewayHealth();
22775
23722
  closeDb();
22776
23723
  console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: "Gateway stopped" }));
22777
23724
  process.exit(0);
@@ -22799,6 +23746,8 @@ var init_gateway = __esm({
22799
23746
  init_db2();
22800
23747
  init_task_service();
22801
23748
  init_task_run_service();
23749
+ init_health();
23750
+ init_process_control();
22802
23751
  STALE_THRESHOLD_MS = 3e4;
22803
23752
  if (import.meta.main) {
22804
23753
  main();
@@ -22806,247 +23755,87 @@ var init_gateway = __esm({
22806
23755
  }
22807
23756
  });
22808
23757
 
22809
- // src/daemon/pm2.ts
22810
- var pm2_exports = {};
22811
- __export(pm2_exports, {
22812
- ensureGateway: () => ensureGateway,
22813
- install: () => install,
22814
- isGatewayRunning: () => isGatewayRunning,
22815
- uninstall: () => uninstall,
22816
- upgrade: () => upgrade
23758
+ // src/daemon/update.ts
23759
+ var update_exports = {};
23760
+ __export(update_exports, {
23761
+ installLatestPlugin: () => installLatestPlugin,
23762
+ resolveInstalledPlugin: () => resolveInstalledPlugin
22817
23763
  });
22818
- import { execSync, spawnSync } from "child_process";
22819
- import { join as join3, dirname as dirname3 } from "path";
22820
- import { fileURLToPath as fileURLToPath2 } from "url";
22821
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync4 } from "fs";
22822
- import { homedir as homedir3 } from "os";
22823
- function getPackageVersion() {
22824
- try {
22825
- const pkgPath = join3(__dirname, "../package.json");
22826
- const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
22827
- return pkg.version || "0.0.0";
22828
- } catch {
22829
- return "0.0.0";
22830
- }
22831
- }
22832
- function getRunningVersion() {
23764
+ import { spawnSync as spawnSync3 } from "child_process";
23765
+ import { existsSync as existsSync5, readFileSync as readFileSync4, readdirSync } from "fs";
23766
+ import { homedir as homedir4 } from "os";
23767
+ import { join as join4 } from "path";
23768
+ function pluginAt(packageDir) {
23769
+ const packageJson = join4(packageDir, "package.json");
23770
+ const gatewayEntry = join4(packageDir, "dist/gateway/index.js");
23771
+ if (!existsSync5(packageJson) || !existsSync5(gatewayEntry)) return null;
22833
23772
  try {
22834
- if (!existsSync4(VERSION_FILE)) return null;
22835
- return readFileSync3(VERSION_FILE, "utf-8").trim() || null;
23773
+ const pkg = JSON.parse(readFileSync4(packageJson, "utf8"));
23774
+ if (pkg.name !== PACKAGE_NAME || typeof pkg.version !== "string") return null;
23775
+ return { packageDir, gatewayEntry, version: pkg.version };
22836
23776
  } catch {
22837
23777
  return null;
22838
23778
  }
22839
23779
  }
22840
- function writeRunningVersion(version2) {
22841
- try {
22842
- writeFileSync2(VERSION_FILE, version2, "utf-8");
22843
- } catch {
22844
- }
22845
- }
22846
- function pm2Bin() {
22847
- return process.platform === "win32" ? "pm2.cmd" : "pm2";
22848
- }
22849
- function isPm2Installed() {
22850
- try {
22851
- const cmd = process.platform === "win32" ? "where pm2" : "which pm2";
22852
- execSync(cmd, { stdio: "pipe" });
22853
- return true;
22854
- } catch {
22855
- return false;
22856
- }
22857
- }
22858
- function installPm2() {
22859
- console.log("[supertask] Installing pm2...");
22860
- try {
22861
- execSync("npm install -g pm2", { stdio: "inherit" });
22862
- return true;
22863
- } catch {
22864
- try {
22865
- execSync("bun install -g pm2", { stdio: "inherit" });
22866
- return true;
22867
- } catch {
22868
- return false;
22869
- }
22870
- }
22871
- }
22872
- function pm2Exec(args) {
22873
- const bin = pm2Bin();
22874
- try {
22875
- const result = spawnSync(bin, args, {
22876
- stdio: ["pipe", "pipe", "pipe"],
22877
- encoding: "utf-8",
22878
- shell: process.platform === "win32"
22879
- });
22880
- const output = (result.stdout ?? "") + (result.stderr ?? "");
22881
- return { ok: result.status === 0, output };
22882
- } catch (err) {
22883
- return { ok: false, output: err instanceof Error ? err.message : String(err) };
22884
- }
22885
- }
22886
- function pm2JsonList() {
22887
- const { ok, output } = pm2Exec(["jlist"]);
22888
- if (!ok) return [];
22889
- try {
22890
- return JSON.parse(output);
22891
- } catch {
22892
- return [];
22893
- }
22894
- }
22895
- function isGatewayRunning() {
22896
- const list = pm2JsonList();
22897
- const proc = list.find((p) => p.name === PROCESS_NAME);
22898
- if (!proc) return false;
22899
- return proc.pm2_env?.status === "online";
23780
+ function versionParts(version2) {
23781
+ const match2 = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(version2);
23782
+ return match2 ? match2.slice(1).map(Number) : null;
22900
23783
  }
22901
- function findBunPath() {
22902
- try {
22903
- const cmd = process.platform === "win32" ? "where bun" : "which bun";
22904
- return execSync(cmd, { stdio: "pipe" }).toString().trim().split("\n")[0];
22905
- } catch {
22906
- return process.execPath;
23784
+ function compareVersions(left, right) {
23785
+ const a = versionParts(left);
23786
+ const b = versionParts(right);
23787
+ if (!a || !b) return left.localeCompare(right);
23788
+ for (let index2 = 0; index2 < 3; index2 += 1) {
23789
+ if (a[index2] !== b[index2]) return a[index2] - b[index2];
22907
23790
  }
23791
+ return 0;
22908
23792
  }
22909
- function pm2StartGateway(version2) {
22910
- const bunPath = findBunPath();
22911
- return pm2Exec([
22912
- "start",
22913
- bunPath,
22914
- "--name",
22915
- PROCESS_NAME,
22916
- "--interpreter",
22917
- "none",
22918
- "--restart-delay",
22919
- "5000",
22920
- "--max-restarts",
22921
- "30",
22922
- "--",
22923
- GATEWAY_ENTRY
22924
- ]);
23793
+ function cacheRoot() {
23794
+ return process.env.SUPERTASK_OPENCODE_CACHE_DIR ?? join4(homedir4(), ".cache/opencode/packages");
22925
23795
  }
22926
- function install() {
22927
- if (!isPm2Installed()) {
22928
- if (!installPm2()) {
22929
- throw new Error("[supertask] Failed to install pm2. Please install it manually: npm install -g pm2");
22930
- }
23796
+ function resolveInstalledPlugin() {
23797
+ const override = process.env.SUPERTASK_PLUGIN_PACKAGE_DIR;
23798
+ if (override) {
23799
+ const plugin = pluginAt(override);
23800
+ if (!plugin) throw new Error(`[supertask] \u5B89\u88C5\u5305\u65E0\u6548\u6216\u7F3A\u5C11 Gateway \u6784\u5EFA\u4EA7\u7269: ${override}`);
23801
+ return plugin;
22931
23802
  }
22932
- console.log("[supertask] pm2 ready");
22933
- const list = pm2JsonList();
22934
- const existing = list.find((p) => p.name === PROCESS_NAME);
22935
- if (existing) {
22936
- console.log("[supertask] Gateway process already registered, reloading...");
22937
- const { ok } = pm2Exec(["reload", PROCESS_NAME]);
22938
- if (!ok) {
22939
- console.error("[supertask] pm2 reload failed, trying restart...");
22940
- pm2Exec(["restart", PROCESS_NAME]);
22941
- }
22942
- } else {
22943
- console.log("[supertask] Starting Gateway with pm2...");
22944
- const version2 = getPackageVersion();
22945
- const { ok, output } = pm2StartGateway(version2);
22946
- if (!ok) {
22947
- throw new Error(`[supertask] pm2 start failed: ${output}`);
22948
- }
22949
- writeRunningVersion(version2);
22950
- }
22951
- pm2Exec(["save"]);
22952
- console.log("[supertask] Configuring startup...");
22953
- const { ok: startupOk, output: startupOutput } = pm2Exec(["startup"]);
22954
- if (!startupOk) {
22955
- if (startupOutput.includes("sudo") || startupOutput.includes("run as root")) {
22956
- console.log("[supertask] pm2 startup requires elevated permissions.");
22957
- console.log("[supertask] Run the command shown above, or manually execute:");
22958
- console.log(` pm2 startup`);
22959
- console.log(` pm2 save`);
22960
- } else if (process.platform === "win32") {
22961
- console.log("[supertask] On Windows, use pm2-installer for startup:");
22962
- console.log(" npm install -g pm2-windows-startup");
22963
- console.log(" pm2-startup install");
22964
- }
23803
+ const root = cacheRoot();
23804
+ const packageDirs = existsSync5(root) ? readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && (entry.name === PACKAGE_NAME || entry.name.startsWith(`${PACKAGE_NAME}@`))).map((entry) => join4(root, entry.name, "node_modules", PACKAGE_NAME)) : [];
23805
+ const installed = packageDirs.map(pluginAt).filter((plugin) => plugin !== null).sort((left, right) => compareVersions(right.version, left.version));
23806
+ if (!installed[0]) {
23807
+ throw new Error(`[supertask] OpenCode \u63D2\u4EF6\u7F13\u5B58\u4E2D\u627E\u4E0D\u5230\u53EF\u8FD0\u884C\u7684 ${PACKAGE_NAME}`);
22965
23808
  }
22966
- console.log("\n[supertask] Gateway installed and running!");
22967
- console.log("[supertask] Manage with: pm2 status / pm2 logs supertask-gateway");
23809
+ return installed[0];
22968
23810
  }
22969
- function uninstall() {
22970
- console.log("[supertask] Stopping Gateway...");
22971
- pm2Exec(["stop", PROCESS_NAME]);
22972
- console.log("[supertask] Removing Gateway from pm2...");
22973
- pm2Exec(["delete", PROCESS_NAME]);
22974
- pm2Exec(["save"]);
22975
- console.log("\n[supertask] Gateway removed from pm2.");
22976
- console.log("[supertask] Note: pm2 startup config was not removed (you may have other pm2 processes).");
22977
- console.log("[supertask] To fully remove pm2 startup: pm2 unstartup");
23811
+ function opencodeBin() {
23812
+ return process.env.SUPERTASK_OPENCODE_BIN ?? "opencode";
22978
23813
  }
22979
- function upgrade() {
22980
- const before = getRunningVersion();
22981
- const currentVersion = getPackageVersion();
22982
- const list = pm2JsonList();
22983
- const proc = list.find((p) => p.name === PROCESS_NAME);
22984
- if (proc) {
22985
- console.log(`[supertask] Stopping Gateway (version ${before ?? "unknown"})...`);
22986
- pm2Exec(["delete", PROCESS_NAME]);
22987
- }
22988
- console.log(`[supertask] Starting Gateway (version ${currentVersion})...`);
22989
- const { ok } = pm2StartGateway(currentVersion);
22990
- if (ok) {
22991
- writeRunningVersion(currentVersion);
22992
- pm2Exec(["save"]);
22993
- console.log(`[supertask] Gateway upgraded: ${before ?? "unknown"} \u2192 ${currentVersion}`);
22994
- } else {
22995
- throw new Error(`[supertask] Failed to start Gateway after upgrade`);
22996
- }
22997
- return { before, after: currentVersion, restarted: true };
22998
- }
22999
- function ensureGateway() {
23000
- const currentVersion = getPackageVersion();
23001
- try {
23002
- const list = pm2JsonList();
23003
- const proc = list.find((p) => p.name === PROCESS_NAME);
23004
- if (proc && proc.pm2_env?.status === "online") {
23005
- const runningVersion = getRunningVersion();
23006
- if (runningVersion === currentVersion) {
23007
- return;
23008
- }
23009
- console.log(`[supertask] Version changed: ${runningVersion ?? "unknown"} \u2192 ${currentVersion}, reloading Gateway...`);
23010
- pm2Exec(["delete", PROCESS_NAME]);
23011
- const { ok } = pm2StartGateway(currentVersion);
23012
- if (ok) writeRunningVersion(currentVersion);
23013
- pm2Exec(["save"]);
23014
- return;
23015
- }
23016
- } catch {
23017
- }
23018
- if (!isPm2Installed()) {
23019
- console.log("[supertask] Installing pm2 for Gateway process management...");
23020
- try {
23021
- execSync("npm install -g pm2", { stdio: "pipe" });
23022
- } catch {
23023
- try {
23024
- execSync("bun install -g pm2", { stdio: "pipe" });
23025
- } catch {
23026
- console.warn("[supertask] Could not install pm2. Gateway will not auto-start. Run `supertask install` manually.");
23027
- return;
23028
- }
23029
- }
23814
+ function installLatestPlugin() {
23815
+ const result = spawnSync3(opencodeBin(), [
23816
+ "plugin",
23817
+ `${PACKAGE_NAME}@latest`,
23818
+ "--global",
23819
+ "--force"
23820
+ ], {
23821
+ encoding: "utf8",
23822
+ env: process.env,
23823
+ timeout: 12e4
23824
+ });
23825
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
23826
+ if (result.error) {
23827
+ throw new Error(`[supertask] OpenCode \u63D2\u4EF6\u66F4\u65B0\u5931\u8D25: ${result.error.message}`);
23030
23828
  }
23031
- const pm2List = pm2JsonList();
23032
- const existing = pm2List.find((p) => p.name === PROCESS_NAME);
23033
- if (existing) {
23034
- pm2Exec(["restart", PROCESS_NAME]);
23035
- } else {
23036
- const version2 = getPackageVersion();
23037
- const { ok } = pm2StartGateway(version2);
23038
- if (ok) writeRunningVersion(version2);
23829
+ if (result.status !== 0) {
23830
+ throw new Error(`[supertask] OpenCode \u63D2\u4EF6\u66F4\u65B0\u5931\u8D25: ${output || `\u9000\u51FA\u7801 ${result.status}`}`);
23039
23831
  }
23040
- pm2Exec(["save"]);
23832
+ return resolveInstalledPlugin();
23041
23833
  }
23042
- var __dirname, GATEWAY_ENTRY, PROCESS_NAME, VERSION_FILE;
23043
- var init_pm2 = __esm({
23044
- "src/daemon/pm2.ts"() {
23834
+ var PACKAGE_NAME;
23835
+ var init_update2 = __esm({
23836
+ "src/daemon/update.ts"() {
23045
23837
  "use strict";
23046
- __dirname = dirname3(fileURLToPath2(import.meta.url));
23047
- GATEWAY_ENTRY = join3(__dirname, "../gateway/index.js");
23048
- PROCESS_NAME = "supertask-gateway";
23049
- VERSION_FILE = join3(homedir3(), ".local/share/opencode/supertask-gateway-version");
23838
+ PACKAGE_NAME = "opencode-supertask";
23050
23839
  }
23051
23840
  });
23052
23841
 
@@ -23102,6 +23891,7 @@ function parseDuration(input) {
23102
23891
  }
23103
23892
 
23104
23893
  // src/cli/index.ts
23894
+ init_pm2();
23105
23895
  async function withDb(fn) {
23106
23896
  try {
23107
23897
  return await fn();
@@ -23114,9 +23904,14 @@ async function withDb(fn) {
23114
23904
  }
23115
23905
  }
23116
23906
  var program2 = new Command();
23117
- program2.name("supertask").description("\u901A\u7528\u4EFB\u52A1\u7BA1\u7406\u7CFB\u7EDF - AI Agent \u4EFB\u52A1\u8C03\u5EA6\u5668").version("0.1.0");
23118
- program2.command("add").description("\u521B\u5EFA\u65B0\u4EFB\u52A1").requiredOption("-n, --name <name>", "\u4EFB\u52A1\u540D\u79F0").requiredOption("-a, --agent <agent>", "\u4E3B Agent \u540D\u79F0").requiredOption("-p, --prompt <prompt>", "\u63D0\u793A\u8BCD").option("-m, --model <model>", "\u6A21\u578B").option("-c, --category <category>", "\u5206\u7C7B", "general").option("-i, --importance <number>", "\u91CD\u8981\u7A0B\u5EA6 (1-5)", "3").option("-u, --urgency <number>", "\u7D27\u6025\u7A0B\u5EA6 (1-5)", "3").option("-b, --batch <batchId>", "\u6279\u6B21 ID").option("-d, --depends <taskId>", "\u4F9D\u8D56\u7684\u4EFB\u52A1 ID").option("-w, --cwd <path>", "(\u5DF2\u5E9F\u5F03) \u5DE5\u4F5C\u76EE\u5F55\u3002\u7CFB\u7EDF\u4F1A\u81EA\u52A8\u8BB0\u5F55\u63D0\u4EA4\u4EFB\u52A1\u65F6\u7684\u5F53\u524D\u76EE\u5F55").action(async (options) => withDb(async () => {
23907
+ program2.name("supertask").description("\u901A\u7528\u4EFB\u52A1\u7BA1\u7406\u7CFB\u7EDF - AI Agent \u4EFB\u52A1\u8C03\u5EA6\u5668").version(getPackageVersion());
23908
+ program2.command("add").description("\u521B\u5EFA\u65B0\u4EFB\u52A1").requiredOption("-n, --name <name>", "\u4EFB\u52A1\u540D\u79F0").requiredOption("-a, --agent <agent>", "\u4E3B Agent \u540D\u79F0").requiredOption("-p, --prompt <prompt>", "\u63D0\u793A\u8BCD").option("-m, --model <model>", "\u6A21\u578B").option("-c, --category <category>", "\u5206\u7C7B", "general").option("-i, --importance <number>", "\u91CD\u8981\u7A0B\u5EA6 (1-5)", "3").option("-u, --urgency <number>", "\u7D27\u6025\u7A0B\u5EA6 (1-5)", "3").option("-b, --batch <batchId>", "\u6279\u6B21 ID").option("-d, --depends <taskId>", "\u4F9D\u8D56\u7684\u4EFB\u52A1 ID").option("--max-retries <number>", "\u9996\u6B21\u6267\u884C\u4E4B\u5916\u5141\u8BB8\u7684\u91CD\u8BD5\u6B21\u6570", "3").option("--retry-backoff <duration>", "\u91CD\u8BD5\u9000\u907F\u57FA\u7840\u95F4\u9694\uFF0C\u5982 30s / 5min", "30s").option("--timeout <duration>", "\u4EFB\u52A1\u786C\u8D85\u65F6\uFF0C\u5982 30min / 2h").option("-w, --cwd <path>", "(\u5DF2\u5E9F\u5F03) \u5DE5\u4F5C\u76EE\u5F55\u3002\u7CFB\u7EDF\u4F1A\u81EA\u52A8\u8BB0\u5F55\u63D0\u4EA4\u4EFB\u52A1\u65F6\u7684\u5F53\u524D\u76EE\u5F55").action(async (options) => withDb(async () => {
23119
23909
  const submitCwd = process.cwd();
23910
+ const retryBackoffMs = parseDuration(options.retryBackoff);
23911
+ const timeoutMs = options.timeout ? parseDuration(options.timeout) : null;
23912
+ if (retryBackoffMs === null || options.timeout && timeoutMs === null) {
23913
+ throw new Error("retry-backoff \u6216 timeout \u683C\u5F0F\u65E0\u6548");
23914
+ }
23120
23915
  const task = await TaskService.add({
23121
23916
  name: options.name,
23122
23917
  agent: options.agent,
@@ -23127,7 +23922,10 @@ program2.command("add").description("\u521B\u5EFA\u65B0\u4EFB\u52A1").requiredOp
23127
23922
  urgency: parseInt(options.urgency),
23128
23923
  batchId: options.batch,
23129
23924
  dependsOn: options.depends ? parseInt(options.depends) : void 0,
23130
- cwd: submitCwd
23925
+ cwd: submitCwd,
23926
+ maxRetries: parseInt(options.maxRetries),
23927
+ retryBackoffMs,
23928
+ timeoutMs
23131
23929
  });
23132
23930
  console.log(JSON.stringify({ id: task.id, status: "created" }, null, 2));
23133
23931
  }));
@@ -23146,7 +23944,7 @@ program2.command("next").description("\u83B7\u53D6\u4E0B\u4E00\u4E2A\u5F85\u6267
23146
23944
  urgency: task.urgency
23147
23945
  }, null, 2));
23148
23946
  } else {
23149
- console.log(JSON.stringify({ id: null, message: "No pending tasks" }));
23947
+ console.log(JSON.stringify({ id: null, message: "No executable tasks" }));
23150
23948
  }
23151
23949
  }));
23152
23950
  program2.command("start").description("\u5F00\u59CB\u6267\u884C\u4EFB\u52A1\uFF08\u6807\u8BB0\u4E3A running\uFF09").requiredOption("--id <id>", "\u4EFB\u52A1 ID").action(async (options) => withDb(async () => {
@@ -23234,9 +24032,14 @@ program2.command("delete").description("\u5220\u9664\u4EFB\u52A1").requiredOptio
23234
24032
  console.log(JSON.stringify({ deleted, id: parseInt(options.id) }));
23235
24033
  }));
23236
24034
  program2.command("template").description("\u7BA1\u7406\u4EFB\u52A1\u8C03\u5EA6\u6A21\u677F").addCommand(
23237
- new Command("add").description("\u521B\u5EFA\u8C03\u5EA6\u6A21\u677F").requiredOption("-n, --name <name>", "\u6A21\u677F\u540D\u79F0").requiredOption("-a, --agent <agent>", "Agent \u540D\u79F0").requiredOption("-p, --prompt <prompt>", "\u63D0\u793A\u8BCD").requiredOption("-t, --type <type>", "\u8C03\u5EA6\u7C7B\u578B\uFF1Acron/delayed/recurring").option("--cron <expr>", "cron \u8868\u8FBE\u5F0F\uFF08cron \u7C7B\u578B\u5FC5\u586B\uFF09").option("--delay <duration>", "\u5EF6\u8FDF\u65F6\u95F4\uFF08delayed \u7C7B\u578B\u5FC5\u586B\uFF09\uFF0C\u5982 30s / 5min / 1h / 2d").option("--interval <duration>", "\u5FAA\u73AF\u95F4\u9694\uFF08recurring \u7C7B\u578B\u5FC5\u586B\uFF09\uFF0C\u5982 1h / 30min / 5s").option("-m, --model <model>", "\u6A21\u578B").option("-c, --category <category>", "\u5206\u7C7B", "general").option("-i, --importance <number>", "\u91CD\u8981\u7A0B\u5EA6 1-5", "3").option("-u, --urgency <number>", "\u7D27\u6025\u7A0B\u5EA6 1-5", "3").option("--max-instances <number>", "\u6700\u5927\u5E76\u53D1\u5B9E\u4F8B\u6570", "1").option("--max-retries <number>", "\u6700\u5927\u91CD\u8BD5\u6B21\u6570", "3").option("--retry-backoff <ms>", "\u9000\u907F\u57FA\u7840\u95F4\u9694 ms", "30000").action(async (options) => withDb(async () => {
24035
+ new Command("add").description("\u521B\u5EFA\u8C03\u5EA6\u6A21\u677F").requiredOption("-n, --name <name>", "\u6A21\u677F\u540D\u79F0").requiredOption("-a, --agent <agent>", "Agent \u540D\u79F0").requiredOption("-p, --prompt <prompt>", "\u63D0\u793A\u8BCD").requiredOption("-t, --type <type>", "\u8C03\u5EA6\u7C7B\u578B\uFF1Acron/delayed/recurring").option("--cron <expr>", "cron \u8868\u8FBE\u5F0F\uFF08cron \u7C7B\u578B\u5FC5\u586B\uFF09").option("--delay <duration>", "\u5EF6\u8FDF\u65F6\u95F4\uFF08delayed \u7C7B\u578B\u5FC5\u586B\uFF09\uFF0C\u5982 30s / 5min / 1h / 2d").option("--interval <duration>", "\u5FAA\u73AF\u95F4\u9694\uFF08recurring \u7C7B\u578B\u5FC5\u586B\uFF09\uFF0C\u5982 1h / 30min / 5s").option("-m, --model <model>", "\u6A21\u578B").option("-c, --category <category>", "\u5206\u7C7B", "general").option("-i, --importance <number>", "\u91CD\u8981\u7A0B\u5EA6 1-5", "3").option("-u, --urgency <number>", "\u7D27\u6025\u7A0B\u5EA6 1-5", "3").option("-b, --batch <batchId>", "\u6A21\u677F\u751F\u6210\u4EFB\u52A1\u7684\u6279\u6B21 ID").option("--max-instances <number>", "\u6700\u5927\u5E76\u53D1\u5B9E\u4F8B\u6570", "1").option("--max-retries <number>", "\u6700\u5927\u91CD\u8BD5\u6B21\u6570", "3").option("--retry-backoff <duration>", "\u9000\u907F\u57FA\u7840\u95F4\u9694\uFF0C\u5982 30s / 5min", "30s").option("--timeout <duration>", "\u6BCF\u6B21\u4EFB\u52A1\u786C\u8D85\u65F6\uFF0C\u5982 30min / 2h").action(async (options) => withDb(async () => {
23238
24036
  let intervalMs = null;
23239
24037
  let runAt = null;
24038
+ const retryBackoffMs = parseDuration(options.retryBackoff);
24039
+ const timeoutMs = options.timeout ? parseDuration(options.timeout) : null;
24040
+ if (retryBackoffMs === null || options.timeout && timeoutMs === null) {
24041
+ throw new Error("retry-backoff \u6216 timeout \u683C\u5F0F\u65E0\u6548");
24042
+ }
23240
24043
  if (options.interval) {
23241
24044
  intervalMs = parseDuration(options.interval);
23242
24045
  if (intervalMs === null) {
@@ -23260,13 +24063,16 @@ program2.command("template").description("\u7BA1\u7406\u4EFB\u52A1\u8C03\u5EA6\u
23260
24063
  category: options.category,
23261
24064
  importance: parseInt(options.importance),
23262
24065
  urgency: parseInt(options.urgency),
24066
+ cwd: process.cwd(),
24067
+ batchId: options.batch,
23263
24068
  scheduleType: options.type,
23264
24069
  cronExpr: options.cron,
23265
24070
  intervalMs,
23266
24071
  runAt,
23267
24072
  maxInstances: parseInt(options.maxInstances),
23268
24073
  maxRetries: parseInt(options.maxRetries),
23269
- retryBackoffMs: parseInt(options.retryBackoff)
24074
+ retryBackoffMs,
24075
+ timeoutMs
23270
24076
  });
23271
24077
  console.log(JSON.stringify({ id: tmpl.id, status: "created", nextRunAt: tmpl.nextRunAt }, null, 2));
23272
24078
  }))
@@ -23302,36 +24108,29 @@ program2.command("template").description("\u7BA1\u7406\u4EFB\u52A1\u8C03\u5EA6\u
23302
24108
  }))
23303
24109
  );
23304
24110
  program2.command("init").description("Initialize SuperTask (create config + run migrations)").action(async () => withDb(async () => {
23305
- const { existsSync: existsSync5, mkdirSync: mkdirSync3, writeFileSync: writeFileSync3 } = await import("fs");
23306
- const { homedir: homedir4 } = await import("os");
23307
- const { join: join4, dirname: dirname4 } = await import("path");
23308
- const { CONFIG_PATH: CONFIG_PATH2 } = await Promise.resolve().then(() => (init_config(), config_exports));
23309
- if (!existsSync5(CONFIG_PATH2)) {
23310
- const dir = dirname4(CONFIG_PATH2);
23311
- if (!existsSync5(dir)) mkdirSync3(dir, { recursive: true });
23312
- writeFileSync3(CONFIG_PATH2, JSON.stringify({
24111
+ const { existsSync: existsSync6, mkdirSync: mkdirSync4, writeFileSync: writeFileSync3 } = await import("fs");
24112
+ const { dirname: dirname4 } = await import("path");
24113
+ const { getConfigPath: getConfigPath2 } = await Promise.resolve().then(() => (init_config(), config_exports));
24114
+ const configPath = getConfigPath2();
24115
+ if (!existsSync6(configPath)) {
24116
+ const dir = dirname4(configPath);
24117
+ if (!existsSync6(dir)) mkdirSync4(dir, { recursive: true });
24118
+ writeFileSync3(configPath, JSON.stringify({
24119
+ configVersion: 2,
23313
24120
  worker: { maxConcurrency: 2 },
23314
24121
  scheduler: { enabled: true }
23315
24122
  }, null, 2) + "\n");
23316
- console.log(JSON.stringify({ created: CONFIG_PATH2 }));
24123
+ console.log(JSON.stringify({ created: configPath }));
23317
24124
  } else {
23318
- console.log(JSON.stringify({ exists: CONFIG_PATH2 }));
24125
+ console.log(JSON.stringify({ exists: configPath }));
23319
24126
  }
23320
24127
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db2(), db_exports));
23321
- const { migrate: migrate2 } = await Promise.resolve().then(() => (init_migrator2(), migrator_exports));
23322
- const { join: pJoin, dirname: pDirname } = await import("path");
23323
- const { fileURLToPath: fileURLToPath3 } = await import("url");
23324
- const __dirname2 = pDirname(fileURLToPath3(import.meta.url));
23325
- migrate2(getDb2(), { migrationsFolder: pJoin(__dirname2, "../../drizzle") });
24128
+ getDb2();
23326
24129
  console.log(JSON.stringify({ migrated: true }));
23327
24130
  }));
23328
24131
  program2.command("migrate").description("Run database migrations").action(async () => withDb(async () => {
23329
24132
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db2(), db_exports));
23330
- const { migrate: migrate2 } = await Promise.resolve().then(() => (init_migrator2(), migrator_exports));
23331
- const { join: join4, dirname: dirname4 } = await import("path");
23332
- const { fileURLToPath: fileURLToPath3 } = await import("url");
23333
- const __dirname2 = dirname4(fileURLToPath3(import.meta.url));
23334
- migrate2(getDb2(), { migrationsFolder: join4(__dirname2, "../../drizzle") });
24133
+ getDb2();
23335
24134
  console.log(JSON.stringify({ migrated: true }));
23336
24135
  }));
23337
24136
  program2.command("gateway").description("Start the Gateway process (foreground)").action(async () => {
@@ -23373,26 +24172,20 @@ program2.command("uninstall").description("Stop and remove Gateway pm2 service")
23373
24172
  process.exit(1);
23374
24173
  }
23375
24174
  });
23376
- program2.command("upgrade").description("Update npm package and restart Gateway").action(async () => {
23377
- const { execSync: execSync2 } = await import("child_process");
23378
- const { homedir: homedir4 } = await import("os");
23379
- const { join: join4 } = await import("path");
23380
- const configDir = join4(homedir4(), ".config/opencode");
24175
+ program2.command("upgrade").description("Update OpenCode plugin cache and restart Gateway").action(async () => {
23381
24176
  console.log("Updating opencode-supertask...");
24177
+ let installed;
23382
24178
  try {
23383
- execSync2("npm install opencode-supertask@latest", {
23384
- cwd: configDir,
23385
- stdio: "inherit",
23386
- timeout: 6e4
23387
- });
24179
+ const { installLatestPlugin: installLatestPlugin2 } = await Promise.resolve().then(() => (init_update2(), update_exports));
24180
+ installed = installLatestPlugin2();
23388
24181
  } catch (err) {
23389
- console.error("npm install failed:", err instanceof Error ? err.message : String(err));
23390
- console.error("Try manually: cd ~/.config/opencode && npm install opencode-supertask@latest");
24182
+ console.error(err instanceof Error ? err.message : String(err));
24183
+ console.error("Try manually: opencode plugin opencode-supertask@latest --global --force");
23391
24184
  process.exit(1);
23392
24185
  }
23393
24186
  try {
23394
24187
  const { upgrade: pm2Upgrade } = await Promise.resolve().then(() => (init_pm2(), pm2_exports));
23395
- const result = pm2Upgrade();
24188
+ const result = pm2Upgrade(installed);
23396
24189
  console.log(`
23397
24190
  SuperTask upgraded: ${result.before ?? "unknown"} \u2192 ${result.after}`);
23398
24191
  console.log("Gateway restarted. Please restart opencode to load the new plugin.");