opencode-supertask 0.1.19 → 0.1.21

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
 
@@ -9400,7 +9411,15 @@ import { join, dirname } from "path";
9400
9411
  import { fileURLToPath } from "url";
9401
9412
  function getMigrationsFolder() {
9402
9413
  const __dirname2 = dirname(fileURLToPath(import.meta.url));
9403
- return join(__dirname2, "../../../drizzle");
9414
+ let dir = __dirname2;
9415
+ for (let i = 0; i < 5; i++) {
9416
+ const candidate = join(dir, "drizzle", "meta", "_journal.json");
9417
+ if (existsSync(candidate)) {
9418
+ return join(dir, "drizzle");
9419
+ }
9420
+ dir = dirname(dir);
9421
+ }
9422
+ return join(__dirname2, "../../drizzle");
9404
9423
  }
9405
9424
  function initDb() {
9406
9425
  const dataDir = dirname(DB_FILE_PATH);
@@ -9415,16 +9434,30 @@ function initDb() {
9415
9434
  id INTEGER PRIMARY KEY CHECK (id = 1),
9416
9435
  pid INTEGER NOT NULL,
9417
9436
  acquired_at INTEGER NOT NULL,
9418
- heartbeat_at INTEGER NOT NULL
9437
+ heartbeat_at INTEGER NOT NULL,
9438
+ ready_at INTEGER
9419
9439
  );
9420
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
+ }
9421
9445
  _db = drizzle(_sqlite, { schema: schema_exports });
9422
9446
  if (!_migrationRan) {
9423
- _migrationRan = true;
9424
9447
  try {
9425
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;
9426
9455
  } catch (err) {
9427
9456
  const msg = err instanceof Error ? err.message : String(err);
9457
+ _migrationRan = false;
9458
+ _sqlite.close();
9459
+ _sqlite = null;
9460
+ _db = null;
9428
9461
  console.error(`[supertask] migration failed: ${msg}`);
9429
9462
  throw new Error(`[supertask] DB migration failed: ${msg}`);
9430
9463
  }
@@ -9444,6 +9477,7 @@ function closeDb() {
9444
9477
  _sqlite.close();
9445
9478
  _sqlite = null;
9446
9479
  _db = null;
9480
+ _migrationRan = false;
9447
9481
  }
9448
9482
  }
9449
9483
  var DEFAULT_DB_PATH, DB_FILE_PATH, _sqlite, _db, _migrationRan, db, sqlite;
@@ -9527,14 +9561,14 @@ var init_backoff = __esm({
9527
9561
  });
9528
9562
 
9529
9563
  // src/core/services/task.service.ts
9530
- var tasks2, TaskService;
9564
+ var tasks2, taskRuns2, TaskService;
9531
9565
  var init_task_service = __esm({
9532
9566
  "src/core/services/task.service.ts"() {
9533
9567
  "use strict";
9534
9568
  init_db2();
9535
9569
  init_drizzle_orm();
9536
9570
  init_backoff();
9537
- ({ tasks: tasks2 } = schema_exports);
9571
+ ({ tasks: tasks2, taskRuns: taskRuns2 } = schema_exports);
9538
9572
  TaskService = class {
9539
9573
  static buildScopeWhere(scope) {
9540
9574
  const conditions = [];
@@ -9544,9 +9578,27 @@ var init_task_service = __esm({
9544
9578
  return conditions;
9545
9579
  }
9546
9580
  static async add(data) {
9581
+ this.validateNewTask(data);
9547
9582
  const result = await db.insert(tasks2).values(data).returning();
9548
9583
  return result[0];
9549
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
+ }
9550
9602
  static async next(scope = {}) {
9551
9603
  const baseConditions = [...this.buildScopeWhere(scope)];
9552
9604
  const nowMs = Date.now();
@@ -9569,7 +9621,7 @@ var init_task_service = __esm({
9569
9621
  ),
9570
9622
  and(
9571
9623
  eq(tasks2.status, "failed"),
9572
- sql`${tasks2.retryCount} < ${tasks2.maxRetries}`,
9624
+ sql`${tasks2.retryCount} <= ${tasks2.maxRetries}`,
9573
9625
  retryAfterFilter
9574
9626
  )
9575
9627
  );
@@ -9583,7 +9635,8 @@ var init_task_service = __esm({
9583
9635
  const allTasks = await db.select().from(tasks2).where(and(...conditions)).orderBy(
9584
9636
  desc(tasks2.urgency),
9585
9637
  desc(tasks2.importance),
9586
- asc(tasks2.createdAt)
9638
+ asc(tasks2.createdAt),
9639
+ asc(tasks2.id)
9587
9640
  );
9588
9641
  for (const task of allTasks) {
9589
9642
  if (task.dependsOn) {
@@ -9604,7 +9657,7 @@ var init_task_service = __esm({
9604
9657
  eq(tasks2.status, "pending"),
9605
9658
  and(
9606
9659
  eq(tasks2.status, "failed"),
9607
- sql`${tasks2.retryCount} < ${tasks2.maxRetries}`
9660
+ sql`${tasks2.retryCount} <= ${tasks2.maxRetries}`
9608
9661
  )
9609
9662
  ),
9610
9663
  ...this.buildScopeWhere(scope)
@@ -9617,7 +9670,11 @@ var init_task_service = __esm({
9617
9670
  return result[0] || null;
9618
9671
  }
9619
9672
  static async done(id, log, scope = {}) {
9620
- 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
+ ];
9621
9678
  const result = await db.update(tasks2).set({
9622
9679
  status: "done",
9623
9680
  finishedAt: /* @__PURE__ */ new Date(),
@@ -9628,17 +9685,24 @@ var init_task_service = __esm({
9628
9685
  }
9629
9686
  static async fail(id, log, scope = {}, options) {
9630
9687
  const current = await this.getById(id, scope);
9631
- if (!current) return null;
9688
+ if (!current || current.status !== "running") return null;
9632
9689
  const newRetryCount = (current.retryCount ?? 0) + 1;
9633
9690
  const maxRetries = current.maxRetries ?? 3;
9634
- const isDeadLetter = options?.setDeadLetter ?? newRetryCount >= maxRetries;
9635
- 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
+ ];
9636
9697
  const result = await db.update(tasks2).set({
9637
9698
  status: isDeadLetter ? "dead_letter" : "failed",
9638
9699
  finishedAt: /* @__PURE__ */ new Date(),
9639
9700
  resultLog: log,
9640
9701
  retryCount: newRetryCount,
9641
- 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
+ )
9642
9706
  }).where(and(...conditions)).returning();
9643
9707
  return result[0] || null;
9644
9708
  }
@@ -9671,8 +9735,20 @@ var init_task_service = __esm({
9671
9735
  return result.length;
9672
9736
  }
9673
9737
  static async cancel(id, scope = {}) {
9674
- const conditions = [eq(tasks2.id, id), ...this.buildScopeWhere(scope)];
9675
- const result = await db.update(tasks2).set({ status: "cancelled" }).where(and(...conditions)).returning();
9738
+ const conditions = [
9739
+ eq(tasks2.id, id),
9740
+ or(
9741
+ eq(tasks2.status, "pending"),
9742
+ eq(tasks2.status, "running"),
9743
+ eq(tasks2.status, "failed")
9744
+ ),
9745
+ ...this.buildScopeWhere(scope)
9746
+ ];
9747
+ const result = await db.update(tasks2).set({
9748
+ status: "cancelled",
9749
+ finishedAt: /* @__PURE__ */ new Date(),
9750
+ retryAfter: null
9751
+ }).where(and(...conditions)).returning();
9676
9752
  return result[0] || null;
9677
9753
  }
9678
9754
  static async retry(id, scope = {}) {
@@ -9684,7 +9760,8 @@ var init_task_service = __esm({
9684
9760
  status: "pending",
9685
9761
  startedAt: null,
9686
9762
  finishedAt: null,
9687
- retryAfter: null
9763
+ retryAfter: null,
9764
+ retryCount: 0
9688
9765
  }).where(and(...conditions)).returning();
9689
9766
  return result[0] || null;
9690
9767
  }
@@ -9698,7 +9775,8 @@ var init_task_service = __esm({
9698
9775
  status: "pending",
9699
9776
  startedAt: null,
9700
9777
  finishedAt: null,
9701
- retryAfter: null
9778
+ retryAfter: null,
9779
+ retryCount: 0
9702
9780
  }).where(and(...conditions)).returning();
9703
9781
  return result.length;
9704
9782
  }
@@ -9766,6 +9844,9 @@ var init_task_service = __esm({
9766
9844
  }
9767
9845
  static async delete(id, scope = {}) {
9768
9846
  const conditions = [eq(tasks2.id, id), ...this.buildScopeWhere(scope)];
9847
+ const existing = await db.select({ id: tasks2.id }).from(tasks2).where(and(...conditions)).limit(1);
9848
+ if (!existing[0]) return false;
9849
+ await db.delete(taskRuns2).where(eq(taskRuns2.taskId, id));
9769
9850
  const result = await db.delete(tasks2).where(and(...conditions)).returning();
9770
9851
  return result.length > 0;
9771
9852
  }
@@ -9984,7 +10065,7 @@ var require_CronField = __commonJS({
9984
10065
  if (!isValidRange) {
9985
10066
  throw new Error(`${this.constructor.name} Validation error, got value ${badValue} expected range ${this.min}-${this.max}${charsString}`);
9986
10067
  }
9987
- const duplicate = this.#values.find((value, index) => this.#values.indexOf(value) !== index);
10068
+ const duplicate = this.#values.find((value, index2) => this.#values.indexOf(value) !== index2);
9988
10069
  if (duplicate) {
9989
10070
  throw new Error(`${this.constructor.name} Validation error, duplicate values found: ${duplicate}`);
9990
10071
  }
@@ -17829,11 +17910,11 @@ var require_CronFieldCollection = __commonJS({
17829
17910
  throw new Error("Unexpected range end");
17830
17911
  }
17831
17912
  if (step * multiplier > range.end) {
17832
- const mapFn = (_, index) => {
17913
+ const mapFn = (_, index2) => {
17833
17914
  if (typeof range.start !== "number") {
17834
17915
  throw new Error("Unexpected range start");
17835
17916
  }
17836
- return index % step === 0 ? range.start + index : null;
17917
+ return index2 % step === 0 ? range.start + index2 : null;
17837
17918
  };
17838
17919
  if (typeof range.start !== "number") {
17839
17920
  throw new Error("Unexpected range start");
@@ -18725,9 +18806,9 @@ var require_CronExpressionParser = __commonJS({
18725
18806
  if (field === CronUnit.DayOfWeek && max % 7 === 0) {
18726
18807
  stack.push(0);
18727
18808
  }
18728
- for (let index = min; index <= max; index += repeatInterval) {
18729
- if (stack.indexOf(index) === -1) {
18730
- stack.push(index);
18809
+ for (let index2 = min; index2 <= max; index2 += repeatInterval) {
18810
+ if (stack.indexOf(index2) === -1) {
18811
+ stack.push(index2);
18731
18812
  }
18732
18813
  }
18733
18814
  return stack;
@@ -18850,8 +18931,8 @@ var require_CronFileParser = __commonJS({
18850
18931
  * @throws If file cannot be read
18851
18932
  */
18852
18933
  static parseFileSync(filePath) {
18853
- const { readFileSync: readFileSync4 } = __require("fs");
18854
- const data = readFileSync4(filePath, "utf8");
18934
+ const { readFileSync: readFileSync5 } = __require("fs");
18935
+ const data = readFileSync5(filePath, "utf8");
18855
18936
  return _CronFileParser.#parseContent(data);
18856
18937
  }
18857
18938
  /**
@@ -18950,11 +19031,6 @@ var require_dist = __commonJS({
18950
19031
  });
18951
19032
 
18952
19033
  // src/core/cron-parser.ts
18953
- var cron_parser_exports = {};
18954
- __export(cron_parser_exports, {
18955
- getNextCronRun: () => getNextCronRun,
18956
- isValidCronExpr: () => isValidCronExpr
18957
- });
18958
19034
  function getNextCronRun(expr, afterMs) {
18959
19035
  try {
18960
19036
  const fromDate = afterMs != null ? new Date(afterMs) : /* @__PURE__ */ new Date();
@@ -18992,6 +19068,7 @@ var init_task_template_service = __esm({
18992
19068
  ({ taskTemplates: taskTemplates2 } = schema_exports);
18993
19069
  TaskTemplateService = class {
18994
19070
  static async create(data) {
19071
+ this.validate(data);
18995
19072
  const now = Date.now();
18996
19073
  const result = await db.insert(taskTemplates2).values({ ...data, createdAt: now, updatedAt: now }).returning();
18997
19074
  const tmpl = result[0];
@@ -19007,8 +19084,38 @@ var init_task_template_service = __esm({
19007
19084
  }
19008
19085
  return tmpl;
19009
19086
  }
19087
+ static validate(data) {
19088
+ if (!data.name.trim()) throw new Error("name \u4E0D\u80FD\u4E3A\u7A7A");
19089
+ if (!data.agent.trim()) throw new Error("agent \u4E0D\u80FD\u4E3A\u7A7A");
19090
+ if (!data.prompt.trim()) throw new Error("prompt \u4E0D\u80FD\u4E3A\u7A7A");
19091
+ const scheduleType = data.scheduleType;
19092
+ if (!["cron", "delayed", "recurring"].includes(scheduleType)) {
19093
+ throw new Error("scheduleType \u5FC5\u987B\u662F cron\u3001delayed \u6216 recurring");
19094
+ }
19095
+ if (scheduleType === "cron" && (!data.cronExpr || !isValidCronExpr(data.cronExpr))) {
19096
+ throw new Error("cronExpr \u7F3A\u5931\u6216\u683C\u5F0F\u65E0\u6548");
19097
+ }
19098
+ if (scheduleType === "recurring" && (!Number.isInteger(data.intervalMs) || (data.intervalMs ?? 0) <= 0)) {
19099
+ throw new Error("intervalMs \u5FC5\u987B\u662F\u6B63\u6574\u6570");
19100
+ }
19101
+ if (scheduleType === "delayed" && (!Number.isInteger(data.runAt) || (data.runAt ?? 0) <= 0)) {
19102
+ throw new Error("runAt \u5FC5\u987B\u662F\u6B63\u6574\u6570\u65F6\u95F4\u6233");
19103
+ }
19104
+ this.validateInteger("importance", data.importance, 1, 5);
19105
+ this.validateInteger("urgency", data.urgency, 1, 5);
19106
+ this.validateInteger("maxInstances", data.maxInstances, 1, 1e3);
19107
+ this.validateInteger("maxRetries", data.maxRetries, 0, 1e3);
19108
+ this.validateInteger("retryBackoffMs", data.retryBackoffMs, 0, 864e5);
19109
+ this.validateInteger("timeoutMs", data.timeoutMs, 1e3, 6048e5);
19110
+ }
19111
+ static validateInteger(name, value, min, max) {
19112
+ if (value === void 0 || value === null) return;
19113
+ if (!Number.isInteger(value) || value < min || value > max) {
19114
+ throw new Error(`${name} \u5FC5\u987B\u662F ${min} \u5230 ${max} \u4E4B\u95F4\u7684\u6574\u6570`);
19115
+ }
19116
+ }
19010
19117
  static async list(limit = 50) {
19011
- return await db.select().from(taskTemplates2).orderBy(desc(taskTemplates2.createdAt)).limit(limit);
19118
+ return await db.select().from(taskTemplates2).orderBy(desc(taskTemplates2.createdAt), desc(taskTemplates2.id)).limit(limit);
19012
19119
  }
19013
19120
  static async getById(id) {
19014
19121
  const result = await db.select().from(taskTemplates2).where(eq(taskTemplates2.id, id));
@@ -19051,137 +19158,466 @@ var init_task_template_service = __esm({
19051
19158
  // src/gateway/config.ts
19052
19159
  var config_exports = {};
19053
19160
  __export(config_exports, {
19054
- CONFIG_PATH: () => CONFIG_PATH,
19055
- loadConfig: () => loadConfig
19161
+ DEFAULT_CONFIG: () => DEFAULT_CONFIG,
19162
+ DEFAULT_CONFIG_PATH: () => DEFAULT_CONFIG_PATH,
19163
+ getConfigPath: () => getConfigPath,
19164
+ loadConfig: () => loadConfig,
19165
+ validateConfig: () => validateConfig
19056
19166
  });
19057
- import { readFileSync, existsSync as existsSync2 } from "fs";
19167
+ import { existsSync as existsSync2, readFileSync } from "fs";
19058
19168
  import { homedir as homedir2 } from "os";
19059
19169
  import { join as join2 } from "path";
19060
- function deepMerge(base, override) {
19061
- const result = { ...base };
19062
- for (const key of Object.keys(override)) {
19063
- const val = override[key];
19064
- if (val !== null && typeof val === "object" && !Array.isArray(val) && key in result) {
19065
- const baseVal = result[key];
19066
- if (baseVal !== null && typeof baseVal === "object" && !Array.isArray(baseVal)) {
19067
- result[key] = deepMerge(baseVal, val);
19068
- continue;
19069
- }
19070
- }
19071
- if (val !== void 0) {
19072
- result[key] = val;
19073
- }
19170
+ function getConfigPath() {
19171
+ return process.env.SUPERTASK_CONFIG_PATH ?? DEFAULT_CONFIG_PATH;
19172
+ }
19173
+ function objectAt(value, path) {
19174
+ if (value === void 0) return {};
19175
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
19176
+ throw new Error(`${path} \u5FC5\u987B\u662F\u5BF9\u8C61`);
19074
19177
  }
19075
- return result;
19178
+ return value;
19076
19179
  }
19077
- function loadConfig() {
19078
- if (!existsSync2(CONFIG_PATH)) {
19079
- return DEFAULT_CONFIG;
19180
+ function integerAt(source, key, fallback, min, max, path) {
19181
+ const value = source[key];
19182
+ if (value === void 0) return fallback;
19183
+ if (typeof value !== "number" || !Number.isInteger(value) || value < min || value > max) {
19184
+ throw new Error(`${path}.${key} \u5FC5\u987B\u662F ${min} \u5230 ${max} \u4E4B\u95F4\u7684\u6574\u6570`);
19080
19185
  }
19186
+ return value;
19187
+ }
19188
+ function booleanAt(source, key, fallback, path) {
19189
+ const value = source[key];
19190
+ if (value === void 0) return fallback;
19191
+ if (typeof value !== "boolean") throw new Error(`${path}.${key} \u5FC5\u987B\u662F\u5E03\u5C14\u503C`);
19192
+ return value;
19193
+ }
19194
+ function validateConfig(input) {
19195
+ const root = objectAt(input, "config");
19196
+ const version2 = root.configVersion ?? 1;
19197
+ if (version2 !== 1 && version2 !== 2) {
19198
+ throw new Error("config.configVersion \u53EA\u652F\u6301 1 \u6216 2");
19199
+ }
19200
+ const worker = objectAt(root.worker, "worker");
19201
+ const scheduler = objectAt(root.scheduler, "scheduler");
19202
+ const watchdog = objectAt(root.watchdog, "watchdog");
19203
+ const dashboard = objectAt(root.dashboard, "dashboard");
19204
+ const heartbeatIntervalMs = integerAt(worker, "heartbeatIntervalMs", DEFAULT_CONFIG.worker.heartbeatIntervalMs, 1e3, 36e5, "worker");
19205
+ const heartbeatTimeoutMs = integerAt(watchdog, "heartbeatTimeoutMs", DEFAULT_CONFIG.watchdog.heartbeatTimeoutMs, 1e3, 864e5, "watchdog");
19206
+ let checkIntervalMs;
19207
+ let cleanupIntervalMs;
19208
+ if (version2 === 1 && watchdog.checkIntervalMs === void 0 && watchdog.cleanupIntervalMs !== void 0) {
19209
+ checkIntervalMs = integerAt(watchdog, "cleanupIntervalMs", DEFAULT_CONFIG.watchdog.checkIntervalMs, 1e3, 36e5, "watchdog");
19210
+ cleanupIntervalMs = DEFAULT_CONFIG.watchdog.cleanupIntervalMs;
19211
+ } else {
19212
+ checkIntervalMs = integerAt(watchdog, "checkIntervalMs", DEFAULT_CONFIG.watchdog.checkIntervalMs, 1e3, 36e5, "watchdog");
19213
+ cleanupIntervalMs = integerAt(watchdog, "cleanupIntervalMs", DEFAULT_CONFIG.watchdog.cleanupIntervalMs, 6e4, 6048e5, "watchdog");
19214
+ }
19215
+ if (heartbeatIntervalMs >= heartbeatTimeoutMs) {
19216
+ throw new Error("worker.heartbeatIntervalMs \u5FC5\u987B\u5C0F\u4E8E watchdog.heartbeatTimeoutMs");
19217
+ }
19218
+ if (checkIntervalMs > heartbeatTimeoutMs) {
19219
+ throw new Error("watchdog.checkIntervalMs \u4E0D\u80FD\u5927\u4E8E watchdog.heartbeatTimeoutMs");
19220
+ }
19221
+ return {
19222
+ configVersion: 2,
19223
+ worker: {
19224
+ maxConcurrency: integerAt(worker, "maxConcurrency", DEFAULT_CONFIG.worker.maxConcurrency, 1, 64, "worker"),
19225
+ pollIntervalMs: integerAt(worker, "pollIntervalMs", DEFAULT_CONFIG.worker.pollIntervalMs, 50, 6e4, "worker"),
19226
+ heartbeatIntervalMs,
19227
+ taskTimeoutMs: integerAt(worker, "taskTimeoutMs", DEFAULT_CONFIG.worker.taskTimeoutMs, 1e3, 6048e5, "worker"),
19228
+ shutdownGracePeriodMs: integerAt(worker, "shutdownGracePeriodMs", DEFAULT_CONFIG.worker.shutdownGracePeriodMs, 0, 36e5, "worker")
19229
+ },
19230
+ scheduler: {
19231
+ enabled: booleanAt(scheduler, "enabled", DEFAULT_CONFIG.scheduler.enabled, "scheduler"),
19232
+ checkIntervalMs: integerAt(scheduler, "checkIntervalMs", DEFAULT_CONFIG.scheduler.checkIntervalMs, 100, 6e4, "scheduler")
19233
+ },
19234
+ watchdog: {
19235
+ heartbeatTimeoutMs,
19236
+ checkIntervalMs,
19237
+ cleanupIntervalMs,
19238
+ retentionDays: integerAt(watchdog, "retentionDays", DEFAULT_CONFIG.watchdog.retentionDays, 1, 3650, "watchdog")
19239
+ },
19240
+ dashboard: {
19241
+ enabled: booleanAt(dashboard, "enabled", DEFAULT_CONFIG.dashboard.enabled, "dashboard"),
19242
+ port: integerAt(dashboard, "port", DEFAULT_CONFIG.dashboard.port, 1, 65535, "dashboard")
19243
+ }
19244
+ };
19245
+ }
19246
+ function loadConfig(path = getConfigPath()) {
19247
+ if (!existsSync2(path)) return validateConfig({ configVersion: 2 });
19081
19248
  try {
19082
- const raw2 = readFileSync(CONFIG_PATH, "utf-8");
19083
- const userConfig = JSON.parse(raw2);
19084
- return deepMerge(DEFAULT_CONFIG, userConfig);
19085
- } catch (err) {
19086
- const msg = err instanceof Error ? err.message : String(err);
19087
- console.error(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "warn", msg: "config load failed, using defaults", path: CONFIG_PATH, error: msg }));
19088
- return DEFAULT_CONFIG;
19249
+ return validateConfig(JSON.parse(readFileSync(path, "utf-8")));
19250
+ } catch (error) {
19251
+ const message = error instanceof Error ? error.message : String(error);
19252
+ throw new Error(`\u65E0\u6CD5\u8BFB\u53D6\u914D\u7F6E ${path}: ${message}`);
19089
19253
  }
19090
19254
  }
19091
- var DEFAULT_CONFIG, CONFIG_PATH;
19255
+ var DEFAULT_CONFIG, DEFAULT_CONFIG_PATH;
19092
19256
  var init_config = __esm({
19093
19257
  "src/gateway/config.ts"() {
19094
19258
  "use strict";
19095
19259
  DEFAULT_CONFIG = {
19260
+ configVersion: 2,
19096
19261
  worker: {
19097
19262
  maxConcurrency: 2,
19098
19263
  pollIntervalMs: 1e3,
19099
19264
  heartbeatIntervalMs: 3e4,
19100
- taskTimeoutMs: 18e5
19265
+ taskTimeoutMs: 18e5,
19266
+ shutdownGracePeriodMs: 3e4
19101
19267
  },
19102
19268
  scheduler: {
19103
19269
  enabled: true,
19104
- checkIntervalMs: 1e3,
19105
- catchUp: "next"
19270
+ checkIntervalMs: 1e3
19106
19271
  },
19107
19272
  watchdog: {
19108
19273
  heartbeatTimeoutMs: 6e5,
19109
- cleanupIntervalMs: 6e4,
19274
+ checkIntervalMs: 6e4,
19275
+ cleanupIntervalMs: 864e5,
19110
19276
  retentionDays: 30
19111
19277
  },
19112
19278
  dashboard: {
19113
19279
  enabled: true,
19114
19280
  port: 4680
19115
- },
19116
- logging: {
19117
- level: "info",
19118
- format: "json"
19119
19281
  }
19120
19282
  };
19121
- CONFIG_PATH = join2(homedir2(), ".config/opencode/supertask.json");
19283
+ DEFAULT_CONFIG_PATH = join2(homedir2(), ".config/opencode/supertask.json");
19284
+ }
19285
+ });
19286
+
19287
+ // src/daemon/pm2.ts
19288
+ var pm2_exports = {};
19289
+ __export(pm2_exports, {
19290
+ ensureGateway: () => ensureGateway,
19291
+ getPackageVersion: () => getPackageVersion,
19292
+ install: () => install,
19293
+ isGatewayReady: () => isGatewayReady,
19294
+ isGatewayRunning: () => isGatewayRunning,
19295
+ isPm2Installed: () => isPm2Installed,
19296
+ resolveGatewayEntry: () => resolveGatewayEntry,
19297
+ uninstall: () => uninstall,
19298
+ upgrade: () => upgrade
19299
+ });
19300
+ import { execSync, spawnSync } from "child_process";
19301
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
19302
+ import { homedir as homedir3 } from "os";
19303
+ import { dirname as dirname2, join as join3 } from "path";
19304
+ import { fileURLToPath as fileURLToPath2 } from "url";
19305
+ import { Database as Database3 } from "bun:sqlite";
19306
+ function getPackageVersion() {
19307
+ const envVersion = process.env.npm_package_version;
19308
+ if (envVersion) return envVersion;
19309
+ try {
19310
+ const pkgPath = join3(__dirname, "../../package.json");
19311
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
19312
+ return typeof pkg.version === "string" ? pkg.version : "0.0.0";
19313
+ } catch {
19314
+ return "0.0.0";
19315
+ }
19316
+ }
19317
+ function resolveGatewayEntry() {
19318
+ const override = process.env.SUPERTASK_GATEWAY_ENTRY;
19319
+ if (override) {
19320
+ if (!existsSync3(override)) throw new Error(`[supertask] Gateway entry not found: ${override}`);
19321
+ return override;
19322
+ }
19323
+ const candidates = [
19324
+ join3(__dirname, "../gateway/index.js"),
19325
+ join3(__dirname, "../gateway/index.ts")
19326
+ ];
19327
+ const entry = candidates.find((candidate) => existsSync3(candidate));
19328
+ if (!entry) throw new Error(`[supertask] Gateway entry not found. Checked: ${candidates.join(", ")}`);
19329
+ return entry;
19330
+ }
19331
+ function versionFile() {
19332
+ return process.env.SUPERTASK_VERSION_FILE ?? join3(homedir3(), ".local/share/opencode/supertask-gateway-version");
19333
+ }
19334
+ function getRunningVersion() {
19335
+ try {
19336
+ const path = versionFile();
19337
+ if (!existsSync3(path)) return null;
19338
+ return readFileSync2(path, "utf-8").trim() || null;
19339
+ } catch {
19340
+ return null;
19341
+ }
19342
+ }
19343
+ function writeRunningVersion(version2) {
19344
+ const path = versionFile();
19345
+ mkdirSync2(dirname2(path), { recursive: true });
19346
+ writeFileSync(path, version2, "utf-8");
19347
+ }
19348
+ function pm2Bin() {
19349
+ return process.env.SUPERTASK_PM2_BIN ?? (process.platform === "win32" ? "pm2.cmd" : "pm2");
19350
+ }
19351
+ function isPm2Installed() {
19352
+ const result = spawnSync(pm2Bin(), ["--version"], {
19353
+ stdio: "ignore",
19354
+ shell: process.platform === "win32"
19355
+ });
19356
+ return result.status === 0;
19357
+ }
19358
+ function installPm2() {
19359
+ console.log("[supertask] Installing pm2...");
19360
+ try {
19361
+ execSync("npm install -g pm2", { stdio: "inherit" });
19362
+ return true;
19363
+ } catch {
19364
+ try {
19365
+ execSync("bun install -g pm2", { stdio: "inherit" });
19366
+ return true;
19367
+ } catch {
19368
+ return false;
19369
+ }
19370
+ }
19371
+ }
19372
+ function pm2Exec(args) {
19373
+ const result = spawnSync(pm2Bin(), args, {
19374
+ stdio: ["pipe", "pipe", "pipe"],
19375
+ encoding: "utf-8",
19376
+ env: process.env,
19377
+ shell: process.platform === "win32"
19378
+ });
19379
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
19380
+ if (result.error) return { ok: false, output: result.error.message };
19381
+ return { ok: result.status === 0, output };
19382
+ }
19383
+ function requirePm2(args, action) {
19384
+ const result = pm2Exec(args);
19385
+ if (!result.ok) throw new Error(`[supertask] ${action} failed: ${result.output || "unknown pm2 error"}`);
19386
+ return result.output;
19387
+ }
19388
+ function pm2JsonList() {
19389
+ const output = requirePm2(["jlist"], "pm2 jlist");
19390
+ try {
19391
+ const parsed = JSON.parse(output);
19392
+ if (!Array.isArray(parsed)) throw new Error("result is not an array");
19393
+ return parsed;
19394
+ } catch (error) {
19395
+ const message = error instanceof Error ? error.message : String(error);
19396
+ throw new Error(`[supertask] Invalid pm2 jlist output: ${message}`);
19397
+ }
19398
+ }
19399
+ function isGatewayRunning() {
19400
+ if (!isPm2Installed()) return false;
19401
+ const proc = pm2JsonList().find((item) => item.name === PROCESS_NAME);
19402
+ return proc?.pm2_env?.status === "online" && typeof proc.pid === "number" && isGatewayReady(proc.pid);
19403
+ }
19404
+ function databasePath() {
19405
+ return process.env.SUPERTASK_DB_PATH ?? join3(homedir3(), ".local/share/opencode/tasks.db");
19406
+ }
19407
+ function isGatewayReady(expectedPid) {
19408
+ const path = databasePath();
19409
+ if (!existsSync3(path)) return false;
19410
+ let database = null;
19411
+ try {
19412
+ database = new Database3(path, { readonly: true });
19413
+ const row = database.query(
19414
+ "SELECT pid, heartbeat_at, ready_at FROM gateway_lock WHERE id = 1"
19415
+ ).get();
19416
+ if (!row || row.ready_at == null) return false;
19417
+ if (expectedPid !== void 0 && row.pid !== expectedPid) return false;
19418
+ const ageMs = Date.now() - row.heartbeat_at;
19419
+ return ageMs >= -5e3 && ageMs < GATEWAY_LOCK_STALE_MS;
19420
+ } catch {
19421
+ return false;
19422
+ } finally {
19423
+ database?.close();
19424
+ }
19425
+ }
19426
+ function readyTimeoutMs() {
19427
+ const value = Number(process.env.SUPERTASK_GATEWAY_READY_TIMEOUT_MS ?? 3e4);
19428
+ return Number.isFinite(value) && value > 0 ? value : 3e4;
19429
+ }
19430
+ function waitForGatewayReady(pid) {
19431
+ const deadline = Date.now() + readyTimeoutMs();
19432
+ const sleeper = new Int32Array(new SharedArrayBuffer(4));
19433
+ while (Date.now() < deadline) {
19434
+ if (isGatewayReady(pid)) return true;
19435
+ Atomics.wait(sleeper, 0, 0, 100);
19436
+ }
19437
+ return isGatewayReady(pid);
19438
+ }
19439
+ function findBunPath() {
19440
+ const override = process.env.SUPERTASK_BUN_BIN;
19441
+ if (override) return override;
19442
+ try {
19443
+ const command = process.platform === "win32" ? "where bun" : "which bun";
19444
+ return execSync(command, { stdio: "pipe" }).toString().trim().split("\n")[0];
19445
+ } catch {
19446
+ return process.execPath;
19447
+ }
19448
+ }
19449
+ function pm2StartGateway(gatewayEntry = resolveGatewayEntry()) {
19450
+ const configuredKillTimeout = Number(process.env.SUPERTASK_PM2_KILL_TIMEOUT_MS);
19451
+ const killTimeoutMs = Number.isInteger(configuredKillTimeout) && configuredKillTimeout >= 5e3 ? configuredKillTimeout : loadConfig().worker.shutdownGracePeriodMs + 5e3;
19452
+ requirePm2([
19453
+ "start",
19454
+ findBunPath(),
19455
+ "--name",
19456
+ PROCESS_NAME,
19457
+ "--interpreter",
19458
+ "none",
19459
+ "--restart-delay",
19460
+ "5000",
19461
+ "--max-restarts",
19462
+ "30",
19463
+ "--kill-timeout",
19464
+ String(killTimeoutMs),
19465
+ "--",
19466
+ gatewayEntry
19467
+ ], "pm2 start");
19468
+ const started = pm2JsonList().find((item) => item.name === PROCESS_NAME);
19469
+ if (started?.pm2_env?.status !== "online") {
19470
+ throw new Error(`[supertask] Gateway did not become online (status: ${started?.pm2_env?.status ?? "missing"})`);
19471
+ }
19472
+ if (typeof started.pid !== "number" || !waitForGatewayReady(started.pid)) {
19473
+ 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");
19474
+ }
19475
+ }
19476
+ function savePm2State() {
19477
+ requirePm2(["save"], "pm2 save");
19478
+ }
19479
+ function install() {
19480
+ if (!isPm2Installed() && !installPm2()) {
19481
+ throw new Error("[supertask] Failed to install pm2. Please install it manually: npm install -g pm2");
19482
+ }
19483
+ const existing = pm2JsonList().find((item) => item.name === PROCESS_NAME);
19484
+ if (existing) requirePm2(["delete", PROCESS_NAME], "pm2 delete existing Gateway");
19485
+ const version2 = getPackageVersion();
19486
+ pm2StartGateway();
19487
+ writeRunningVersion(version2);
19488
+ savePm2State();
19489
+ const startup = pm2Exec(["startup"]);
19490
+ if (startup.output) console.log(startup.output);
19491
+ if (!startup.ok) {
19492
+ 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");
19493
+ }
19494
+ console.log("[supertask] Gateway installed and running.");
19495
+ console.log("[supertask] Manage with: pm2 status / pm2 logs supertask-gateway");
19496
+ }
19497
+ function uninstall() {
19498
+ if (!isPm2Installed()) throw new Error("[supertask] pm2 is not installed");
19499
+ const existing = pm2JsonList().find((item) => item.name === PROCESS_NAME);
19500
+ if (existing) requirePm2(["delete", PROCESS_NAME], "pm2 delete Gateway");
19501
+ savePm2State();
19502
+ console.log("[supertask] Gateway removed from pm2. Other pm2 startup entries were preserved.");
19503
+ }
19504
+ function upgrade(target) {
19505
+ if (!isPm2Installed()) {
19506
+ throw new Error("[supertask] pm2 is not installed. Run `supertask install` first.");
19507
+ }
19508
+ const before = getRunningVersion();
19509
+ const oldGatewayEntry = resolveGatewayEntry();
19510
+ const currentVersion = target?.version ?? getPackageVersion();
19511
+ const existing = pm2JsonList().find((item) => item.name === PROCESS_NAME);
19512
+ if (existing) requirePm2(["delete", PROCESS_NAME], "pm2 delete old Gateway");
19513
+ try {
19514
+ pm2StartGateway(target?.gatewayEntry ?? oldGatewayEntry);
19515
+ writeRunningVersion(currentVersion);
19516
+ savePm2State();
19517
+ return { before, after: currentVersion, restarted: true };
19518
+ } catch (error) {
19519
+ const failed = pm2JsonList().find((item) => item.name === PROCESS_NAME);
19520
+ if (failed) requirePm2(["delete", PROCESS_NAME], "pm2 delete failed Gateway");
19521
+ try {
19522
+ pm2StartGateway(oldGatewayEntry);
19523
+ if (before) writeRunningVersion(before);
19524
+ savePm2State();
19525
+ } catch (rollbackError) {
19526
+ const original = error instanceof Error ? error.message : String(error);
19527
+ const rollback = rollbackError instanceof Error ? rollbackError.message : String(rollbackError);
19528
+ throw new Error(`${original}; \u65E7 Gateway \u56DE\u6EDA\u4E5F\u5931\u8D25: ${rollback}`);
19529
+ }
19530
+ const message = error instanceof Error ? error.message : String(error);
19531
+ throw new Error(`${message}; \u5DF2\u56DE\u6EDA\u5230\u65E7 Gateway`);
19532
+ }
19533
+ }
19534
+ function ensureGateway() {
19535
+ if (!isPm2Installed()) {
19536
+ return { ok: false, reason: "pm2-not-installed" };
19537
+ }
19538
+ const currentVersion = getPackageVersion();
19539
+ const processList = pm2JsonList();
19540
+ const existing = processList.find((item) => item.name === PROCESS_NAME);
19541
+ if (existing?.pm2_env?.status === "online" && typeof existing.pid === "number" && isGatewayReady(existing.pid) && getRunningVersion() === currentVersion) {
19542
+ return { ok: true, action: "already-running" };
19543
+ }
19544
+ if (existing) requirePm2(["delete", PROCESS_NAME], "pm2 delete stale Gateway");
19545
+ pm2StartGateway();
19546
+ writeRunningVersion(currentVersion);
19547
+ savePm2State();
19548
+ return { ok: true, action: existing ? "restarted" : "started" };
19549
+ }
19550
+ var __dirname, PROCESS_NAME, GATEWAY_LOCK_STALE_MS;
19551
+ var init_pm2 = __esm({
19552
+ "src/daemon/pm2.ts"() {
19553
+ "use strict";
19554
+ init_config();
19555
+ __dirname = dirname2(fileURLToPath2(import.meta.url));
19556
+ PROCESS_NAME = "supertask-gateway";
19557
+ GATEWAY_LOCK_STALE_MS = 3e4;
19122
19558
  }
19123
19559
  });
19124
19560
 
19125
19561
  // src/core/services/task-run.service.ts
19126
- var taskRuns2, TaskRunService;
19562
+ var taskRuns3, TaskRunService;
19127
19563
  var init_task_run_service = __esm({
19128
19564
  "src/core/services/task-run.service.ts"() {
19129
19565
  "use strict";
19130
19566
  init_db2();
19131
19567
  init_drizzle_orm();
19132
- ({ taskRuns: taskRuns2 } = schema_exports);
19568
+ ({ taskRuns: taskRuns3 } = schema_exports);
19133
19569
  TaskRunService = class {
19134
19570
  static async create(data) {
19135
- const result = await db.insert(taskRuns2).values(data).returning();
19571
+ const result = await db.insert(taskRuns3).values(data).returning();
19136
19572
  return result[0];
19137
19573
  }
19138
19574
  static async updateSessionId(id, sessionId) {
19139
- const result = await db.update(taskRuns2).set({ sessionId }).where(eq(taskRuns2.id, id)).returning();
19575
+ const result = await db.update(taskRuns3).set({ sessionId }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
19140
19576
  return result[0] || null;
19141
19577
  }
19142
19578
  static async done(id, log) {
19143
- const result = await db.update(taskRuns2).set({
19579
+ const result = await db.update(taskRuns3).set({
19144
19580
  status: "done",
19145
19581
  finishedAt: /* @__PURE__ */ new Date(),
19146
19582
  log
19147
- }).where(eq(taskRuns2.id, id)).returning();
19583
+ }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
19148
19584
  return result[0] || null;
19149
19585
  }
19150
19586
  static async fail(id, log) {
19151
- const result = await db.update(taskRuns2).set({
19587
+ const result = await db.update(taskRuns3).set({
19152
19588
  status: "failed",
19153
19589
  finishedAt: /* @__PURE__ */ new Date(),
19154
19590
  log
19155
- }).where(eq(taskRuns2.id, id)).returning();
19591
+ }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
19156
19592
  return result[0] || null;
19157
19593
  }
19158
19594
  static async heartbeat(id) {
19159
- const result = await db.update(taskRuns2).set({ heartbeatAt: Date.now() }).where(and(eq(taskRuns2.id, id), eq(taskRuns2.status, "running"))).returning();
19595
+ const result = await db.update(taskRuns3).set({ heartbeatAt: Date.now() }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
19160
19596
  return result[0] || null;
19161
19597
  }
19162
19598
  static async updatePid(id, workerPid, childPid) {
19163
- const result = await db.update(taskRuns2).set({
19599
+ const result = await db.update(taskRuns3).set({
19164
19600
  workerPid,
19165
19601
  childPid,
19166
19602
  lockedAt: Date.now(),
19167
19603
  lockedBy: `gateway-${process.pid}`
19168
- }).where(eq(taskRuns2.id, id)).returning();
19604
+ }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
19169
19605
  return result[0] || null;
19170
19606
  }
19171
19607
  static async getById(id) {
19172
- const result = await db.select().from(taskRuns2).where(eq(taskRuns2.id, id));
19608
+ const result = await db.select().from(taskRuns3).where(eq(taskRuns3.id, id));
19173
19609
  return result[0] || null;
19174
19610
  }
19175
19611
  static async listByTaskId(taskId) {
19176
- return await db.select().from(taskRuns2).where(eq(taskRuns2.taskId, taskId)).orderBy(desc(taskRuns2.startedAt), desc(taskRuns2.id));
19612
+ return await db.select().from(taskRuns3).where(eq(taskRuns3.taskId, taskId)).orderBy(desc(taskRuns3.startedAt), desc(taskRuns3.id));
19177
19613
  }
19178
19614
  static async getLatestByTaskId(taskId) {
19179
- const result = await db.select().from(taskRuns2).where(eq(taskRuns2.taskId, taskId)).orderBy(desc(taskRuns2.startedAt), desc(taskRuns2.id)).limit(1);
19615
+ const result = await db.select().from(taskRuns3).where(eq(taskRuns3.taskId, taskId)).orderBy(desc(taskRuns3.startedAt), desc(taskRuns3.id)).limit(1);
19180
19616
  return result[0] || null;
19181
19617
  }
19182
19618
  static async getLatestByTaskIds(taskIds) {
19183
19619
  if (taskIds.length === 0) return /* @__PURE__ */ new Map();
19184
- const latestRuns = await db.select().from(taskRuns2).where(inArray(taskRuns2.taskId, taskIds)).orderBy(desc(taskRuns2.startedAt));
19620
+ const latestRuns = await db.select().from(taskRuns3).where(inArray(taskRuns3.taskId, taskIds)).orderBy(desc(taskRuns3.startedAt), desc(taskRuns3.id));
19185
19621
  const result = /* @__PURE__ */ new Map();
19186
19622
  for (const run of latestRuns) {
19187
19623
  if (!result.has(run.taskId)) {
@@ -19195,22 +19631,23 @@ var init_task_run_service = __esm({
19195
19631
  const cutoffSec = Math.floor(cutoffMs / 1e3);
19196
19632
  const { tasks: tasksTable } = schema_exports;
19197
19633
  const result = await db.select({
19198
- runId: taskRuns2.id,
19199
- taskId: taskRuns2.taskId,
19200
- childPid: taskRuns2.childPid,
19634
+ runId: taskRuns3.id,
19635
+ taskId: taskRuns3.taskId,
19636
+ childPid: taskRuns3.childPid,
19201
19637
  taskRetryCount: tasksTable.retryCount,
19202
- taskMaxRetries: tasksTable.maxRetries
19203
- }).from(taskRuns2).innerJoin(tasksTable, eq(taskRuns2.taskId, tasksTable.id)).where(
19638
+ taskMaxRetries: tasksTable.maxRetries,
19639
+ taskRetryBackoffMs: tasksTable.retryBackoffMs
19640
+ }).from(taskRuns3).innerJoin(tasksTable, eq(taskRuns3.taskId, tasksTable.id)).where(
19204
19641
  and(
19205
- eq(taskRuns2.status, "running"),
19642
+ eq(taskRuns3.status, "running"),
19206
19643
  or(
19207
19644
  and(
19208
- sql`${taskRuns2.heartbeatAt} IS NULL`,
19209
- sql`${taskRuns2.startedAt} < ${cutoffSec}`
19645
+ sql`${taskRuns3.heartbeatAt} IS NULL`,
19646
+ sql`${taskRuns3.startedAt} < ${cutoffSec}`
19210
19647
  ),
19211
19648
  and(
19212
- sql`${taskRuns2.heartbeatAt} IS NOT NULL`,
19213
- sql`${taskRuns2.heartbeatAt} < ${cutoffMs}`
19649
+ sql`${taskRuns3.heartbeatAt} IS NOT NULL`,
19650
+ sql`${taskRuns3.heartbeatAt} < ${cutoffMs}`
19214
19651
  )
19215
19652
  )
19216
19653
  )
@@ -19220,65 +19657,234 @@ var init_task_run_service = __esm({
19220
19657
  taskId: row.taskId,
19221
19658
  childPid: row.childPid,
19222
19659
  taskRetryCount: row.taskRetryCount ?? 0,
19223
- taskMaxRetries: row.taskMaxRetries ?? 3
19660
+ taskMaxRetries: row.taskMaxRetries ?? 3,
19661
+ taskRetryBackoffMs: row.taskRetryBackoffMs ?? 3e4
19224
19662
  }));
19225
19663
  }
19226
19664
  static async getRunningRunByTaskId(taskId) {
19227
- 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);
19665
+ 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);
19228
19666
  return result[0] || null;
19229
19667
  }
19230
19668
  static async deleteByTaskIds(taskIds) {
19231
19669
  if (taskIds.length === 0) return 0;
19232
- const result = await db.delete(taskRuns2).where(inArray(taskRuns2.taskId, taskIds)).returning();
19670
+ const result = await db.delete(taskRuns3).where(inArray(taskRuns3.taskId, taskIds)).returning();
19233
19671
  return result.length;
19234
19672
  }
19235
19673
  static async getAllRunningRuns() {
19236
- return await db.select().from(taskRuns2).where(eq(taskRuns2.status, "running"));
19674
+ return await db.select().from(taskRuns3).where(eq(taskRuns3.status, "running"));
19237
19675
  }
19238
19676
  };
19239
19677
  }
19240
19678
  });
19241
19679
 
19680
+ // src/gateway/health.ts
19681
+ function initializeGatewayHealth(config) {
19682
+ const now = Date.now();
19683
+ state = {
19684
+ startedAt: now,
19685
+ config,
19686
+ lastActivityAt: {
19687
+ worker: now,
19688
+ scheduler: now,
19689
+ watchdog: now
19690
+ }
19691
+ };
19692
+ }
19693
+ function markGatewayActivity(component) {
19694
+ if (state) state.lastActivityAt[component] = Date.now();
19695
+ }
19696
+ function resetGatewayHealth() {
19697
+ state = null;
19698
+ }
19699
+ function componentStatus(component, enabled, maxAgeMs, now) {
19700
+ const lastActivityAt = state?.lastActivityAt[component] ?? null;
19701
+ const ageMs = lastActivityAt == null ? null : Math.max(0, now - lastActivityAt);
19702
+ return {
19703
+ enabled,
19704
+ healthy: !enabled || ageMs != null && ageMs <= maxAgeMs,
19705
+ lastActivityAt,
19706
+ ageMs,
19707
+ maxAgeMs
19708
+ };
19709
+ }
19710
+ function getGatewayHealth(now = Date.now()) {
19711
+ const worker = componentStatus(
19712
+ "worker",
19713
+ true,
19714
+ Math.max((state?.config.workerPollIntervalMs ?? 1e3) * 5, 5e3),
19715
+ now
19716
+ );
19717
+ const scheduler = componentStatus(
19718
+ "scheduler",
19719
+ state?.config.schedulerEnabled ?? false,
19720
+ Math.max((state?.config.schedulerCheckIntervalMs ?? 1e3) * 5, 5e3),
19721
+ now
19722
+ );
19723
+ const watchdog = componentStatus(
19724
+ "watchdog",
19725
+ true,
19726
+ Math.max((state?.config.watchdogCheckIntervalMs ?? 6e4) * 3, 5e3),
19727
+ now
19728
+ );
19729
+ let lock = { pid: null, heartbeatAt: null, readyAt: null, ageMs: null, healthy: false };
19730
+ try {
19731
+ const row = sqlite.prepare(
19732
+ "SELECT pid, heartbeat_at, ready_at FROM gateway_lock WHERE id = 1"
19733
+ ).get();
19734
+ if (row) {
19735
+ const ageMs = Math.max(0, now - row.heartbeat_at);
19736
+ lock = {
19737
+ pid: row.pid,
19738
+ heartbeatAt: row.heartbeat_at,
19739
+ readyAt: row.ready_at,
19740
+ ageMs,
19741
+ healthy: row.pid === process.pid && row.ready_at != null && ageMs < LOCK_STALE_MS
19742
+ };
19743
+ }
19744
+ } catch {
19745
+ }
19746
+ const healthy = state != null && lock.healthy && worker.healthy && scheduler.healthy && watchdog.healthy;
19747
+ return {
19748
+ status: healthy ? "ok" : "degraded",
19749
+ pid: process.pid,
19750
+ uptimeMs: state ? Math.max(0, now - state.startedAt) : 0,
19751
+ lock,
19752
+ components: { worker, scheduler, watchdog }
19753
+ };
19754
+ }
19755
+ var LOCK_STALE_MS, state;
19756
+ var init_health = __esm({
19757
+ "src/gateway/health.ts"() {
19758
+ "use strict";
19759
+ init_db2();
19760
+ LOCK_STALE_MS = 3e4;
19761
+ state = null;
19762
+ }
19763
+ });
19764
+
19765
+ // src/core/process-control.ts
19766
+ import { spawnSync as spawnSync2 } from "child_process";
19767
+ import { basename } from "path";
19768
+ function isSafePid(pid) {
19769
+ return Number.isInteger(pid) && pid > 1 && pid !== process.pid;
19770
+ }
19771
+ function inspectUnixProcess(pid) {
19772
+ const result = spawnSync2("ps", ["-o", "pgid=", "-o", "command=", "-p", String(pid)], {
19773
+ encoding: "utf8",
19774
+ stdio: ["ignore", "pipe", "ignore"]
19775
+ });
19776
+ if (result.status !== 0) return null;
19777
+ const match2 = result.stdout.trim().match(/^(\d+)\s+(.+)$/s);
19778
+ if (!match2) return null;
19779
+ return { processGroupId: Number(match2[1]), command: match2[2] };
19780
+ }
19781
+ function commandMatches(command, expectedExecutable) {
19782
+ const expectedName = basename(expectedExecutable).trim().toLowerCase();
19783
+ if (!expectedName) return false;
19784
+ return command.toLowerCase().includes(expectedName);
19785
+ }
19786
+ function signalSpawnedProcessTree(pid, signal) {
19787
+ if (!isSafePid(pid)) return false;
19788
+ if (process.platform !== "win32") {
19789
+ try {
19790
+ process.kill(-pid, signal);
19791
+ return true;
19792
+ } catch {
19793
+ }
19794
+ }
19795
+ try {
19796
+ process.kill(pid, signal);
19797
+ return true;
19798
+ } catch {
19799
+ return false;
19800
+ }
19801
+ }
19802
+ function signalRecordedProcessTree(pid, signal, expectedExecutable) {
19803
+ if (!isSafePid(pid)) return false;
19804
+ if (process.platform === "win32") {
19805
+ const list = spawnSync2("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
19806
+ encoding: "utf8",
19807
+ stdio: ["ignore", "pipe", "ignore"]
19808
+ });
19809
+ if (list.status !== 0 || !commandMatches(list.stdout, expectedExecutable)) return false;
19810
+ const args = ["/PID", String(pid), "/T"];
19811
+ if (signal === "SIGKILL") args.push("/F");
19812
+ return spawnSync2("taskkill", args, { stdio: "ignore" }).status === 0;
19813
+ }
19814
+ const info = inspectUnixProcess(pid);
19815
+ if (!info || !commandMatches(info.command, expectedExecutable)) return false;
19816
+ try {
19817
+ if (info.processGroupId === pid) process.kill(-pid, signal);
19818
+ else process.kill(pid, signal);
19819
+ return true;
19820
+ } catch {
19821
+ return false;
19822
+ }
19823
+ }
19824
+ var init_process_control = __esm({
19825
+ "src/core/process-control.ts"() {
19826
+ "use strict";
19827
+ }
19828
+ });
19829
+
19242
19830
  // src/worker/index.ts
19243
19831
  import { spawn } from "child_process";
19244
- var WorkerEngine;
19832
+ var DEFAULT_MAX_OUTPUT_CHARS, FORBIDDEN_AGENT, WorkerEngine;
19245
19833
  var init_worker = __esm({
19246
19834
  "src/worker/index.ts"() {
19247
19835
  "use strict";
19248
19836
  init_task_service();
19249
19837
  init_task_run_service();
19838
+ init_health();
19839
+ init_process_control();
19840
+ DEFAULT_MAX_OUTPUT_CHARS = 64 * 1024;
19841
+ FORBIDDEN_AGENT = "supertask-runner";
19250
19842
  WorkerEngine = class {
19251
19843
  activeBatchIds = /* @__PURE__ */ new Set();
19252
19844
  runningTasks = /* @__PURE__ */ new Map();
19253
19845
  stopped = false;
19254
19846
  pollTimer = null;
19255
19847
  heartbeatTimer = null;
19848
+ pollCyclePromise = null;
19256
19849
  cfg;
19257
- constructor(cfg) {
19850
+ opencodeBin;
19851
+ maxOutputChars;
19852
+ constructor(cfg, options = {}) {
19258
19853
  this.cfg = cfg.worker;
19854
+ this.opencodeBin = options.opencodeBin ?? process.env.SUPERTASK_OPENCODE_BIN ?? "opencode";
19855
+ this.maxOutputChars = options.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
19259
19856
  }
19260
19857
  start() {
19261
19858
  this.stopped = false;
19859
+ markGatewayActivity("worker");
19262
19860
  this.poll();
19263
19861
  this.heartbeatTimer = setInterval(() => this.updateHeartbeats(), this.cfg.heartbeatIntervalMs);
19264
19862
  }
19265
- stop() {
19863
+ async stop(gracePeriodMs = 0) {
19266
19864
  this.stopped = true;
19267
19865
  if (this.pollTimer) {
19268
19866
  clearTimeout(this.pollTimer);
19269
19867
  this.pollTimer = null;
19270
19868
  }
19869
+ if (this.pollCyclePromise) await this.pollCyclePromise;
19870
+ if (gracePeriodMs > 0 && this.runningTasks.size > 0) {
19871
+ const deadline = Date.now() + gracePeriodMs;
19872
+ while (this.runningTasks.size > 0 && Date.now() < deadline) {
19873
+ await Bun.sleep(Math.min(50, deadline - Date.now()));
19874
+ }
19875
+ }
19271
19876
  if (this.heartbeatTimer) {
19272
19877
  clearInterval(this.heartbeatTimer);
19273
19878
  this.heartbeatTimer = null;
19274
19879
  }
19880
+ const interruptedTaskIds = [...this.runningTasks.keys()];
19275
19881
  const killPromises = [];
19276
- for (const [, entry] of this.runningTasks) {
19882
+ for (const entry of this.runningTasks.values()) {
19277
19883
  entry.shutdown = true;
19278
19884
  killPromises.push(this.killEntry(entry));
19279
19885
  }
19280
- return Promise.allSettled(killPromises).then(() => {
19281
- });
19886
+ await Promise.allSettled(killPromises);
19887
+ return interruptedTaskIds;
19282
19888
  }
19283
19889
  getRunningTaskIds() {
19284
19890
  return [...this.runningTasks.keys()];
@@ -19288,136 +19894,244 @@ var init_worker = __esm({
19288
19894
  }
19289
19895
  poll() {
19290
19896
  if (this.stopped) return;
19291
- this.tryDispatch().then(() => {
19897
+ markGatewayActivity("worker");
19898
+ this.pollCyclePromise = this.tryDispatch().finally(() => {
19899
+ this.pollCyclePromise = null;
19292
19900
  if (this.stopped) return;
19293
19901
  this.pollTimer = setTimeout(() => this.poll(), this.cfg.pollIntervalMs);
19294
19902
  });
19295
19903
  }
19296
19904
  async tryDispatch() {
19905
+ await this.reconcileCancelledTasks();
19297
19906
  while (!this.stopped && this.runningTasks.size < this.cfg.maxConcurrency) {
19907
+ let task;
19908
+ try {
19909
+ task = await TaskService.next({ excludedBatchIds: [...this.activeBatchIds] });
19910
+ } catch (err) {
19911
+ this.logError("task claim failed", err);
19912
+ break;
19913
+ }
19914
+ if (!task) break;
19915
+ if (this.stopped) break;
19916
+ if (!await TaskService.start(task.id)) continue;
19917
+ if (task.batchId) this.activeBatchIds.add(task.batchId);
19918
+ if (this.stopped) {
19919
+ await TaskService.resetRunningToPending([task.id]);
19920
+ this.releaseBatch(task);
19921
+ break;
19922
+ }
19298
19923
  try {
19299
- const excludedBatchIds = [...this.activeBatchIds];
19300
- const task = await TaskService.next({ excludedBatchIds });
19301
- if (!task) break;
19302
- if (!await TaskService.start(task.id)) continue;
19303
- if (task.batchId) {
19304
- this.activeBatchIds.add(task.batchId);
19305
- }
19306
19924
  const run = await TaskRunService.create({
19307
19925
  taskId: task.id,
19308
19926
  model: this.resolveModel(task.model),
19309
19927
  status: "running"
19310
19928
  });
19311
- const modelToUse = this.resolveModel(task.model);
19312
- const args = ["run", "--agent", "supertask-runner", "--format", "json"];
19313
- if (modelToUse) {
19314
- args.push("-m", modelToUse);
19929
+ if (this.stopped) {
19930
+ await TaskRunService.fail(run.id, "Gateway shutdown before spawn");
19931
+ await TaskService.resetRunningToPending([task.id]);
19932
+ this.releaseBatch(task);
19933
+ break;
19315
19934
  }
19316
- args.push(`\u6267\u884C\u4EFB\u52A1 ID: ${task.id}${modelToUse ? ` OVERRIDE_MODEL=${modelToUse}` : ""}`);
19317
- const cwd = task.cwd || process.cwd();
19318
- const child = spawn("opencode", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
19319
- await TaskRunService.updatePid(run.id, process.pid, child.pid ?? 0);
19320
- let output = "";
19321
- const handleData = (data) => {
19322
- const text2 = data.toString();
19323
- output += text2;
19324
- process.stdout.write(text2);
19325
- const match2 = text2.match(/"sessionID"\s*:\s*"(ses_[^"]+)"/);
19326
- if (match2) {
19327
- TaskRunService.updateSessionId(run.id, match2[1]).then(() => {
19328
- console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: "sessionId captured", taskId: task.id, sessionId: match2[1] }));
19329
- });
19330
- }
19331
- };
19332
- child.stdout?.on("data", handleData);
19333
- child.stderr?.on("data", handleData);
19334
- const entry = { task, runId: run.id, child, startedAt: Date.now(), shutdown: false };
19335
- this.runningTasks.set(task.id, entry);
19336
- child.on("close", async (code) => {
19337
- this.runningTasks.delete(task.id);
19338
- if (task.batchId) this.activeBatchIds.delete(task.batchId);
19339
- if (entry.shutdown) return;
19340
- const currentRun = await TaskRunService.getById(run.id);
19341
- if (!currentRun || currentRun.status !== "running") return;
19342
- if (code === 0) {
19343
- await TaskRunService.done(run.id);
19344
- await TaskService.done(task.id);
19345
- console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: "task done", taskId: task.id }));
19346
- } else {
19347
- const lastOutput = output.slice(-2e3);
19348
- await TaskRunService.fail(run.id, lastOutput);
19349
- const currentStatus = await TaskService.getById(task.id);
19350
- if (currentStatus?.status === "running") {
19351
- await TaskService.fail(task.id, "Worker\u6267\u884C\u5F02\u5E38\uFF1AOpencode \u8FDB\u7A0B\u975E\u6B63\u5E38\u9000\u51FA");
19352
- }
19353
- console.error(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "error", msg: "task failed", taskId: task.id, code }));
19354
- }
19355
- });
19356
- child.on("error", async (err) => {
19357
- this.runningTasks.delete(task.id);
19358
- if (task.batchId) this.activeBatchIds.delete(task.batchId);
19359
- if (entry.shutdown) return;
19360
- const currentRun = await TaskRunService.getById(run.id);
19361
- if (!currentRun || currentRun.status !== "running") return;
19362
- await TaskRunService.fail(run.id, err.message);
19363
- const currentStatus = await TaskService.getById(task.id);
19364
- if (currentStatus?.status === "running") {
19365
- await TaskService.fail(task.id, `spawn \u5F02\u5E38: ${err.message}`);
19366
- }
19367
- console.error(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "error", msg: "task spawn error", taskId: task.id, error: err.message }));
19935
+ if (task.agent === FORBIDDEN_AGENT) {
19936
+ const message = `\u7981\u6B62\u6267\u884C\u9012\u5F52 Agent: ${FORBIDDEN_AGENT}`;
19937
+ await TaskRunService.fail(run.id, message);
19938
+ await TaskService.fail(task.id, message, {}, { setDeadLetter: true });
19939
+ this.releaseBatch(task);
19940
+ continue;
19941
+ }
19942
+ this.spawnTask(task, run.id);
19943
+ } catch (err) {
19944
+ this.releaseBatch(task);
19945
+ const message = `Worker \u542F\u52A8\u4EFB\u52A1\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`;
19946
+ try {
19947
+ await TaskService.fail(task.id, message);
19948
+ } catch (failErr) {
19949
+ this.logError("failed to compensate task startup", failErr, task.id);
19950
+ }
19951
+ this.logError("task dispatch failed", err, task.id);
19952
+ }
19953
+ }
19954
+ }
19955
+ spawnTask(task, runId) {
19956
+ const model = this.resolveModel(task.model);
19957
+ const args = ["run", "--agent", task.agent, "--format", "json"];
19958
+ if (model) args.push("-m", model);
19959
+ args.push(task.prompt);
19960
+ const child = spawn(this.opencodeBin, args, {
19961
+ cwd: task.cwd || process.cwd(),
19962
+ stdio: ["ignore", "pipe", "pipe"],
19963
+ detached: process.platform !== "win32"
19964
+ });
19965
+ const entry = {
19966
+ task,
19967
+ runId,
19968
+ child,
19969
+ output: "",
19970
+ sessionId: null,
19971
+ timeoutTimer: null,
19972
+ shutdown: false,
19973
+ settled: false
19974
+ };
19975
+ this.runningTasks.set(task.id, entry);
19976
+ const handleData = (data) => {
19977
+ const text2 = data.toString();
19978
+ entry.output = (entry.output + text2).slice(-this.maxOutputChars);
19979
+ process.stdout.write(text2);
19980
+ const match2 = entry.output.match(/"sessionID"\s*:\s*"(ses_[^"]+)"/);
19981
+ if (match2?.[1] && match2[1] !== entry.sessionId) {
19982
+ entry.sessionId = match2[1];
19983
+ TaskRunService.updateSessionId(runId, match2[1]).catch((err) => {
19984
+ this.logError("sessionId update failed", err, task.id);
19368
19985
  });
19986
+ }
19987
+ };
19988
+ child.stdout?.on("data", handleData);
19989
+ child.stderr?.on("data", handleData);
19990
+ child.once("error", (err) => {
19991
+ void this.finishEntry(entry, null, `\u65E0\u6CD5\u542F\u52A8 opencode\uFF1A${err.message}`);
19992
+ });
19993
+ child.once("close", (code, signal) => {
19994
+ const failure = code === 0 ? void 0 : `opencode \u9000\u51FA\u7801 ${code ?? "null"}${signal ? `\uFF0C\u4FE1\u53F7 ${signal}` : ""}`;
19995
+ void this.finishEntry(entry, code, failure);
19996
+ });
19997
+ const timeoutMs = task.timeoutMs ?? this.cfg.taskTimeoutMs;
19998
+ if (timeoutMs > 0) {
19999
+ entry.timeoutTimer = setTimeout(() => {
20000
+ this.signalEntry(entry, "SIGKILL");
20001
+ void this.finishEntry(entry, null, `\u4EFB\u52A1\u8D85\u65F6\uFF08${timeoutMs}ms\uFF09`);
20002
+ }, timeoutMs);
20003
+ }
20004
+ TaskRunService.updatePid(runId, process.pid, child.pid ?? 0).catch((err) => {
20005
+ this.signalEntry(entry, "SIGKILL");
20006
+ void this.finishEntry(entry, null, `\u8BB0\u5F55 Worker PID \u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`);
20007
+ });
20008
+ }
20009
+ async finishEntry(entry, code, failure) {
20010
+ if (entry.settled) return;
20011
+ entry.settled = true;
20012
+ if (entry.timeoutTimer) {
20013
+ clearTimeout(entry.timeoutTimer);
20014
+ entry.timeoutTimer = null;
20015
+ }
20016
+ try {
20017
+ if (entry.shutdown) return;
20018
+ const currentRun = await TaskRunService.getById(entry.runId);
20019
+ if (!currentRun || currentRun.status !== "running") return;
20020
+ const output = entry.output.trim();
20021
+ const log = failure ? `${failure}${output ? `
20022
+ ${output}` : ""}` : output;
20023
+ if (code === 0 && !failure) {
20024
+ const completed = await TaskService.done(entry.task.id, log);
20025
+ if (completed) {
20026
+ await TaskRunService.done(entry.runId, log);
20027
+ console.log(JSON.stringify({
20028
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
20029
+ level: "info",
20030
+ msg: "task done",
20031
+ taskId: entry.task.id
20032
+ }));
20033
+ return;
20034
+ }
20035
+ await TaskRunService.fail(entry.runId, "\u4EFB\u52A1\u72B6\u6001\u5DF2\u88AB\u5176\u4ED6\u64CD\u4F5C\u6539\u53D8");
20036
+ return;
20037
+ }
20038
+ await TaskRunService.fail(entry.runId, log);
20039
+ const failed = await TaskService.fail(entry.task.id, log);
20040
+ if (!failed) {
20041
+ this.logError("task failure state transition rejected", failure ?? "unknown failure", entry.task.id);
20042
+ }
20043
+ console.error(JSON.stringify({
20044
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
20045
+ level: "error",
20046
+ msg: "task failed",
20047
+ taskId: entry.task.id,
20048
+ error: failure
20049
+ }));
20050
+ } finally {
20051
+ this.runningTasks.delete(entry.task.id);
20052
+ this.releaseBatch(entry.task);
20053
+ }
20054
+ }
20055
+ async reconcileCancelledTasks() {
20056
+ for (const entry of [...this.runningTasks.values()]) {
20057
+ try {
20058
+ const task = await TaskService.getById(entry.task.id);
20059
+ if (task?.status === "cancelled") await this.cancelEntry(entry);
19369
20060
  } catch (err) {
19370
- console.error(JSON.stringify({
19371
- ts: (/* @__PURE__ */ new Date()).toISOString(),
19372
- level: "error",
19373
- msg: "tryDispatch iteration failed",
19374
- error: err instanceof Error ? err.message : String(err)
19375
- }));
19376
- break;
20061
+ this.logError("cancel reconciliation failed", err, entry.task.id);
19377
20062
  }
19378
20063
  }
19379
20064
  }
20065
+ async cancelEntry(entry) {
20066
+ if (entry.settled) return;
20067
+ entry.settled = true;
20068
+ if (entry.timeoutTimer) {
20069
+ clearTimeout(entry.timeoutTimer);
20070
+ entry.timeoutTimer = null;
20071
+ }
20072
+ try {
20073
+ await this.killEntry(entry);
20074
+ const output = entry.output.trim();
20075
+ const log = `\u4EFB\u52A1\u5DF2\u53D6\u6D88${output ? `
20076
+ ${output}` : ""}`;
20077
+ await TaskRunService.fail(entry.runId, log);
20078
+ console.log(JSON.stringify({
20079
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
20080
+ level: "info",
20081
+ msg: "running task cancelled",
20082
+ taskId: entry.task.id
20083
+ }));
20084
+ } finally {
20085
+ this.runningTasks.delete(entry.task.id);
20086
+ this.releaseBatch(entry.task);
20087
+ }
20088
+ }
19380
20089
  async updateHeartbeats() {
19381
- for (const [, entry] of this.runningTasks) {
20090
+ for (const entry of this.runningTasks.values()) {
19382
20091
  try {
19383
20092
  await TaskRunService.heartbeat(entry.runId);
19384
- } catch {
20093
+ } catch (err) {
20094
+ this.logError("heartbeat update failed", err, entry.task.id);
19385
20095
  }
19386
20096
  }
19387
20097
  }
19388
20098
  killEntry(entry) {
19389
- if (entry.child.exitCode !== null) {
20099
+ if (entry.child.exitCode !== null || entry.child.signalCode !== null) {
19390
20100
  return Promise.resolve();
19391
20101
  }
19392
20102
  return new Promise((resolve) => {
19393
20103
  const timeout = setTimeout(() => {
19394
- try {
19395
- if (entry.child.pid) process.kill(entry.child.pid, "SIGKILL");
19396
- } catch {
19397
- }
20104
+ this.signalEntry(entry, "SIGKILL");
19398
20105
  resolve();
19399
20106
  }, 5e3);
19400
- entry.child.on("close", () => {
20107
+ entry.child.once("close", () => {
19401
20108
  clearTimeout(timeout);
19402
20109
  resolve();
19403
20110
  });
19404
- try {
19405
- if (entry.child.pid) {
19406
- entry.child.kill("SIGTERM");
19407
- } else {
19408
- clearTimeout(timeout);
19409
- resolve();
19410
- }
19411
- } catch {
19412
- clearTimeout(timeout);
19413
- resolve();
19414
- }
20111
+ this.signalEntry(entry, "SIGTERM");
19415
20112
  });
19416
20113
  }
20114
+ signalEntry(entry, signal) {
20115
+ const pid = entry.child.pid;
20116
+ if (!pid) return;
20117
+ signalSpawnedProcessTree(pid, signal);
20118
+ }
20119
+ releaseBatch(task) {
20120
+ if (task.batchId) this.activeBatchIds.delete(task.batchId);
20121
+ }
19417
20122
  resolveModel(taskModel) {
19418
20123
  if (!taskModel || taskModel === "default") return null;
19419
20124
  return taskModel;
19420
20125
  }
20126
+ logError(message, error, taskId) {
20127
+ console.error(JSON.stringify({
20128
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
20129
+ level: "error",
20130
+ msg: message,
20131
+ taskId,
20132
+ error: error instanceof Error ? error.message : String(error)
20133
+ }));
20134
+ }
19421
20135
  };
19422
20136
  }
19423
20137
  });
@@ -19429,15 +20143,23 @@ async function checkHeartbeats(heartbeatTimeoutMs) {
19429
20143
  for (const run of staleRuns) {
19430
20144
  try {
19431
20145
  if (run.childPid != null && run.childPid > 0) {
19432
- try {
19433
- process.kill(run.childPid, "SIGKILL");
19434
- } catch {
20146
+ const expectedExecutable = process.env.SUPERTASK_OPENCODE_BIN ?? "opencode";
20147
+ const killed = signalRecordedProcessTree(run.childPid, "SIGKILL", expectedExecutable);
20148
+ if (!killed) {
20149
+ console.warn(JSON.stringify({
20150
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
20151
+ level: "warn",
20152
+ msg: "stale child process was not killed because identity validation failed",
20153
+ taskId: run.taskId,
20154
+ runId: run.runId,
20155
+ childPid: run.childPid
20156
+ }));
19435
20157
  }
19436
20158
  }
19437
20159
  await TaskRunService.fail(run.runId, `\u5FC3\u8DF3\u8D85\u65F6 (${heartbeatTimeoutMs / 1e3}s)\uFF0CWatchdog kill`);
19438
20160
  const newRetryCount = run.taskRetryCount + 1;
19439
20161
  const maxRetries = run.taskMaxRetries;
19440
- if (newRetryCount >= maxRetries) {
20162
+ if (newRetryCount > maxRetries) {
19441
20163
  await TaskService.markDeadLetter(run.taskId, newRetryCount);
19442
20164
  console.log(JSON.stringify({
19443
20165
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -19449,7 +20171,7 @@ async function checkHeartbeats(heartbeatTimeoutMs) {
19449
20171
  maxRetries
19450
20172
  }));
19451
20173
  } else {
19452
- const backoffMs = computeBackoff(newRetryCount);
20174
+ const backoffMs = computeBackoff(newRetryCount, run.taskRetryBackoffMs);
19453
20175
  const retryAfter = Date.now() + backoffMs;
19454
20176
  await TaskService.markPendingForRetry(run.taskId, retryAfter, newRetryCount);
19455
20177
  console.log(JSON.stringify({
@@ -19480,6 +20202,7 @@ var init_heartbeat = __esm({
19480
20202
  init_task_run_service();
19481
20203
  init_task_service();
19482
20204
  init_backoff();
20205
+ init_process_control();
19483
20206
  }
19484
20207
  });
19485
20208
 
@@ -19524,6 +20247,7 @@ var init_watchdog = __esm({
19524
20247
  "use strict";
19525
20248
  init_heartbeat();
19526
20249
  init_cleanup();
20250
+ init_health();
19527
20251
  Watchdog = class {
19528
20252
  constructor(cfg) {
19529
20253
  this.cfg = cfg;
@@ -19534,13 +20258,14 @@ var init_watchdog = __esm({
19534
20258
  cleanupTimer = null;
19535
20259
  start() {
19536
20260
  this.stopped = false;
20261
+ markGatewayActivity("watchdog");
19537
20262
  this.heartbeatTimer = setInterval(
19538
20263
  () => this.runHeartbeatCheck(),
19539
- this.cfg.watchdog.cleanupIntervalMs
20264
+ this.cfg.watchdog.checkIntervalMs
19540
20265
  );
19541
20266
  this.cleanupTimer = setInterval(
19542
20267
  () => this.runCleanup(),
19543
- this.cfg.watchdog.cleanupIntervalMs * 24 * 60
20268
+ this.cfg.watchdog.cleanupIntervalMs
19544
20269
  );
19545
20270
  }
19546
20271
  stop() {
@@ -19556,6 +20281,7 @@ var init_watchdog = __esm({
19556
20281
  }
19557
20282
  async runHeartbeatCheck() {
19558
20283
  if (this.stopped) return;
20284
+ markGatewayActivity("watchdog");
19559
20285
  try {
19560
20286
  await checkHeartbeats(this.cfg.watchdog.heartbeatTimeoutMs);
19561
20287
  } catch (err) {
@@ -19588,7 +20314,7 @@ var init_watchdog = __esm({
19588
20314
  async function cloneTaskFromTemplate(templateId) {
19589
20315
  const rows = await db.select().from(taskTemplates3).where(eq(taskTemplates3.id, templateId)).limit(1);
19590
20316
  const tmpl = rows[0];
19591
- if (!tmpl) return null;
20317
+ if (!tmpl || !tmpl.enabled) return null;
19592
20318
  const activeCount = await db.select({ count: sql`count(*)` }).from(schema_exports.tasks).where(
19593
20319
  and(
19594
20320
  eq(schema_exports.tasks.templateId, templateId),
@@ -19597,7 +20323,8 @@ async function cloneTaskFromTemplate(templateId) {
19597
20323
  ).then((r) => r[0].count);
19598
20324
  if (activeCount >= (tmpl.maxInstances ?? 1)) return null;
19599
20325
  const nowMs = Date.now();
19600
- const nextRunAt = TaskTemplateService.calculateNextRunAt(
20326
+ const isDelayed = tmpl.scheduleType === "delayed";
20327
+ const nextRunAt = isDelayed ? null : TaskTemplateService.calculateNextRunAt(
19601
20328
  tmpl.scheduleType,
19602
20329
  tmpl,
19603
20330
  nowMs
@@ -19611,13 +20338,17 @@ async function cloneTaskFromTemplate(templateId) {
19611
20338
  category: tmpl.category ?? "general",
19612
20339
  importance: tmpl.importance ?? 3,
19613
20340
  urgency: tmpl.urgency ?? 3,
20341
+ batchId: tmpl.batchId,
19614
20342
  maxRetries: tmpl.maxRetries ?? 3,
20343
+ retryBackoffMs: tmpl.retryBackoffMs ?? 3e4,
20344
+ timeoutMs: tmpl.timeoutMs,
19615
20345
  templateId: tmpl.id,
19616
20346
  scheduledAt: nowMs
19617
20347
  });
19618
20348
  await db.update(taskTemplates3).set({
19619
20349
  lastRunAt: nowMs,
19620
20350
  nextRunAt,
20351
+ enabled: isDelayed ? false : tmpl.enabled,
19621
20352
  updatedAt: nowMs
19622
20353
  }).where(eq(taskTemplates3.id, templateId));
19623
20354
  return task;
@@ -19630,7 +20361,7 @@ async function getDueTemplates() {
19630
20361
  sql`${taskTemplates3.nextRunAt} IS NOT NULL`,
19631
20362
  sql`${taskTemplates3.nextRunAt} <= ${nowMs}`
19632
20363
  )
19633
- );
20364
+ ).orderBy(asc(taskTemplates3.nextRunAt), asc(taskTemplates3.id));
19634
20365
  }
19635
20366
  async function initializeNextRunAt(templateId) {
19636
20367
  const rows = await db.select().from(taskTemplates3).where(eq(taskTemplates3.id, templateId)).limit(1);
@@ -19664,6 +20395,7 @@ var init_scheduler = __esm({
19664
20395
  init_job_templates();
19665
20396
  init_db2();
19666
20397
  init_drizzle_orm();
20398
+ init_health();
19667
20399
  Scheduler = class {
19668
20400
  constructor(cfg) {
19669
20401
  this.cfg = cfg;
@@ -19675,6 +20407,7 @@ var init_scheduler = __esm({
19675
20407
  async start() {
19676
20408
  if (!this.cfg.scheduler.enabled) return;
19677
20409
  this.stopped = false;
20410
+ markGatewayActivity("scheduler");
19678
20411
  await this.initializeTemplates();
19679
20412
  this.timer = setInterval(() => this.tick(), this.cfg.scheduler.checkIntervalMs);
19680
20413
  }
@@ -19687,6 +20420,7 @@ var init_scheduler = __esm({
19687
20420
  }
19688
20421
  async tick() {
19689
20422
  if (this.stopped || this.ticking) return;
20423
+ markGatewayActivity("scheduler");
19690
20424
  this.ticking = true;
19691
20425
  try {
19692
20426
  const dueTemplates = await getDueTemplates();
@@ -19741,13 +20475,13 @@ var init_compose = __esm({
19741
20475
  "use strict";
19742
20476
  compose = (middleware, onError, onNotFound) => {
19743
20477
  return (context, next) => {
19744
- let index = -1;
20478
+ let index2 = -1;
19745
20479
  return dispatch(0);
19746
20480
  async function dispatch(i) {
19747
- if (i <= index) {
20481
+ if (i <= index2) {
19748
20482
  throw new Error("next() called multiple times");
19749
20483
  }
19750
- index = i;
20484
+ index2 = i;
19751
20485
  let res;
19752
20486
  let isError = false;
19753
20487
  let handler;
@@ -19862,8 +20596,8 @@ var init_body = __esm({
19862
20596
  handleParsingNestedValues = (form, key, value) => {
19863
20597
  let nestedForm = form;
19864
20598
  const keys = key.split(".");
19865
- keys.forEach((key2, index) => {
19866
- if (index === keys.length - 1) {
20599
+ keys.forEach((key2, index2) => {
20600
+ if (index2 === keys.length - 1) {
19867
20601
  nestedForm[key2] = value;
19868
20602
  } else {
19869
20603
  if (!nestedForm[key2] || typeof nestedForm[key2] !== "object" || Array.isArray(nestedForm[key2]) || nestedForm[key2] instanceof File) {
@@ -19895,8 +20629,8 @@ var init_url = __esm({
19895
20629
  };
19896
20630
  extractGroupsFromPath = (path) => {
19897
20631
  const groups = [];
19898
- path = path.replace(/\{[^}]+\}/g, (match2, index) => {
19899
- const mark = `@${index}`;
20632
+ path = path.replace(/\{[^}]+\}/g, (match2, index2) => {
20633
+ const mark = `@${index2}`;
19900
20634
  groups.push([mark, match2]);
19901
20635
  return mark;
19902
20636
  });
@@ -20415,10 +21149,10 @@ var init_html = __esm({
20415
21149
  return;
20416
21150
  }
20417
21151
  let escape;
20418
- let index;
21152
+ let index2;
20419
21153
  let lastIndex = 0;
20420
- for (index = match2; index < str.length; index++) {
20421
- switch (str.charCodeAt(index)) {
21154
+ for (index2 = match2; index2 < str.length; index2++) {
21155
+ switch (str.charCodeAt(index2)) {
20422
21156
  case 34:
20423
21157
  escape = "&quot;";
20424
21158
  break;
@@ -20437,10 +21171,10 @@ var init_html = __esm({
20437
21171
  default:
20438
21172
  continue;
20439
21173
  }
20440
- buffer[0] += str.substring(lastIndex, index) + escape;
20441
- lastIndex = index + 1;
21174
+ buffer[0] += str.substring(lastIndex, index2) + escape;
21175
+ lastIndex = index2 + 1;
20442
21176
  }
20443
- buffer[0] += str.substring(lastIndex, index);
21177
+ buffer[0] += str.substring(lastIndex, index2);
20444
21178
  };
20445
21179
  resolveCallbackSync = (str) => {
20446
21180
  const callbacks = str.callbacks;
@@ -21316,8 +22050,8 @@ function match(method, path) {
21316
22050
  if (!match3) {
21317
22051
  return [[], emptyParam];
21318
22052
  }
21319
- const index = match3.indexOf("", 1);
21320
- return [matcher[1][index], match3];
22053
+ const index2 = match3.indexOf("", 1);
22054
+ return [matcher[1][index2], match3];
21321
22055
  });
21322
22056
  this.match = match2;
21323
22057
  return match2(method, path);
@@ -21364,7 +22098,7 @@ var init_node = __esm({
21364
22098
  #index;
21365
22099
  #varIndex;
21366
22100
  #children = /* @__PURE__ */ Object.create(null);
21367
- insert(tokens, index, paramMap, context, pathErrorCheckOnly) {
22101
+ insert(tokens, index2, paramMap, context, pathErrorCheckOnly) {
21368
22102
  if (tokens.length === 0) {
21369
22103
  if (this.#index !== void 0) {
21370
22104
  throw PATH_ERROR;
@@ -21372,7 +22106,7 @@ var init_node = __esm({
21372
22106
  if (pathErrorCheckOnly) {
21373
22107
  return;
21374
22108
  }
21375
- this.#index = index;
22109
+ this.#index = index2;
21376
22110
  return;
21377
22111
  }
21378
22112
  const [token, ...restTokens] = tokens;
@@ -21422,7 +22156,7 @@ var init_node = __esm({
21422
22156
  node = this.#children[token] = new _Node();
21423
22157
  }
21424
22158
  }
21425
- node.insert(restTokens, index, paramMap, context, pathErrorCheckOnly);
22159
+ node.insert(restTokens, index2, paramMap, context, pathErrorCheckOnly);
21426
22160
  }
21427
22161
  buildRegExpStr() {
21428
22162
  const childKeys = Object.keys(this.#children).sort(compareKey);
@@ -21454,7 +22188,7 @@ var init_trie = __esm({
21454
22188
  Trie = class {
21455
22189
  #context = { varIndex: 0 };
21456
22190
  #root = new Node();
21457
- insert(path, index, pathErrorCheckOnly) {
22191
+ insert(path, index2, pathErrorCheckOnly) {
21458
22192
  const paramAssoc = [];
21459
22193
  const groups = [];
21460
22194
  for (let i = 0; ; ) {
@@ -21480,7 +22214,7 @@ var init_trie = __esm({
21480
22214
  }
21481
22215
  }
21482
22216
  }
21483
- this.#root.insert(tokens, index, paramAssoc, this.#context, pathErrorCheckOnly);
22217
+ this.#root.insert(tokens, index2, paramAssoc, this.#context, pathErrorCheckOnly);
21484
22218
  return paramAssoc;
21485
22219
  }
21486
22220
  buildRegExp() {
@@ -22074,8 +22808,19 @@ __export(web_exports, {
22074
22808
  dashboardApp: () => dashboardApp,
22075
22809
  default: () => web_default
22076
22810
  });
22077
- import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
22078
- import { dirname as dirname2 } from "path";
22811
+ import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, renameSync } from "fs";
22812
+ import { dirname as dirname3 } from "path";
22813
+ function parsePositiveInteger(value) {
22814
+ if (!/^\d+$/.test(value)) return null;
22815
+ const parsed = Number(value);
22816
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
22817
+ }
22818
+ function parseTaskStatus(value) {
22819
+ return TASK_STATUSES.has(value) ? value : null;
22820
+ }
22821
+ function safeStatus(value) {
22822
+ return value && TASK_STATUSES.has(value) ? value : "unknown";
22823
+ }
22079
22824
  function formatDuration(startAt, endAt) {
22080
22825
  if (!startAt) return "-";
22081
22826
  const start = new Date(startAt).getTime();
@@ -22113,17 +22858,21 @@ function esc(s) {
22113
22858
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
22114
22859
  }
22115
22860
  function readCurrentConfig() {
22116
- if (!existsSync3(CONFIG_PATH)) return {};
22861
+ const configPath = getConfigPath();
22862
+ if (!existsSync4(configPath)) return {};
22117
22863
  try {
22118
- return JSON.parse(readFileSync2(CONFIG_PATH, "utf-8"));
22864
+ return JSON.parse(readFileSync3(configPath, "utf-8"));
22119
22865
  } catch {
22120
22866
  return {};
22121
22867
  }
22122
22868
  }
22123
22869
  function writeConfig(cfg) {
22124
- const dir = dirname2(CONFIG_PATH);
22125
- if (!existsSync3(dir)) mkdirSync2(dir, { recursive: true });
22126
- writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2) + "\n");
22870
+ const configPath = getConfigPath();
22871
+ const dir = dirname3(configPath);
22872
+ if (!existsSync4(dir)) mkdirSync3(dir, { recursive: true });
22873
+ const tempPath = `${configPath}.${process.pid}.tmp`;
22874
+ writeFileSync2(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
22875
+ renameSync(tempPath, configPath);
22127
22876
  }
22128
22877
  function renderLayout(title, activeTab, body) {
22129
22878
  return `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>${title} - SuperTask</title>${SHARED_STYLES}
@@ -22162,11 +22911,11 @@ async function saveConfig(){
22162
22911
  scheduler:{
22163
22912
  enabled:form.se.checked,
22164
22913
  checkIntervalMs:Number(form.si.value),
22165
- catchUp:form.cu.value,
22166
22914
  },
22167
22915
  watchdog:{
22168
22916
  heartbeatTimeoutMs:Number(form.wt.value)*1000,
22169
- cleanupIntervalMs:Number(form.wc.value)*1000,
22917
+ checkIntervalMs:Number(form.wci.value)*1000,
22918
+ cleanupIntervalMs:Number(form.wcl.value)*3600000,
22170
22919
  retentionDays:Number(form.rd.value),
22171
22920
  }
22172
22921
  };
@@ -22197,7 +22946,7 @@ async function saveConfig(){
22197
22946
  <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>
22198
22947
  </body></html>`;
22199
22948
  }
22200
- var app, SHARED_STYLES, dashboardApp, web_default;
22949
+ var app, TASK_STATUSES, SHARED_STYLES, dashboardApp, web_default;
22201
22950
  var init_web = __esm({
22202
22951
  "src/web/index.tsx"() {
22203
22952
  "use strict";
@@ -22209,7 +22958,47 @@ var init_web = __esm({
22209
22958
  init_drizzle_orm();
22210
22959
  init_db2();
22211
22960
  init_config();
22961
+ init_health();
22212
22962
  app = new Hono2();
22963
+ TASK_STATUSES = /* @__PURE__ */ new Set([
22964
+ "pending",
22965
+ "running",
22966
+ "done",
22967
+ "failed",
22968
+ "dead_letter",
22969
+ "cancelled"
22970
+ ]);
22971
+ app.use("*", async (c, next) => {
22972
+ await next();
22973
+ c.header("X-Content-Type-Options", "nosniff");
22974
+ c.header("X-Frame-Options", "DENY");
22975
+ c.header("Referrer-Policy", "no-referrer");
22976
+ });
22977
+ app.use("/api/*", async (c, next) => {
22978
+ if (["GET", "HEAD", "OPTIONS"].includes(c.req.method)) return next();
22979
+ const fetchSite = c.req.header("Sec-Fetch-Site");
22980
+ if (fetchSite && !["same-origin", "none"].includes(fetchSite)) {
22981
+ return c.json({ error: "cross-site request rejected" }, 403);
22982
+ }
22983
+ const origin = c.req.header("Origin");
22984
+ if (origin) {
22985
+ try {
22986
+ const originUrl = new URL(origin);
22987
+ const requestUrl = new URL(c.req.url);
22988
+ const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(originUrl.hostname);
22989
+ if (!loopback || originUrl.origin !== requestUrl.origin) {
22990
+ return c.json({ error: "cross-site request rejected" }, 403);
22991
+ }
22992
+ } catch {
22993
+ return c.json({ error: "invalid origin" }, 403);
22994
+ }
22995
+ }
22996
+ return next();
22997
+ });
22998
+ app.get("/health", (c) => {
22999
+ const health = getGatewayHealth();
23000
+ return c.json(health, health.status === "ok" ? 200 : 503);
23001
+ });
22213
23002
  SHARED_STYLES = html`
22214
23003
  <style>
22215
23004
  :root { --bg:#0d1117; --card:#161b22; --border:#30363d; --t1:#c9d1d9; --t2:#8b949e; --green:#238636; --red:#da3633; --yellow:#d29922; --blue:#1f6feb; --purple:#8957e5; }
@@ -22286,12 +23075,16 @@ var init_web = __esm({
22286
23075
  </style>
22287
23076
  `;
22288
23077
  app.get("/", async (c) => {
22289
- const page = Number(c.req.query("page") || "1");
23078
+ const pageParam = c.req.query("page") || "1";
23079
+ const page = parsePositiveInteger(pageParam);
23080
+ if (page === null) return c.text("invalid page", 400);
22290
23081
  const statusFilter = c.req.query("status") || "";
23082
+ const parsedStatus = statusFilter ? parseTaskStatus(statusFilter) : null;
23083
+ if (statusFilter && !parsedStatus) return c.text("invalid status", 400);
22291
23084
  const limit = 50;
22292
23085
  const offset = (page - 1) * limit;
22293
23086
  const [tasks3, statsData] = await Promise.all([
22294
- TaskService.list({ limit, offset, ...statusFilter ? { status: statusFilter } : {} }),
23087
+ TaskService.list({ limit, offset, ...parsedStatus ? { status: parsedStatus } : {} }),
22295
23088
  TaskService.stats({})
22296
23089
  ]);
22297
23090
  const taskIds = tasks3.map((t) => t.id);
@@ -22314,12 +23107,13 @@ var init_web = __esm({
22314
23107
  </div>`;
22315
23108
  let rows = "";
22316
23109
  for (const task of tasks3) {
22317
- const st = (task.status ?? "").toUpperCase();
23110
+ const status = safeStatus(task.status);
23111
+ const st = status.toUpperCase();
22318
23112
  rows += `<tr>
22319
23113
  <td class="mu">#${task.id}</td>
22320
23114
  <td><div style="font-weight:500">${esc(task.name)}</div><div class="mu sm el">${esc(task.prompt.substring(0, 120))}</div></td>
22321
23115
  <td><span class="tag">${esc(task.agent)}</span></td>
22322
- <td><span class="badge b-${task.status}">${st}</span></td>
23116
+ <td><span class="badge b-${status}">${st}</span></td>
22323
23117
  <td class="sm ${task.status === "running" ? "" : "mu"}">${formatDuration(task.startedAt, task.finishedAt)}</td>
22324
23118
  <td class="mu sm">${(task.retryCount ?? 0) > 0 ? task.retryCount : "-"}</td>
22325
23119
  <td>
@@ -22351,21 +23145,13 @@ var init_web = __esm({
22351
23145
  });
22352
23146
  app.get("/templates", async (c) => {
22353
23147
  const templates = await TaskTemplateService.list(100);
22354
- for (const tmpl of templates) {
22355
- if (tmpl.enabled && tmpl.scheduleType === "cron" && tmpl.cronExpr) {
22356
- try {
22357
- const { getNextCronRun: getNextCronRun2 } = await Promise.resolve().then(() => (init_cron_parser(), cron_parser_exports));
22358
- tmpl.nextRunAt = getNextCronRun2(tmpl.cronExpr, Date.now());
22359
- } catch {
22360
- }
22361
- }
22362
- }
22363
23148
  const enabled = templates.filter((t) => t.enabled).length;
22364
23149
  const disabled = templates.length - enabled;
22365
23150
  let rows = "";
22366
23151
  for (const t of templates) {
22367
- const typeLabel = t.scheduleType === "cron" ? "Cron" : t.scheduleType === "recurring" ? "\u5FAA\u73AF" : "\u5B9A\u65F6";
22368
- const typeClass = "tag t-" + t.scheduleType;
23152
+ const scheduleType = ["cron", "recurring", "delayed"].includes(t.scheduleType) ? t.scheduleType : "unknown";
23153
+ const typeLabel = scheduleType === "cron" ? "Cron" : scheduleType === "recurring" ? "\u5FAA\u73AF" : scheduleType === "delayed" ? "\u5B9A\u65F6" : "\u672A\u77E5";
23154
+ const typeClass = "tag t-" + scheduleType;
22369
23155
  let rule = "-";
22370
23156
  if (t.scheduleType === "cron") rule = t.cronExpr || "-";
22371
23157
  else if (t.scheduleType === "recurring") rule = t.intervalMs ? `${Math.floor(t.intervalMs / 6e4)}\u5206\u949F` : "-";
@@ -22377,7 +23163,7 @@ var init_web = __esm({
22377
23163
  <td><div style="font-weight:500">${esc(t.name)}</div><div class="mu sm el">${esc(t.prompt.substring(0, 100))}</div>
22378
23164
  <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>
22379
23165
  <td><span class="${typeClass}">${typeLabel}</span></td>
22380
- <td class="m sm">${rule}</td>
23166
+ <td class="m sm">${esc(rule)}</td>
22381
23167
  <td>${statusBadge}</td>
22382
23168
  <td class="sm">${t.lastRunAt ? timeAgo(t.lastRunAt) : "-"}</td>
22383
23169
  <td class="sm">${t.nextRunAt ? timeUntil(t.nextRunAt) : "-"}</td>
@@ -22405,7 +23191,8 @@ var init_web = __esm({
22405
23191
  return c.html(renderLayout("\u5B9A\u65F6\u4EFB\u52A1", "templates", body));
22406
23192
  });
22407
23193
  app.get("/runs", async (c) => {
22408
- const page = Number(c.req.query("page") || "1");
23194
+ const page = parsePositiveInteger(c.req.query("page") || "1");
23195
+ if (page === null) return c.text("invalid page", 400);
22409
23196
  const limit = 50;
22410
23197
  const offset = (page - 1) * limit;
22411
23198
  const { taskRuns: tr, tasks: tk } = schema_exports;
@@ -22423,13 +23210,14 @@ var init_web = __esm({
22423
23210
  childPid: tr.childPid,
22424
23211
  taskName: tk.name,
22425
23212
  taskAgent: tk.agent
22426
- }).from(tr).innerJoin(tk, eq(tr.taskId, tk.id)).orderBy(desc(tr.startedAt)).limit(limit).offset(offset);
23213
+ }).from(tr).innerJoin(tk, eq(tr.taskId, tk.id)).orderBy(desc(tr.startedAt), desc(tr.id)).limit(limit).offset(offset);
22427
23214
  const totalResult = await db.select({ count: sql`count(*)` }).from(tr);
22428
23215
  const total = Number(totalResult[0]?.count ?? 0);
22429
23216
  const totalPages = Math.ceil(total / limit);
22430
23217
  let rows = "";
22431
23218
  const logsHtml = [];
22432
23219
  for (const run of runs) {
23220
+ const status = safeStatus(run.status);
22433
23221
  const shortSession = run.sessionId ? run.sessionId.slice(4, 7) + "***" + run.sessionId.slice(-3) : "-";
22434
23222
  const logBtn = run.log ? `<button class="btn btn-sm" onclick="toggleLog(${run.id})">\u65E5\u5FD7</button>` : "";
22435
23223
  rows += `<tr>
@@ -22437,15 +23225,15 @@ var init_web = __esm({
22437
23225
  <td><div style="font-weight:500">${esc(run.taskName)} <span class="mu">(#${run.taskId})</span></div>
22438
23226
  ${run.model ? `<div class="sm"><span class="tag">${esc(run.model)}</span></div>` : ""}</td>
22439
23227
  <td><span class="tag">${esc(run.taskAgent)}</span></td>
22440
- <td><span class="badge b-${run.status}">${(run.status ?? "").toUpperCase()}</span></td>
23228
+ <td><span class="badge b-${status}">${status.toUpperCase()}</span></td>
22441
23229
  <td class="sm">${formatDuration(run.startedAt, run.finishedAt)}</td>
22442
23230
  <td class="sm mu">${run.heartbeatAt ? timeAgo(run.heartbeatAt) : "-"}</td>
22443
23231
  <td><button class="btn btn-sm" onclick="showRunDetail(${run.id})">\u8BE6\u60C5</button>${logBtn}</td>
22444
23232
  </tr>`;
22445
23233
  if (run.log) {
22446
23234
  logsHtml.push(`<div id="log-${run.id}" style="display:none" class="mt8">
22447
- <div class="panel"><div class="ph"><h3>Run #${run.id} \u65E5\u5FD7 \u2014 ${run.taskName}</h3></div>
22448
- <div class="log-box">${run.log.replace(/</g, "&lt;")}</div></div></div>`);
23235
+ <div class="panel"><div class="ph"><h3>Run #${run.id} \u65E5\u5FD7 \u2014 ${esc(run.taskName)}</h3></div>
23236
+ <div class="log-box">${esc(run.log)}</div></div></div>`);
22449
23237
  }
22450
23238
  }
22451
23239
  let paging = `<div class="pn">`;
@@ -22471,17 +23259,18 @@ var init_web = __esm({
22471
23259
  });
22472
23260
  app.get("/system", async (c) => {
22473
23261
  const config = loadConfig();
23262
+ const configPath = getConfigPath();
22474
23263
  const stats = await TaskService.stats({});
22475
23264
  const runningRuns = await TaskRunService.getAllRunningRuns();
22476
23265
  const templates = await TaskTemplateService.list(100);
22477
- const configExists = existsSync3(CONFIG_PATH);
23266
+ const configExists = existsSync4(configPath);
22478
23267
  let runRows = "";
22479
23268
  if (runningRuns.length > 0) {
22480
23269
  for (const run of runningRuns) {
22481
23270
  const shortS = run.sessionId ? run.sessionId.slice(4, 7) + "***" + run.sessionId.slice(-3) : "-";
22482
23271
  runRows += `<tr>
22483
23272
  <td class="mu">#${run.id}</td><td>#${run.taskId}</td>
22484
- <td class="m sm">${shortS}</td>
23273
+ <td class="m sm">${esc(shortS)}</td>
22485
23274
  <td class="sm">${esc(run.model) || "-"}</td>
22486
23275
  <td class="sm">${formatDate(run.startedAt)}</td>
22487
23276
  <td class="sm">${run.heartbeatAt ? timeAgo(run.heartbeatAt) : "-"}</td>
@@ -22506,19 +23295,14 @@ var init_web = __esm({
22506
23295
  <h3 style="margin:0 0 12px;font-size:14px">Scheduler \u914D\u7F6E</h3>
22507
23296
  <div class="form-row"><label>\u542F\u7528\u8C03\u5EA6</label><input type="checkbox" name="se" ${config.scheduler.enabled ? "checked" : ""}></div>
22508
23297
  <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>
22509
- <div class="form-row"><label>\u8FFD\u8D76\u6A21\u5F0F</label><select name="cu" style="width:100px">
22510
- <option value="next" ${config.scheduler.catchUp === "next" ? "selected" : ""}>next</option>
22511
- <option value="all" ${config.scheduler.catchUp === "all" ? "selected" : ""}>all</option>
22512
- <option value="latest" ${config.scheduler.catchUp === "latest" ? "selected" : ""}>latest</option>
22513
- </select></div>
22514
23298
  <div class="ir"><span class="ik">\u6D3B\u8DC3\u6A21\u677F</span><span class="iv">${templates.filter((t) => t.enabled).length} / ${templates.length}</span></div>
22515
23299
  </div>
22516
23300
  <div class="card">
22517
23301
  <h3 style="margin:0 0 12px;font-size:14px">Watchdog \u914D\u7F6E</h3>
22518
23302
  <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>
22519
- <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>
23303
+ <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>
23304
+ <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>
22520
23305
  <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>
22521
- <div class="ir"><span class="ik">\u65E5\u5FD7\u683C\u5F0F</span><span class="iv">${config.logging.format}</span></div>
22522
23306
  </div>
22523
23307
  </div>
22524
23308
  <div style="text-align:center;margin-bottom:24px">
@@ -22547,7 +23331,7 @@ var init_web = __esm({
22547
23331
 
22548
23332
  <div class="card mt16">
22549
23333
  <h3 style="margin:0 0 12px;font-size:14px">\u914D\u7F6E\u6587\u4EF6</h3>
22550
- <div class="ir"><span class="ik">\u8DEF\u5F84</span><span class="iv m sm">${CONFIG_PATH}</span></div>
23334
+ <div class="ir"><span class="ik">\u8DEF\u5F84</span><span class="iv m sm">${esc(configPath)}</span></div>
22551
23335
  <div class="ir"><span class="ik">\u6587\u4EF6\u5B58\u5728</span><span class="iv">${configFileStatus}</span></div>
22552
23336
  </div>
22553
23337
 
@@ -22559,46 +23343,61 @@ var init_web = __esm({
22559
23343
  return c.html(renderLayout("\u7CFB\u7EDF\u72B6\u6001", "system", body));
22560
23344
  });
22561
23345
  app.get("/api/tasks/:id", async (c) => {
22562
- const id = Number(c.req.param("id"));
23346
+ const id = parsePositiveInteger(c.req.param("id"));
23347
+ if (id === null) return c.json({ error: "invalid id" }, 400);
22563
23348
  const task = await TaskService.getById(id);
22564
23349
  if (!task) return c.json({ error: "not found" }, 404);
22565
23350
  const runs = await TaskRunService.listByTaskId(id);
22566
23351
  return c.json({ ...task, _runs: runs });
22567
23352
  });
22568
23353
  app.get("/api/runs/:id", async (c) => {
22569
- const id = Number(c.req.param("id"));
23354
+ const id = parsePositiveInteger(c.req.param("id"));
23355
+ if (id === null) return c.json({ error: "invalid id" }, 400);
22570
23356
  const run = await TaskRunService.getById(id);
22571
23357
  if (!run) return c.json({ error: "not found" }, 404);
22572
23358
  return c.json(run);
22573
23359
  });
22574
23360
  app.get("/api/templates/:id", async (c) => {
22575
- const id = Number(c.req.param("id"));
23361
+ const id = parsePositiveInteger(c.req.param("id"));
23362
+ if (id === null) return c.json({ error: "invalid id" }, 400);
22576
23363
  const tmpl = await TaskTemplateService.getById(id);
22577
23364
  if (!tmpl) return c.json({ error: "not found" }, 404);
22578
23365
  return c.json(tmpl);
22579
23366
  });
22580
23367
  app.post("/api/tasks/:id/retry", async (c) => {
22581
- await TaskService.retry(Number(c.req.param("id")));
22582
- return c.json({ success: true });
23368
+ const id = parsePositiveInteger(c.req.param("id"));
23369
+ if (id === null) return c.json({ error: "invalid id" }, 400);
23370
+ const task = await TaskService.retry(id);
23371
+ if (task) return c.json({ success: true });
23372
+ return await TaskService.getById(id) ? c.json({ error: "task status does not allow retry" }, 409) : c.json({ error: "not found" }, 404);
22583
23373
  });
22584
23374
  app.delete("/api/tasks/:id", async (c) => {
22585
- await TaskService.delete(Number(c.req.param("id")));
22586
- return c.json({ success: true });
23375
+ const id = parsePositiveInteger(c.req.param("id"));
23376
+ if (id === null) return c.json({ error: "invalid id" }, 400);
23377
+ const deleted = await TaskService.delete(id);
23378
+ return deleted ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
22587
23379
  });
22588
23380
  app.post("/api/templates/:id/enable", async (c) => {
22589
- const result = await TaskTemplateService.enable(Number(c.req.param("id")));
22590
- return c.json({ success: !!result });
23381
+ const id = parsePositiveInteger(c.req.param("id"));
23382
+ if (id === null) return c.json({ error: "invalid id" }, 400);
23383
+ const result = await TaskTemplateService.enable(id);
23384
+ return result ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
22591
23385
  });
22592
23386
  app.post("/api/templates/:id/disable", async (c) => {
22593
- const result = await TaskTemplateService.disable(Number(c.req.param("id")));
22594
- return c.json({ success: !!result });
23387
+ const id = parsePositiveInteger(c.req.param("id"));
23388
+ if (id === null) return c.json({ error: "invalid id" }, 400);
23389
+ const result = await TaskTemplateService.disable(id);
23390
+ return result ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
22595
23391
  });
22596
23392
  app.delete("/api/templates/:id", async (c) => {
22597
- const ok = await TaskTemplateService.delete(Number(c.req.param("id")));
22598
- return c.json({ success: ok });
23393
+ const id = parsePositiveInteger(c.req.param("id"));
23394
+ if (id === null) return c.json({ error: "invalid id" }, 400);
23395
+ const ok = await TaskTemplateService.delete(id);
23396
+ return ok ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
22599
23397
  });
22600
23398
  app.post("/api/templates/:id/trigger", async (c) => {
22601
- const id = Number(c.req.param("id"));
23399
+ const id = parsePositiveInteger(c.req.param("id"));
23400
+ if (id === null) return c.json({ error: "invalid id" }, 400);
22602
23401
  const tmpl = await TaskTemplateService.getById(id);
22603
23402
  if (!tmpl) return c.json({ error: "not found" }, 404);
22604
23403
  const task = await TaskService.add({
@@ -22610,7 +23409,10 @@ var init_web = __esm({
22610
23409
  category: tmpl.category,
22611
23410
  importance: tmpl.importance,
22612
23411
  urgency: tmpl.urgency,
23412
+ batchId: tmpl.batchId,
22613
23413
  maxRetries: tmpl.maxRetries,
23414
+ retryBackoffMs: tmpl.retryBackoffMs,
23415
+ timeoutMs: tmpl.timeoutMs,
22614
23416
  templateId: tmpl.id
22615
23417
  });
22616
23418
  return c.json({ success: true, taskId: task.id });
@@ -22625,22 +23427,29 @@ var init_web = __esm({
22625
23427
  const bW = body.worker ?? {};
22626
23428
  const bS = body.scheduler ?? {};
22627
23429
  const bD = body.watchdog ?? {};
22628
- const merged = { ...current, ...body, worker: { ...curW, ...bW }, scheduler: { ...curS, ...bS }, watchdog: { ...curD, ...bD } };
22629
- writeConfig(merged);
23430
+ const merged = {
23431
+ ...current,
23432
+ ...body,
23433
+ configVersion: 2,
23434
+ worker: { ...curW, ...bW },
23435
+ scheduler: { ...curS, ...bS },
23436
+ watchdog: { ...curD, ...bD }
23437
+ };
23438
+ writeConfig(validateConfig(merged));
22630
23439
  return c.json({ success: true });
22631
23440
  } catch (err) {
22632
- return c.json({ success: false, error: err instanceof Error ? err.message : String(err) });
23441
+ return c.json({ success: false, error: err instanceof Error ? err.message : String(err) }, 400);
22633
23442
  }
22634
23443
  });
22635
23444
  app.post("/api/database/clear", async (c) => {
22636
23445
  try {
22637
- const { tasks: tasks3, taskRuns: taskRuns3, taskTemplates: taskTemplates4 } = schema_exports;
22638
- await db.delete(taskRuns3);
23446
+ const { tasks: tasks3, taskRuns: taskRuns4, taskTemplates: taskTemplates4 } = schema_exports;
23447
+ await db.delete(taskRuns4);
22639
23448
  await db.delete(taskTemplates4);
22640
23449
  await db.delete(tasks3);
22641
23450
  return c.json({ success: true });
22642
23451
  } catch (err) {
22643
- return c.json({ success: false, error: err instanceof Error ? err.message : String(err) });
23452
+ return c.json({ success: false, error: err instanceof Error ? err.message : String(err) }, 500);
22644
23453
  }
22645
23454
  });
22646
23455
  dashboardApp = app;
@@ -22676,7 +23485,7 @@ function acquireLock() {
22676
23485
  sqlite.exec("DELETE FROM gateway_lock WHERE id = 1");
22677
23486
  }
22678
23487
  sqlite.exec(
22679
- "INSERT INTO gateway_lock (id, pid, acquired_at, heartbeat_at) VALUES (1, ?, ?, ?)",
23488
+ "INSERT INTO gateway_lock (id, pid, acquired_at, heartbeat_at, ready_at) VALUES (1, ?, ?, ?, NULL)",
22680
23489
  [pid, now, now]
22681
23490
  );
22682
23491
  sqlite.exec("COMMIT");
@@ -22710,6 +23519,18 @@ function updateLockHeartbeat() {
22710
23519
  } catch {
22711
23520
  }
22712
23521
  }
23522
+ function markGatewayReady() {
23523
+ sqlite.exec(
23524
+ "UPDATE gateway_lock SET heartbeat_at = ?, ready_at = ? WHERE pid = ?",
23525
+ [Date.now(), Date.now(), process.pid]
23526
+ );
23527
+ }
23528
+ function markGatewayNotReady() {
23529
+ try {
23530
+ sqlite.exec("UPDATE gateway_lock SET ready_at = NULL WHERE pid = ?", [process.pid]);
23531
+ } catch {
23532
+ }
23533
+ }
22713
23534
  async function main() {
22714
23535
  console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: "SuperTask Gateway starting", pid: process.pid }));
22715
23536
  if (!acquireLock()) {
@@ -22721,20 +23542,21 @@ async function main() {
22721
23542
  const worker = new WorkerEngine(cfg);
22722
23543
  const watchdog = new Watchdog(cfg);
22723
23544
  const scheduler = new Scheduler(cfg);
23545
+ initializeGatewayHealth({
23546
+ workerPollIntervalMs: cfg.worker.pollIntervalMs,
23547
+ schedulerEnabled: cfg.scheduler.enabled,
23548
+ schedulerCheckIntervalMs: cfg.scheduler.checkIntervalMs,
23549
+ watchdogCheckIntervalMs: cfg.watchdog.checkIntervalMs
23550
+ });
22724
23551
  worker.start();
22725
23552
  watchdog.start();
22726
23553
  await scheduler.start();
22727
23554
  if (cfg.dashboard.enabled) {
22728
23555
  const { dashboardApp: dashboardApp2 } = await Promise.resolve().then(() => (init_web(), web_exports));
22729
23556
  Bun.serve({
23557
+ hostname: "127.0.0.1",
22730
23558
  port: cfg.dashboard.port,
22731
- fetch(req) {
22732
- const url = new URL(req.url);
22733
- if (url.hostname !== "localhost" && url.hostname !== "127.0.0.1" && url.hostname !== "::1") {
22734
- return new Response("Forbidden", { status: 403 });
22735
- }
22736
- return dashboardApp2.fetch(req);
22737
- }
23559
+ fetch: dashboardApp2.fetch
22738
23560
  });
22739
23561
  console.log(JSON.stringify({
22740
23562
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -22743,6 +23565,7 @@ async function main() {
22743
23565
  url: `http://localhost:${cfg.dashboard.port}`
22744
23566
  }));
22745
23567
  }
23568
+ markGatewayReady();
22746
23569
  console.log(JSON.stringify({
22747
23570
  ts: (/* @__PURE__ */ new Date()).toISOString(),
22748
23571
  level: "info",
@@ -22756,10 +23579,10 @@ async function main() {
22756
23579
  shuttingDown = true;
22757
23580
  console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: `received ${signal}, shutting down...` }));
22758
23581
  clearInterval(heartbeatTimer);
23582
+ markGatewayNotReady();
22759
23583
  scheduler.stop();
22760
23584
  watchdog.stop();
22761
- const runningIds = worker.getRunningTaskIds();
22762
- await worker.stop();
23585
+ const runningIds = await worker.stop(cfg.worker.shutdownGracePeriodMs);
22763
23586
  if (runningIds.length > 0) {
22764
23587
  const resetCount = await TaskService.resetRunningToPending(runningIds);
22765
23588
  console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: "reset running tasks to pending", count: resetCount }));
@@ -22769,6 +23592,7 @@ async function main() {
22769
23592
  await TaskRunService.fail(run.id, "Gateway shutdown");
22770
23593
  }
22771
23594
  releaseLock();
23595
+ resetGatewayHealth();
22772
23596
  closeDb();
22773
23597
  console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: "Gateway stopped" }));
22774
23598
  process.exit(0);
@@ -22796,6 +23620,7 @@ var init_gateway = __esm({
22796
23620
  init_db2();
22797
23621
  init_task_service();
22798
23622
  init_task_run_service();
23623
+ init_health();
22799
23624
  STALE_THRESHOLD_MS = 3e4;
22800
23625
  if (import.meta.main) {
22801
23626
  main();
@@ -22803,247 +23628,87 @@ var init_gateway = __esm({
22803
23628
  }
22804
23629
  });
22805
23630
 
22806
- // src/daemon/pm2.ts
22807
- var pm2_exports = {};
22808
- __export(pm2_exports, {
22809
- ensureGateway: () => ensureGateway,
22810
- install: () => install,
22811
- isGatewayRunning: () => isGatewayRunning,
22812
- uninstall: () => uninstall,
22813
- upgrade: () => upgrade
23631
+ // src/daemon/update.ts
23632
+ var update_exports = {};
23633
+ __export(update_exports, {
23634
+ installLatestPlugin: () => installLatestPlugin,
23635
+ resolveInstalledPlugin: () => resolveInstalledPlugin
22814
23636
  });
22815
- import { execSync, spawnSync } from "child_process";
22816
- import { join as join3, dirname as dirname3 } from "path";
22817
- import { fileURLToPath as fileURLToPath2 } from "url";
22818
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync4 } from "fs";
22819
- import { homedir as homedir3 } from "os";
22820
- function getPackageVersion() {
22821
- try {
22822
- const pkgPath = join3(__dirname, "../package.json");
22823
- const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
22824
- return pkg.version || "0.0.0";
22825
- } catch {
22826
- return "0.0.0";
22827
- }
22828
- }
22829
- function getRunningVersion() {
23637
+ import { spawnSync as spawnSync3 } from "child_process";
23638
+ import { existsSync as existsSync5, readFileSync as readFileSync4, readdirSync } from "fs";
23639
+ import { homedir as homedir4 } from "os";
23640
+ import { join as join4 } from "path";
23641
+ function pluginAt(packageDir) {
23642
+ const packageJson = join4(packageDir, "package.json");
23643
+ const gatewayEntry = join4(packageDir, "dist/gateway/index.js");
23644
+ if (!existsSync5(packageJson) || !existsSync5(gatewayEntry)) return null;
22830
23645
  try {
22831
- if (!existsSync4(VERSION_FILE)) return null;
22832
- return readFileSync3(VERSION_FILE, "utf-8").trim() || null;
23646
+ const pkg = JSON.parse(readFileSync4(packageJson, "utf8"));
23647
+ if (pkg.name !== PACKAGE_NAME || typeof pkg.version !== "string") return null;
23648
+ return { packageDir, gatewayEntry, version: pkg.version };
22833
23649
  } catch {
22834
23650
  return null;
22835
23651
  }
22836
23652
  }
22837
- function writeRunningVersion(version2) {
22838
- try {
22839
- writeFileSync2(VERSION_FILE, version2, "utf-8");
22840
- } catch {
22841
- }
22842
- }
22843
- function pm2Bin() {
22844
- return process.platform === "win32" ? "pm2.cmd" : "pm2";
22845
- }
22846
- function isPm2Installed() {
22847
- try {
22848
- const cmd = process.platform === "win32" ? "where pm2" : "which pm2";
22849
- execSync(cmd, { stdio: "pipe" });
22850
- return true;
22851
- } catch {
22852
- return false;
22853
- }
22854
- }
22855
- function installPm2() {
22856
- console.log("[supertask] Installing pm2...");
22857
- try {
22858
- execSync("npm install -g pm2", { stdio: "inherit" });
22859
- return true;
22860
- } catch {
22861
- try {
22862
- execSync("bun install -g pm2", { stdio: "inherit" });
22863
- return true;
22864
- } catch {
22865
- return false;
22866
- }
22867
- }
22868
- }
22869
- function pm2Exec(args) {
22870
- const bin = pm2Bin();
22871
- try {
22872
- const result = spawnSync(bin, args, {
22873
- stdio: ["pipe", "pipe", "pipe"],
22874
- encoding: "utf-8",
22875
- shell: process.platform === "win32"
22876
- });
22877
- const output = (result.stdout ?? "") + (result.stderr ?? "");
22878
- return { ok: result.status === 0, output };
22879
- } catch (err) {
22880
- return { ok: false, output: err instanceof Error ? err.message : String(err) };
22881
- }
22882
- }
22883
- function pm2JsonList() {
22884
- const { ok, output } = pm2Exec(["jlist"]);
22885
- if (!ok) return [];
22886
- try {
22887
- return JSON.parse(output);
22888
- } catch {
22889
- return [];
22890
- }
22891
- }
22892
- function isGatewayRunning() {
22893
- const list = pm2JsonList();
22894
- const proc = list.find((p) => p.name === PROCESS_NAME);
22895
- if (!proc) return false;
22896
- return proc.pm2_env?.status === "online";
23653
+ function versionParts(version2) {
23654
+ const match2 = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(version2);
23655
+ return match2 ? match2.slice(1).map(Number) : null;
22897
23656
  }
22898
- function findBunPath() {
22899
- try {
22900
- const cmd = process.platform === "win32" ? "where bun" : "which bun";
22901
- return execSync(cmd, { stdio: "pipe" }).toString().trim().split("\n")[0];
22902
- } catch {
22903
- return process.execPath;
23657
+ function compareVersions(left, right) {
23658
+ const a = versionParts(left);
23659
+ const b = versionParts(right);
23660
+ if (!a || !b) return left.localeCompare(right);
23661
+ for (let index2 = 0; index2 < 3; index2 += 1) {
23662
+ if (a[index2] !== b[index2]) return a[index2] - b[index2];
22904
23663
  }
23664
+ return 0;
22905
23665
  }
22906
- function pm2StartGateway(version2) {
22907
- const bunPath = findBunPath();
22908
- return pm2Exec([
22909
- "start",
22910
- bunPath,
22911
- "--name",
22912
- PROCESS_NAME,
22913
- "--interpreter",
22914
- "none",
22915
- "--restart-delay",
22916
- "5000",
22917
- "--max-restarts",
22918
- "30",
22919
- "--",
22920
- GATEWAY_ENTRY
22921
- ]);
23666
+ function cacheRoot() {
23667
+ return process.env.SUPERTASK_OPENCODE_CACHE_DIR ?? join4(homedir4(), ".cache/opencode/packages");
22922
23668
  }
22923
- function install() {
22924
- if (!isPm2Installed()) {
22925
- if (!installPm2()) {
22926
- throw new Error("[supertask] Failed to install pm2. Please install it manually: npm install -g pm2");
22927
- }
23669
+ function resolveInstalledPlugin() {
23670
+ const override = process.env.SUPERTASK_PLUGIN_PACKAGE_DIR;
23671
+ if (override) {
23672
+ const plugin = pluginAt(override);
23673
+ if (!plugin) throw new Error(`[supertask] \u5B89\u88C5\u5305\u65E0\u6548\u6216\u7F3A\u5C11 Gateway \u6784\u5EFA\u4EA7\u7269: ${override}`);
23674
+ return plugin;
22928
23675
  }
22929
- console.log("[supertask] pm2 ready");
22930
- const list = pm2JsonList();
22931
- const existing = list.find((p) => p.name === PROCESS_NAME);
22932
- if (existing) {
22933
- console.log("[supertask] Gateway process already registered, reloading...");
22934
- const { ok } = pm2Exec(["reload", PROCESS_NAME]);
22935
- if (!ok) {
22936
- console.error("[supertask] pm2 reload failed, trying restart...");
22937
- pm2Exec(["restart", PROCESS_NAME]);
22938
- }
22939
- } else {
22940
- console.log("[supertask] Starting Gateway with pm2...");
22941
- const version2 = getPackageVersion();
22942
- const { ok, output } = pm2StartGateway(version2);
22943
- if (!ok) {
22944
- throw new Error(`[supertask] pm2 start failed: ${output}`);
22945
- }
22946
- writeRunningVersion(version2);
22947
- }
22948
- pm2Exec(["save"]);
22949
- console.log("[supertask] Configuring startup...");
22950
- const { ok: startupOk, output: startupOutput } = pm2Exec(["startup"]);
22951
- if (!startupOk) {
22952
- if (startupOutput.includes("sudo") || startupOutput.includes("run as root")) {
22953
- console.log("[supertask] pm2 startup requires elevated permissions.");
22954
- console.log("[supertask] Run the command shown above, or manually execute:");
22955
- console.log(` pm2 startup`);
22956
- console.log(` pm2 save`);
22957
- } else if (process.platform === "win32") {
22958
- console.log("[supertask] On Windows, use pm2-installer for startup:");
22959
- console.log(" npm install -g pm2-windows-startup");
22960
- console.log(" pm2-startup install");
22961
- }
23676
+ const root = cacheRoot();
23677
+ 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)) : [];
23678
+ const installed = packageDirs.map(pluginAt).filter((plugin) => plugin !== null).sort((left, right) => compareVersions(right.version, left.version));
23679
+ if (!installed[0]) {
23680
+ throw new Error(`[supertask] OpenCode \u63D2\u4EF6\u7F13\u5B58\u4E2D\u627E\u4E0D\u5230\u53EF\u8FD0\u884C\u7684 ${PACKAGE_NAME}`);
22962
23681
  }
22963
- console.log("\n[supertask] Gateway installed and running!");
22964
- console.log("[supertask] Manage with: pm2 status / pm2 logs supertask-gateway");
23682
+ return installed[0];
22965
23683
  }
22966
- function uninstall() {
22967
- console.log("[supertask] Stopping Gateway...");
22968
- pm2Exec(["stop", PROCESS_NAME]);
22969
- console.log("[supertask] Removing Gateway from pm2...");
22970
- pm2Exec(["delete", PROCESS_NAME]);
22971
- pm2Exec(["save"]);
22972
- console.log("\n[supertask] Gateway removed from pm2.");
22973
- console.log("[supertask] Note: pm2 startup config was not removed (you may have other pm2 processes).");
22974
- console.log("[supertask] To fully remove pm2 startup: pm2 unstartup");
23684
+ function opencodeBin() {
23685
+ return process.env.SUPERTASK_OPENCODE_BIN ?? "opencode";
22975
23686
  }
22976
- function upgrade() {
22977
- const before = getRunningVersion();
22978
- const currentVersion = getPackageVersion();
22979
- const list = pm2JsonList();
22980
- const proc = list.find((p) => p.name === PROCESS_NAME);
22981
- if (proc) {
22982
- console.log(`[supertask] Stopping Gateway (version ${before ?? "unknown"})...`);
22983
- pm2Exec(["delete", PROCESS_NAME]);
22984
- }
22985
- console.log(`[supertask] Starting Gateway (version ${currentVersion})...`);
22986
- const { ok } = pm2StartGateway(currentVersion);
22987
- if (ok) {
22988
- writeRunningVersion(currentVersion);
22989
- pm2Exec(["save"]);
22990
- console.log(`[supertask] Gateway upgraded: ${before ?? "unknown"} \u2192 ${currentVersion}`);
22991
- } else {
22992
- throw new Error(`[supertask] Failed to start Gateway after upgrade`);
22993
- }
22994
- return { before, after: currentVersion, restarted: true };
22995
- }
22996
- function ensureGateway() {
22997
- const currentVersion = getPackageVersion();
22998
- try {
22999
- const list = pm2JsonList();
23000
- const proc = list.find((p) => p.name === PROCESS_NAME);
23001
- if (proc && proc.pm2_env?.status === "online") {
23002
- const runningVersion = getRunningVersion();
23003
- if (runningVersion === currentVersion) {
23004
- return;
23005
- }
23006
- console.log(`[supertask] Version changed: ${runningVersion ?? "unknown"} \u2192 ${currentVersion}, reloading Gateway...`);
23007
- pm2Exec(["delete", PROCESS_NAME]);
23008
- const { ok } = pm2StartGateway(currentVersion);
23009
- if (ok) writeRunningVersion(currentVersion);
23010
- pm2Exec(["save"]);
23011
- return;
23012
- }
23013
- } catch {
23014
- }
23015
- if (!isPm2Installed()) {
23016
- console.log("[supertask] Installing pm2 for Gateway process management...");
23017
- try {
23018
- execSync("npm install -g pm2", { stdio: "pipe" });
23019
- } catch {
23020
- try {
23021
- execSync("bun install -g pm2", { stdio: "pipe" });
23022
- } catch {
23023
- console.warn("[supertask] Could not install pm2. Gateway will not auto-start. Run `supertask install` manually.");
23024
- return;
23025
- }
23026
- }
23687
+ function installLatestPlugin() {
23688
+ const result = spawnSync3(opencodeBin(), [
23689
+ "plugin",
23690
+ `${PACKAGE_NAME}@latest`,
23691
+ "--global",
23692
+ "--force"
23693
+ ], {
23694
+ encoding: "utf8",
23695
+ env: process.env,
23696
+ timeout: 12e4
23697
+ });
23698
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
23699
+ if (result.error) {
23700
+ throw new Error(`[supertask] OpenCode \u63D2\u4EF6\u66F4\u65B0\u5931\u8D25: ${result.error.message}`);
23027
23701
  }
23028
- const pm2List = pm2JsonList();
23029
- const existing = pm2List.find((p) => p.name === PROCESS_NAME);
23030
- if (existing) {
23031
- pm2Exec(["restart", PROCESS_NAME]);
23032
- } else {
23033
- const version2 = getPackageVersion();
23034
- const { ok } = pm2StartGateway(version2);
23035
- if (ok) writeRunningVersion(version2);
23702
+ if (result.status !== 0) {
23703
+ throw new Error(`[supertask] OpenCode \u63D2\u4EF6\u66F4\u65B0\u5931\u8D25: ${output || `\u9000\u51FA\u7801 ${result.status}`}`);
23036
23704
  }
23037
- pm2Exec(["save"]);
23705
+ return resolveInstalledPlugin();
23038
23706
  }
23039
- var __dirname, GATEWAY_ENTRY, PROCESS_NAME, VERSION_FILE;
23040
- var init_pm2 = __esm({
23041
- "src/daemon/pm2.ts"() {
23707
+ var PACKAGE_NAME;
23708
+ var init_update2 = __esm({
23709
+ "src/daemon/update.ts"() {
23042
23710
  "use strict";
23043
- __dirname = dirname3(fileURLToPath2(import.meta.url));
23044
- GATEWAY_ENTRY = join3(__dirname, "../gateway/index.js");
23045
- PROCESS_NAME = "supertask-gateway";
23046
- VERSION_FILE = join3(homedir3(), ".local/share/opencode/supertask-gateway-version");
23711
+ PACKAGE_NAME = "opencode-supertask";
23047
23712
  }
23048
23713
  });
23049
23714
 
@@ -23099,6 +23764,7 @@ function parseDuration(input) {
23099
23764
  }
23100
23765
 
23101
23766
  // src/cli/index.ts
23767
+ init_pm2();
23102
23768
  async function withDb(fn) {
23103
23769
  try {
23104
23770
  return await fn();
@@ -23111,9 +23777,14 @@ async function withDb(fn) {
23111
23777
  }
23112
23778
  }
23113
23779
  var program2 = new Command();
23114
- program2.name("supertask").description("\u901A\u7528\u4EFB\u52A1\u7BA1\u7406\u7CFB\u7EDF - AI Agent \u4EFB\u52A1\u8C03\u5EA6\u5668").version("0.1.0");
23115
- 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 () => {
23780
+ program2.name("supertask").description("\u901A\u7528\u4EFB\u52A1\u7BA1\u7406\u7CFB\u7EDF - AI Agent \u4EFB\u52A1\u8C03\u5EA6\u5668").version(getPackageVersion());
23781
+ 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 () => {
23116
23782
  const submitCwd = process.cwd();
23783
+ const retryBackoffMs = parseDuration(options.retryBackoff);
23784
+ const timeoutMs = options.timeout ? parseDuration(options.timeout) : null;
23785
+ if (retryBackoffMs === null || options.timeout && timeoutMs === null) {
23786
+ throw new Error("retry-backoff \u6216 timeout \u683C\u5F0F\u65E0\u6548");
23787
+ }
23117
23788
  const task = await TaskService.add({
23118
23789
  name: options.name,
23119
23790
  agent: options.agent,
@@ -23124,7 +23795,10 @@ program2.command("add").description("\u521B\u5EFA\u65B0\u4EFB\u52A1").requiredOp
23124
23795
  urgency: parseInt(options.urgency),
23125
23796
  batchId: options.batch,
23126
23797
  dependsOn: options.depends ? parseInt(options.depends) : void 0,
23127
- cwd: submitCwd
23798
+ cwd: submitCwd,
23799
+ maxRetries: parseInt(options.maxRetries),
23800
+ retryBackoffMs,
23801
+ timeoutMs
23128
23802
  });
23129
23803
  console.log(JSON.stringify({ id: task.id, status: "created" }, null, 2));
23130
23804
  }));
@@ -23143,7 +23817,7 @@ program2.command("next").description("\u83B7\u53D6\u4E0B\u4E00\u4E2A\u5F85\u6267
23143
23817
  urgency: task.urgency
23144
23818
  }, null, 2));
23145
23819
  } else {
23146
- console.log(JSON.stringify({ id: null, message: "No pending tasks" }));
23820
+ console.log(JSON.stringify({ id: null, message: "No executable tasks" }));
23147
23821
  }
23148
23822
  }));
23149
23823
  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 () => {
@@ -23231,9 +23905,14 @@ program2.command("delete").description("\u5220\u9664\u4EFB\u52A1").requiredOptio
23231
23905
  console.log(JSON.stringify({ deleted, id: parseInt(options.id) }));
23232
23906
  }));
23233
23907
  program2.command("template").description("\u7BA1\u7406\u4EFB\u52A1\u8C03\u5EA6\u6A21\u677F").addCommand(
23234
- 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 () => {
23908
+ 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 () => {
23235
23909
  let intervalMs = null;
23236
23910
  let runAt = null;
23911
+ const retryBackoffMs = parseDuration(options.retryBackoff);
23912
+ const timeoutMs = options.timeout ? parseDuration(options.timeout) : null;
23913
+ if (retryBackoffMs === null || options.timeout && timeoutMs === null) {
23914
+ throw new Error("retry-backoff \u6216 timeout \u683C\u5F0F\u65E0\u6548");
23915
+ }
23237
23916
  if (options.interval) {
23238
23917
  intervalMs = parseDuration(options.interval);
23239
23918
  if (intervalMs === null) {
@@ -23257,13 +23936,16 @@ program2.command("template").description("\u7BA1\u7406\u4EFB\u52A1\u8C03\u5EA6\u
23257
23936
  category: options.category,
23258
23937
  importance: parseInt(options.importance),
23259
23938
  urgency: parseInt(options.urgency),
23939
+ cwd: process.cwd(),
23940
+ batchId: options.batch,
23260
23941
  scheduleType: options.type,
23261
23942
  cronExpr: options.cron,
23262
23943
  intervalMs,
23263
23944
  runAt,
23264
23945
  maxInstances: parseInt(options.maxInstances),
23265
23946
  maxRetries: parseInt(options.maxRetries),
23266
- retryBackoffMs: parseInt(options.retryBackoff)
23947
+ retryBackoffMs,
23948
+ timeoutMs
23267
23949
  });
23268
23950
  console.log(JSON.stringify({ id: tmpl.id, status: "created", nextRunAt: tmpl.nextRunAt }, null, 2));
23269
23951
  }))
@@ -23299,36 +23981,29 @@ program2.command("template").description("\u7BA1\u7406\u4EFB\u52A1\u8C03\u5EA6\u
23299
23981
  }))
23300
23982
  );
23301
23983
  program2.command("init").description("Initialize SuperTask (create config + run migrations)").action(async () => withDb(async () => {
23302
- const { existsSync: existsSync5, mkdirSync: mkdirSync3, writeFileSync: writeFileSync3 } = await import("fs");
23303
- const { homedir: homedir4 } = await import("os");
23304
- const { join: join4, dirname: dirname4 } = await import("path");
23305
- const { CONFIG_PATH: CONFIG_PATH2 } = await Promise.resolve().then(() => (init_config(), config_exports));
23306
- if (!existsSync5(CONFIG_PATH2)) {
23307
- const dir = dirname4(CONFIG_PATH2);
23308
- if (!existsSync5(dir)) mkdirSync3(dir, { recursive: true });
23309
- writeFileSync3(CONFIG_PATH2, JSON.stringify({
23984
+ const { existsSync: existsSync6, mkdirSync: mkdirSync4, writeFileSync: writeFileSync3 } = await import("fs");
23985
+ const { dirname: dirname4 } = await import("path");
23986
+ const { getConfigPath: getConfigPath2 } = await Promise.resolve().then(() => (init_config(), config_exports));
23987
+ const configPath = getConfigPath2();
23988
+ if (!existsSync6(configPath)) {
23989
+ const dir = dirname4(configPath);
23990
+ if (!existsSync6(dir)) mkdirSync4(dir, { recursive: true });
23991
+ writeFileSync3(configPath, JSON.stringify({
23992
+ configVersion: 2,
23310
23993
  worker: { maxConcurrency: 2 },
23311
23994
  scheduler: { enabled: true }
23312
23995
  }, null, 2) + "\n");
23313
- console.log(JSON.stringify({ created: CONFIG_PATH2 }));
23996
+ console.log(JSON.stringify({ created: configPath }));
23314
23997
  } else {
23315
- console.log(JSON.stringify({ exists: CONFIG_PATH2 }));
23998
+ console.log(JSON.stringify({ exists: configPath }));
23316
23999
  }
23317
24000
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db2(), db_exports));
23318
- const { migrate: migrate2 } = await Promise.resolve().then(() => (init_migrator2(), migrator_exports));
23319
- const { join: pJoin, dirname: pDirname } = await import("path");
23320
- const { fileURLToPath: fileURLToPath3 } = await import("url");
23321
- const __dirname2 = pDirname(fileURLToPath3(import.meta.url));
23322
- migrate2(getDb2(), { migrationsFolder: pJoin(__dirname2, "../../drizzle") });
24001
+ getDb2();
23323
24002
  console.log(JSON.stringify({ migrated: true }));
23324
24003
  }));
23325
24004
  program2.command("migrate").description("Run database migrations").action(async () => withDb(async () => {
23326
24005
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db2(), db_exports));
23327
- const { migrate: migrate2 } = await Promise.resolve().then(() => (init_migrator2(), migrator_exports));
23328
- const { join: join4, dirname: dirname4 } = await import("path");
23329
- const { fileURLToPath: fileURLToPath3 } = await import("url");
23330
- const __dirname2 = dirname4(fileURLToPath3(import.meta.url));
23331
- migrate2(getDb2(), { migrationsFolder: join4(__dirname2, "../../drizzle") });
24006
+ getDb2();
23332
24007
  console.log(JSON.stringify({ migrated: true }));
23333
24008
  }));
23334
24009
  program2.command("gateway").description("Start the Gateway process (foreground)").action(async () => {
@@ -23370,26 +24045,20 @@ program2.command("uninstall").description("Stop and remove Gateway pm2 service")
23370
24045
  process.exit(1);
23371
24046
  }
23372
24047
  });
23373
- program2.command("upgrade").description("Update npm package and restart Gateway").action(async () => {
23374
- const { execSync: execSync2 } = await import("child_process");
23375
- const { homedir: homedir4 } = await import("os");
23376
- const { join: join4 } = await import("path");
23377
- const configDir = join4(homedir4(), ".config/opencode");
24048
+ program2.command("upgrade").description("Update OpenCode plugin cache and restart Gateway").action(async () => {
23378
24049
  console.log("Updating opencode-supertask...");
24050
+ let installed;
23379
24051
  try {
23380
- execSync2("npm install opencode-supertask@latest", {
23381
- cwd: configDir,
23382
- stdio: "inherit",
23383
- timeout: 6e4
23384
- });
24052
+ const { installLatestPlugin: installLatestPlugin2 } = await Promise.resolve().then(() => (init_update2(), update_exports));
24053
+ installed = installLatestPlugin2();
23385
24054
  } catch (err) {
23386
- console.error("npm install failed:", err instanceof Error ? err.message : String(err));
23387
- console.error("Try manually: cd ~/.config/opencode && npm install opencode-supertask@latest");
24055
+ console.error(err instanceof Error ? err.message : String(err));
24056
+ console.error("Try manually: opencode plugin opencode-supertask@latest --global --force");
23388
24057
  process.exit(1);
23389
24058
  }
23390
24059
  try {
23391
24060
  const { upgrade: pm2Upgrade } = await Promise.resolve().then(() => (init_pm2(), pm2_exports));
23392
- const result = pm2Upgrade();
24061
+ const result = pm2Upgrade(installed);
23393
24062
  console.log(`
23394
24063
  SuperTask upgraded: ${result.before ?? "unknown"} \u2192 ${result.after}`);
23395
24064
  console.log("Gateway restarted. Please restart opencode to load the new plugin.");