opencode-supertask 0.1.20 → 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.
@@ -1205,8 +1205,8 @@ function haveSameKeys(left, right) {
1205
1205
  if (leftKeys.length !== rightKeys.length) {
1206
1206
  return false;
1207
1207
  }
1208
- for (const [index, key] of leftKeys.entries()) {
1209
- if (key !== rightKeys[index]) {
1208
+ for (const [index2, key] of leftKeys.entries()) {
1209
+ if (key !== rightKeys[index2]) {
1210
1210
  return false;
1211
1211
  }
1212
1212
  }
@@ -2772,8 +2772,8 @@ var SQLiteDialect = class {
2772
2772
  }
2773
2773
  const joinsArray = [];
2774
2774
  if (joins) {
2775
- for (const [index, joinMeta] of joins.entries()) {
2776
- if (index === 0) {
2775
+ for (const [index2, joinMeta] of joins.entries()) {
2776
+ if (index2 === 0) {
2777
2777
  joinsArray.push(sql` `);
2778
2778
  }
2779
2779
  const table = joinMeta.table;
@@ -2790,7 +2790,7 @@ var SQLiteDialect = class {
2790
2790
  sql`${sql.raw(joinMeta.joinType)} join ${table} on ${joinMeta.on}`
2791
2791
  );
2792
2792
  }
2793
- if (index < joins.length - 1) {
2793
+ if (index2 < joins.length - 1) {
2794
2794
  joinsArray.push(sql` `);
2795
2795
  }
2796
2796
  }
@@ -2803,9 +2803,9 @@ var SQLiteDialect = class {
2803
2803
  buildOrderBy(orderBy) {
2804
2804
  const orderByList = [];
2805
2805
  if (orderBy) {
2806
- for (const [index, orderByValue] of orderBy.entries()) {
2806
+ for (const [index2, orderByValue] of orderBy.entries()) {
2807
2807
  orderByList.push(orderByValue);
2808
- if (index < orderBy.length - 1) {
2808
+ if (index2 < orderBy.length - 1) {
2809
2809
  orderByList.push(sql`, `);
2810
2810
  }
2811
2811
  }
@@ -2854,9 +2854,9 @@ var SQLiteDialect = class {
2854
2854
  const havingSql = having ? sql` having ${having}` : void 0;
2855
2855
  const groupByList = [];
2856
2856
  if (groupBy) {
2857
- for (const [index, groupByValue] of groupBy.entries()) {
2857
+ for (const [index2, groupByValue] of groupBy.entries()) {
2858
2858
  groupByList.push(groupByValue);
2859
- if (index < groupBy.length - 1) {
2859
+ if (index2 < groupBy.length - 1) {
2860
2860
  groupByList.push(sql`, `);
2861
2861
  }
2862
2862
  }
@@ -4784,6 +4784,52 @@ var BaseSQLiteDatabase = class {
4784
4784
  }
4785
4785
  };
4786
4786
 
4787
+ // node_modules/drizzle-orm/sqlite-core/indexes.js
4788
+ var IndexBuilderOn = class {
4789
+ constructor(name, unique) {
4790
+ this.name = name;
4791
+ this.unique = unique;
4792
+ }
4793
+ static [entityKind] = "SQLiteIndexBuilderOn";
4794
+ on(...columns) {
4795
+ return new IndexBuilder(this.name, columns, this.unique);
4796
+ }
4797
+ };
4798
+ var IndexBuilder = class {
4799
+ static [entityKind] = "SQLiteIndexBuilder";
4800
+ /** @internal */
4801
+ config;
4802
+ constructor(name, columns, unique) {
4803
+ this.config = {
4804
+ name,
4805
+ columns,
4806
+ unique,
4807
+ where: void 0
4808
+ };
4809
+ }
4810
+ /**
4811
+ * Condition for partial index.
4812
+ */
4813
+ where(condition) {
4814
+ this.config.where = condition;
4815
+ return this;
4816
+ }
4817
+ /** @internal */
4818
+ build(table) {
4819
+ return new Index(this.config, table);
4820
+ }
4821
+ };
4822
+ var Index = class {
4823
+ static [entityKind] = "SQLiteIndex";
4824
+ config;
4825
+ constructor(config, table) {
4826
+ this.config = { ...config, table };
4827
+ }
4828
+ };
4829
+ function index(name) {
4830
+ return new IndexBuilderOn(name, false);
4831
+ }
4832
+
4787
4833
  // node_modules/drizzle-orm/sqlite-core/session.js
4788
4834
  var ExecuteResultSync = class extends QueryPromise {
4789
4835
  constructor(resultCb) {
@@ -5130,12 +5176,17 @@ var tasks = sqliteTable("tasks", {
5130
5176
  resultLog: text("result_log"),
5131
5177
  retryCount: integer("retry_count").default(0),
5132
5178
  maxRetries: integer("max_retries").default(3),
5179
+ retryBackoffMs: integer("retry_backoff_ms").default(3e4),
5133
5180
  // Gateway 扩展字段(毫秒)
5134
5181
  retryAfter: integer("retry_after"),
5135
5182
  timeoutMs: integer("timeout_ms"),
5136
5183
  templateId: integer("template_id"),
5137
5184
  scheduledAt: integer("scheduled_at")
5138
- });
5185
+ }, (table) => [
5186
+ index("tasks_queue_idx").on(table.status, table.retryAfter, table.urgency, table.importance, table.createdAt, table.id),
5187
+ index("tasks_batch_status_idx").on(table.batchId, table.status),
5188
+ index("tasks_template_status_idx").on(table.templateId, table.status)
5189
+ ]);
5139
5190
  var TASK_CATEGORIES = [
5140
5191
  "translate",
5141
5192
  "generate",
@@ -5145,7 +5196,7 @@ var TASK_CATEGORIES = [
5145
5196
  ];
5146
5197
  var taskRuns = sqliteTable("task_runs", {
5147
5198
  id: integer("id").primaryKey({ autoIncrement: true }),
5148
- taskId: integer("task_id").notNull().references(() => tasks.id),
5199
+ taskId: integer("task_id").notNull().references(() => tasks.id, { onDelete: "cascade" }),
5149
5200
  sessionId: text("session_id"),
5150
5201
  model: text("model"),
5151
5202
  status: text("status").default("running"),
@@ -5158,7 +5209,10 @@ var taskRuns = sqliteTable("task_runs", {
5158
5209
  heartbeatAt: integer("heartbeat_at"),
5159
5210
  workerPid: integer("worker_pid"),
5160
5211
  childPid: integer("child_pid")
5161
- });
5212
+ }, (table) => [
5213
+ index("task_runs_task_started_idx").on(table.taskId, table.startedAt, table.id),
5214
+ index("task_runs_status_heartbeat_idx").on(table.status, table.heartbeatAt)
5215
+ ]);
5162
5216
  var taskTemplates = sqliteTable("task_templates", {
5163
5217
  id: integer("id").primaryKey({ autoIncrement: true }),
5164
5218
  name: text("name").notNull(),
@@ -5169,6 +5223,7 @@ var taskTemplates = sqliteTable("task_templates", {
5169
5223
  category: text("category").default("general"),
5170
5224
  importance: integer("importance").default(3),
5171
5225
  urgency: integer("urgency").default(3),
5226
+ batchId: text("batch_id"),
5172
5227
  scheduleType: text("schedule_type").notNull(),
5173
5228
  cronExpr: text("cron_expr"),
5174
5229
  intervalMs: integer("interval_ms"),
@@ -5176,12 +5231,15 @@ var taskTemplates = sqliteTable("task_templates", {
5176
5231
  maxInstances: integer("max_instances").default(1),
5177
5232
  maxRetries: integer("max_retries").default(3),
5178
5233
  retryBackoffMs: integer("retry_backoff_ms").default(3e4),
5234
+ timeoutMs: integer("timeout_ms"),
5179
5235
  lastRunAt: integer("last_run_at"),
5180
5236
  nextRunAt: integer("next_run_at"),
5181
5237
  enabled: integer("enabled", { mode: "boolean" }).default(true),
5182
5238
  createdAt: integer("created_at").default(0),
5183
5239
  updatedAt: integer("updated_at").default(0)
5184
- });
5240
+ }, (table) => [
5241
+ index("task_templates_due_idx").on(table.enabled, table.nextRunAt, table.id)
5242
+ ]);
5185
5243
 
5186
5244
  // src/core/db/index.ts
5187
5245
  import { existsSync, mkdirSync } from "fs";
@@ -5218,16 +5276,30 @@ function initDb() {
5218
5276
  id INTEGER PRIMARY KEY CHECK (id = 1),
5219
5277
  pid INTEGER NOT NULL,
5220
5278
  acquired_at INTEGER NOT NULL,
5221
- heartbeat_at INTEGER NOT NULL
5279
+ heartbeat_at INTEGER NOT NULL,
5280
+ ready_at INTEGER
5222
5281
  );
5223
5282
  `);
5283
+ const gatewayLockColumns = _sqlite.query("PRAGMA table_info(gateway_lock)").all();
5284
+ if (!gatewayLockColumns.some((column) => column.name === "ready_at")) {
5285
+ _sqlite.exec("ALTER TABLE gateway_lock ADD COLUMN ready_at INTEGER;");
5286
+ }
5224
5287
  _db = drizzle(_sqlite, { schema: schema_exports });
5225
5288
  if (!_migrationRan) {
5226
- _migrationRan = true;
5227
5289
  try {
5228
5290
  migrate(_db, { migrationsFolder: getMigrationsFolder() });
5291
+ _sqlite.exec("PRAGMA foreign_keys = ON;");
5292
+ const violations = _sqlite.query("PRAGMA foreign_key_check;").all();
5293
+ if (violations.length > 0) {
5294
+ 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`);
5295
+ }
5296
+ _migrationRan = true;
5229
5297
  } catch (err) {
5230
5298
  const msg = err instanceof Error ? err.message : String(err);
5299
+ _migrationRan = false;
5300
+ _sqlite.close();
5301
+ _sqlite = null;
5302
+ _db = null;
5231
5303
  console.error(`[supertask] migration failed: ${msg}`);
5232
5304
  throw new Error(`[supertask] DB migration failed: ${msg}`);
5233
5305
  }
@@ -5266,7 +5338,7 @@ function computeBackoff(retryCount, baseMs = 3e4, maxMs = MAX_BACKOFF_MS) {
5266
5338
  }
5267
5339
 
5268
5340
  // src/core/services/task.service.ts
5269
- var { tasks: tasks2 } = schema_exports;
5341
+ var { tasks: tasks2, taskRuns: taskRuns2 } = schema_exports;
5270
5342
  var TaskService = class {
5271
5343
  static buildScopeWhere(scope) {
5272
5344
  const conditions = [];
@@ -5276,9 +5348,27 @@ var TaskService = class {
5276
5348
  return conditions;
5277
5349
  }
5278
5350
  static async add(data) {
5351
+ this.validateNewTask(data);
5279
5352
  const result = await db.insert(tasks2).values(data).returning();
5280
5353
  return result[0];
5281
5354
  }
5355
+ static validateNewTask(data) {
5356
+ if (!data.name.trim()) throw new Error("name \u4E0D\u80FD\u4E3A\u7A7A");
5357
+ if (!data.agent.trim()) throw new Error("agent \u4E0D\u80FD\u4E3A\u7A7A");
5358
+ if (!data.prompt.trim()) throw new Error("prompt \u4E0D\u80FD\u4E3A\u7A7A");
5359
+ this.validateInteger("importance", data.importance, 1, 5);
5360
+ this.validateInteger("urgency", data.urgency, 1, 5);
5361
+ this.validateInteger("maxRetries", data.maxRetries, 0, 1e3);
5362
+ this.validateInteger("retryBackoffMs", data.retryBackoffMs, 0, 864e5);
5363
+ this.validateInteger("timeoutMs", data.timeoutMs, 1e3, 6048e5);
5364
+ this.validateInteger("dependsOn", data.dependsOn, 1, Number.MAX_SAFE_INTEGER);
5365
+ }
5366
+ static validateInteger(name, value, min, max) {
5367
+ if (value === void 0 || value === null) return;
5368
+ if (!Number.isInteger(value) || value < min || value > max) {
5369
+ throw new Error(`${name} \u5FC5\u987B\u662F ${min} \u5230 ${max} \u4E4B\u95F4\u7684\u6574\u6570`);
5370
+ }
5371
+ }
5282
5372
  static async next(scope = {}) {
5283
5373
  const baseConditions = [...this.buildScopeWhere(scope)];
5284
5374
  const nowMs = Date.now();
@@ -5301,7 +5391,7 @@ var TaskService = class {
5301
5391
  ),
5302
5392
  and(
5303
5393
  eq(tasks2.status, "failed"),
5304
- sql`${tasks2.retryCount} < ${tasks2.maxRetries}`,
5394
+ sql`${tasks2.retryCount} <= ${tasks2.maxRetries}`,
5305
5395
  retryAfterFilter
5306
5396
  )
5307
5397
  );
@@ -5315,7 +5405,8 @@ var TaskService = class {
5315
5405
  const allTasks = await db.select().from(tasks2).where(and(...conditions)).orderBy(
5316
5406
  desc(tasks2.urgency),
5317
5407
  desc(tasks2.importance),
5318
- asc(tasks2.createdAt)
5408
+ asc(tasks2.createdAt),
5409
+ asc(tasks2.id)
5319
5410
  );
5320
5411
  for (const task of allTasks) {
5321
5412
  if (task.dependsOn) {
@@ -5336,7 +5427,7 @@ var TaskService = class {
5336
5427
  eq(tasks2.status, "pending"),
5337
5428
  and(
5338
5429
  eq(tasks2.status, "failed"),
5339
- sql`${tasks2.retryCount} < ${tasks2.maxRetries}`
5430
+ sql`${tasks2.retryCount} <= ${tasks2.maxRetries}`
5340
5431
  )
5341
5432
  ),
5342
5433
  ...this.buildScopeWhere(scope)
@@ -5349,7 +5440,11 @@ var TaskService = class {
5349
5440
  return result[0] || null;
5350
5441
  }
5351
5442
  static async done(id, log, scope = {}) {
5352
- const conditions = [eq(tasks2.id, id), ...this.buildScopeWhere(scope)];
5443
+ const conditions = [
5444
+ eq(tasks2.id, id),
5445
+ eq(tasks2.status, "running"),
5446
+ ...this.buildScopeWhere(scope)
5447
+ ];
5353
5448
  const result = await db.update(tasks2).set({
5354
5449
  status: "done",
5355
5450
  finishedAt: /* @__PURE__ */ new Date(),
@@ -5360,17 +5455,24 @@ var TaskService = class {
5360
5455
  }
5361
5456
  static async fail(id, log, scope = {}, options) {
5362
5457
  const current = await this.getById(id, scope);
5363
- if (!current) return null;
5458
+ if (!current || current.status !== "running") return null;
5364
5459
  const newRetryCount = (current.retryCount ?? 0) + 1;
5365
5460
  const maxRetries = current.maxRetries ?? 3;
5366
- const isDeadLetter = options?.setDeadLetter ?? newRetryCount >= maxRetries;
5367
- const conditions = [eq(tasks2.id, id), ...this.buildScopeWhere(scope)];
5461
+ const isDeadLetter = options?.setDeadLetter ?? newRetryCount > maxRetries;
5462
+ const conditions = [
5463
+ eq(tasks2.id, id),
5464
+ eq(tasks2.status, "running"),
5465
+ ...this.buildScopeWhere(scope)
5466
+ ];
5368
5467
  const result = await db.update(tasks2).set({
5369
5468
  status: isDeadLetter ? "dead_letter" : "failed",
5370
5469
  finishedAt: /* @__PURE__ */ new Date(),
5371
5470
  resultLog: log,
5372
5471
  retryCount: newRetryCount,
5373
- retryAfter: isDeadLetter ? null : options?.retryAfterMs ?? Date.now() + computeBackoff(newRetryCount)
5472
+ retryAfter: isDeadLetter ? null : options?.retryAfterMs ?? Date.now() + computeBackoff(
5473
+ newRetryCount,
5474
+ current.retryBackoffMs ?? 3e4
5475
+ )
5374
5476
  }).where(and(...conditions)).returning();
5375
5477
  return result[0] || null;
5376
5478
  }
@@ -5403,8 +5505,20 @@ var TaskService = class {
5403
5505
  return result.length;
5404
5506
  }
5405
5507
  static async cancel(id, scope = {}) {
5406
- const conditions = [eq(tasks2.id, id), ...this.buildScopeWhere(scope)];
5407
- const result = await db.update(tasks2).set({ status: "cancelled" }).where(and(...conditions)).returning();
5508
+ const conditions = [
5509
+ eq(tasks2.id, id),
5510
+ or(
5511
+ eq(tasks2.status, "pending"),
5512
+ eq(tasks2.status, "running"),
5513
+ eq(tasks2.status, "failed")
5514
+ ),
5515
+ ...this.buildScopeWhere(scope)
5516
+ ];
5517
+ const result = await db.update(tasks2).set({
5518
+ status: "cancelled",
5519
+ finishedAt: /* @__PURE__ */ new Date(),
5520
+ retryAfter: null
5521
+ }).where(and(...conditions)).returning();
5408
5522
  return result[0] || null;
5409
5523
  }
5410
5524
  static async retry(id, scope = {}) {
@@ -5416,7 +5530,8 @@ var TaskService = class {
5416
5530
  status: "pending",
5417
5531
  startedAt: null,
5418
5532
  finishedAt: null,
5419
- retryAfter: null
5533
+ retryAfter: null,
5534
+ retryCount: 0
5420
5535
  }).where(and(...conditions)).returning();
5421
5536
  return result[0] || null;
5422
5537
  }
@@ -5430,7 +5545,8 @@ var TaskService = class {
5430
5545
  status: "pending",
5431
5546
  startedAt: null,
5432
5547
  finishedAt: null,
5433
- retryAfter: null
5548
+ retryAfter: null,
5549
+ retryCount: 0
5434
5550
  }).where(and(...conditions)).returning();
5435
5551
  return result.length;
5436
5552
  }
@@ -5498,6 +5614,9 @@ var TaskService = class {
5498
5614
  }
5499
5615
  static async delete(id, scope = {}) {
5500
5616
  const conditions = [eq(tasks2.id, id), ...this.buildScopeWhere(scope)];
5617
+ const existing = await db.select({ id: tasks2.id }).from(tasks2).where(and(...conditions)).limit(1);
5618
+ if (!existing[0]) return false;
5619
+ await db.delete(taskRuns2).where(eq(taskRuns2.taskId, id));
5501
5620
  const result = await db.delete(tasks2).where(and(...conditions)).returning();
5502
5621
  return result.length > 0;
5503
5622
  }
@@ -5515,59 +5634,59 @@ var TaskService = class {
5515
5634
  };
5516
5635
 
5517
5636
  // src/core/services/task-run.service.ts
5518
- var { taskRuns: taskRuns2 } = schema_exports;
5637
+ var { taskRuns: taskRuns3 } = schema_exports;
5519
5638
  var TaskRunService = class {
5520
5639
  static async create(data) {
5521
- const result = await db.insert(taskRuns2).values(data).returning();
5640
+ const result = await db.insert(taskRuns3).values(data).returning();
5522
5641
  return result[0];
5523
5642
  }
5524
5643
  static async updateSessionId(id, sessionId) {
5525
- const result = await db.update(taskRuns2).set({ sessionId }).where(eq(taskRuns2.id, id)).returning();
5644
+ const result = await db.update(taskRuns3).set({ sessionId }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
5526
5645
  return result[0] || null;
5527
5646
  }
5528
5647
  static async done(id, log) {
5529
- const result = await db.update(taskRuns2).set({
5648
+ const result = await db.update(taskRuns3).set({
5530
5649
  status: "done",
5531
5650
  finishedAt: /* @__PURE__ */ new Date(),
5532
5651
  log
5533
- }).where(eq(taskRuns2.id, id)).returning();
5652
+ }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
5534
5653
  return result[0] || null;
5535
5654
  }
5536
5655
  static async fail(id, log) {
5537
- const result = await db.update(taskRuns2).set({
5656
+ const result = await db.update(taskRuns3).set({
5538
5657
  status: "failed",
5539
5658
  finishedAt: /* @__PURE__ */ new Date(),
5540
5659
  log
5541
- }).where(eq(taskRuns2.id, id)).returning();
5660
+ }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
5542
5661
  return result[0] || null;
5543
5662
  }
5544
5663
  static async heartbeat(id) {
5545
- const result = await db.update(taskRuns2).set({ heartbeatAt: Date.now() }).where(and(eq(taskRuns2.id, id), eq(taskRuns2.status, "running"))).returning();
5664
+ const result = await db.update(taskRuns3).set({ heartbeatAt: Date.now() }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
5546
5665
  return result[0] || null;
5547
5666
  }
5548
5667
  static async updatePid(id, workerPid, childPid) {
5549
- const result = await db.update(taskRuns2).set({
5668
+ const result = await db.update(taskRuns3).set({
5550
5669
  workerPid,
5551
5670
  childPid,
5552
5671
  lockedAt: Date.now(),
5553
5672
  lockedBy: `gateway-${process.pid}`
5554
- }).where(eq(taskRuns2.id, id)).returning();
5673
+ }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
5555
5674
  return result[0] || null;
5556
5675
  }
5557
5676
  static async getById(id) {
5558
- const result = await db.select().from(taskRuns2).where(eq(taskRuns2.id, id));
5677
+ const result = await db.select().from(taskRuns3).where(eq(taskRuns3.id, id));
5559
5678
  return result[0] || null;
5560
5679
  }
5561
5680
  static async listByTaskId(taskId) {
5562
- return await db.select().from(taskRuns2).where(eq(taskRuns2.taskId, taskId)).orderBy(desc(taskRuns2.startedAt), desc(taskRuns2.id));
5681
+ return await db.select().from(taskRuns3).where(eq(taskRuns3.taskId, taskId)).orderBy(desc(taskRuns3.startedAt), desc(taskRuns3.id));
5563
5682
  }
5564
5683
  static async getLatestByTaskId(taskId) {
5565
- const result = await db.select().from(taskRuns2).where(eq(taskRuns2.taskId, taskId)).orderBy(desc(taskRuns2.startedAt), desc(taskRuns2.id)).limit(1);
5684
+ const result = await db.select().from(taskRuns3).where(eq(taskRuns3.taskId, taskId)).orderBy(desc(taskRuns3.startedAt), desc(taskRuns3.id)).limit(1);
5566
5685
  return result[0] || null;
5567
5686
  }
5568
5687
  static async getLatestByTaskIds(taskIds) {
5569
5688
  if (taskIds.length === 0) return /* @__PURE__ */ new Map();
5570
- const latestRuns = await db.select().from(taskRuns2).where(inArray(taskRuns2.taskId, taskIds)).orderBy(desc(taskRuns2.startedAt));
5689
+ const latestRuns = await db.select().from(taskRuns3).where(inArray(taskRuns3.taskId, taskIds)).orderBy(desc(taskRuns3.startedAt), desc(taskRuns3.id));
5571
5690
  const result = /* @__PURE__ */ new Map();
5572
5691
  for (const run of latestRuns) {
5573
5692
  if (!result.has(run.taskId)) {
@@ -5581,22 +5700,23 @@ var TaskRunService = class {
5581
5700
  const cutoffSec = Math.floor(cutoffMs / 1e3);
5582
5701
  const { tasks: tasksTable } = schema_exports;
5583
5702
  const result = await db.select({
5584
- runId: taskRuns2.id,
5585
- taskId: taskRuns2.taskId,
5586
- childPid: taskRuns2.childPid,
5703
+ runId: taskRuns3.id,
5704
+ taskId: taskRuns3.taskId,
5705
+ childPid: taskRuns3.childPid,
5587
5706
  taskRetryCount: tasksTable.retryCount,
5588
- taskMaxRetries: tasksTable.maxRetries
5589
- }).from(taskRuns2).innerJoin(tasksTable, eq(taskRuns2.taskId, tasksTable.id)).where(
5707
+ taskMaxRetries: tasksTable.maxRetries,
5708
+ taskRetryBackoffMs: tasksTable.retryBackoffMs
5709
+ }).from(taskRuns3).innerJoin(tasksTable, eq(taskRuns3.taskId, tasksTable.id)).where(
5590
5710
  and(
5591
- eq(taskRuns2.status, "running"),
5711
+ eq(taskRuns3.status, "running"),
5592
5712
  or(
5593
5713
  and(
5594
- sql`${taskRuns2.heartbeatAt} IS NULL`,
5595
- sql`${taskRuns2.startedAt} < ${cutoffSec}`
5714
+ sql`${taskRuns3.heartbeatAt} IS NULL`,
5715
+ sql`${taskRuns3.startedAt} < ${cutoffSec}`
5596
5716
  ),
5597
5717
  and(
5598
- sql`${taskRuns2.heartbeatAt} IS NOT NULL`,
5599
- sql`${taskRuns2.heartbeatAt} < ${cutoffMs}`
5718
+ sql`${taskRuns3.heartbeatAt} IS NOT NULL`,
5719
+ sql`${taskRuns3.heartbeatAt} < ${cutoffMs}`
5600
5720
  )
5601
5721
  )
5602
5722
  )
@@ -5606,57 +5726,103 @@ var TaskRunService = class {
5606
5726
  taskId: row.taskId,
5607
5727
  childPid: row.childPid,
5608
5728
  taskRetryCount: row.taskRetryCount ?? 0,
5609
- taskMaxRetries: row.taskMaxRetries ?? 3
5729
+ taskMaxRetries: row.taskMaxRetries ?? 3,
5730
+ taskRetryBackoffMs: row.taskRetryBackoffMs ?? 3e4
5610
5731
  }));
5611
5732
  }
5612
5733
  static async getRunningRunByTaskId(taskId) {
5613
- 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);
5734
+ 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);
5614
5735
  return result[0] || null;
5615
5736
  }
5616
5737
  static async deleteByTaskIds(taskIds) {
5617
5738
  if (taskIds.length === 0) return 0;
5618
- const result = await db.delete(taskRuns2).where(inArray(taskRuns2.taskId, taskIds)).returning();
5739
+ const result = await db.delete(taskRuns3).where(inArray(taskRuns3.taskId, taskIds)).returning();
5619
5740
  return result.length;
5620
5741
  }
5621
5742
  static async getAllRunningRuns() {
5622
- return await db.select().from(taskRuns2).where(eq(taskRuns2.status, "running"));
5743
+ return await db.select().from(taskRuns3).where(eq(taskRuns3.status, "running"));
5623
5744
  }
5624
5745
  };
5625
5746
 
5626
5747
  // src/worker/index.ts
5627
5748
  import { spawn } from "child_process";
5749
+
5750
+ // src/gateway/health.ts
5751
+ var state = null;
5752
+ function markGatewayActivity(component) {
5753
+ if (state) state.lastActivityAt[component] = Date.now();
5754
+ }
5755
+
5756
+ // src/core/process-control.ts
5757
+ function isSafePid(pid) {
5758
+ return Number.isInteger(pid) && pid > 1 && pid !== process.pid;
5759
+ }
5760
+ function signalSpawnedProcessTree(pid, signal) {
5761
+ if (!isSafePid(pid)) return false;
5762
+ if (process.platform !== "win32") {
5763
+ try {
5764
+ process.kill(-pid, signal);
5765
+ return true;
5766
+ } catch {
5767
+ }
5768
+ }
5769
+ try {
5770
+ process.kill(pid, signal);
5771
+ return true;
5772
+ } catch {
5773
+ return false;
5774
+ }
5775
+ }
5776
+
5777
+ // src/worker/index.ts
5778
+ var DEFAULT_MAX_OUTPUT_CHARS = 64 * 1024;
5779
+ var FORBIDDEN_AGENT = "supertask-runner";
5628
5780
  var WorkerEngine = class {
5629
5781
  activeBatchIds = /* @__PURE__ */ new Set();
5630
5782
  runningTasks = /* @__PURE__ */ new Map();
5631
5783
  stopped = false;
5632
5784
  pollTimer = null;
5633
5785
  heartbeatTimer = null;
5786
+ pollCyclePromise = null;
5634
5787
  cfg;
5635
- constructor(cfg) {
5788
+ opencodeBin;
5789
+ maxOutputChars;
5790
+ constructor(cfg, options = {}) {
5636
5791
  this.cfg = cfg.worker;
5792
+ this.opencodeBin = options.opencodeBin ?? process.env.SUPERTASK_OPENCODE_BIN ?? "opencode";
5793
+ this.maxOutputChars = options.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
5637
5794
  }
5638
5795
  start() {
5639
5796
  this.stopped = false;
5797
+ markGatewayActivity("worker");
5640
5798
  this.poll();
5641
5799
  this.heartbeatTimer = setInterval(() => this.updateHeartbeats(), this.cfg.heartbeatIntervalMs);
5642
5800
  }
5643
- stop() {
5801
+ async stop(gracePeriodMs = 0) {
5644
5802
  this.stopped = true;
5645
5803
  if (this.pollTimer) {
5646
5804
  clearTimeout(this.pollTimer);
5647
5805
  this.pollTimer = null;
5648
5806
  }
5807
+ if (this.pollCyclePromise) await this.pollCyclePromise;
5808
+ if (gracePeriodMs > 0 && this.runningTasks.size > 0) {
5809
+ const deadline = Date.now() + gracePeriodMs;
5810
+ while (this.runningTasks.size > 0 && Date.now() < deadline) {
5811
+ await Bun.sleep(Math.min(50, deadline - Date.now()));
5812
+ }
5813
+ }
5649
5814
  if (this.heartbeatTimer) {
5650
5815
  clearInterval(this.heartbeatTimer);
5651
5816
  this.heartbeatTimer = null;
5652
5817
  }
5818
+ const interruptedTaskIds = [...this.runningTasks.keys()];
5653
5819
  const killPromises = [];
5654
- for (const [, entry] of this.runningTasks) {
5820
+ for (const entry of this.runningTasks.values()) {
5655
5821
  entry.shutdown = true;
5656
5822
  killPromises.push(this.killEntry(entry));
5657
5823
  }
5658
- return Promise.allSettled(killPromises).then(() => {
5659
- });
5824
+ await Promise.allSettled(killPromises);
5825
+ return interruptedTaskIds;
5660
5826
  }
5661
5827
  getRunningTaskIds() {
5662
5828
  return [...this.runningTasks.keys()];
@@ -5666,136 +5832,244 @@ var WorkerEngine = class {
5666
5832
  }
5667
5833
  poll() {
5668
5834
  if (this.stopped) return;
5669
- this.tryDispatch().then(() => {
5835
+ markGatewayActivity("worker");
5836
+ this.pollCyclePromise = this.tryDispatch().finally(() => {
5837
+ this.pollCyclePromise = null;
5670
5838
  if (this.stopped) return;
5671
5839
  this.pollTimer = setTimeout(() => this.poll(), this.cfg.pollIntervalMs);
5672
5840
  });
5673
5841
  }
5674
5842
  async tryDispatch() {
5843
+ await this.reconcileCancelledTasks();
5675
5844
  while (!this.stopped && this.runningTasks.size < this.cfg.maxConcurrency) {
5845
+ let task;
5846
+ try {
5847
+ task = await TaskService.next({ excludedBatchIds: [...this.activeBatchIds] });
5848
+ } catch (err) {
5849
+ this.logError("task claim failed", err);
5850
+ break;
5851
+ }
5852
+ if (!task) break;
5853
+ if (this.stopped) break;
5854
+ if (!await TaskService.start(task.id)) continue;
5855
+ if (task.batchId) this.activeBatchIds.add(task.batchId);
5856
+ if (this.stopped) {
5857
+ await TaskService.resetRunningToPending([task.id]);
5858
+ this.releaseBatch(task);
5859
+ break;
5860
+ }
5676
5861
  try {
5677
- const excludedBatchIds = [...this.activeBatchIds];
5678
- const task = await TaskService.next({ excludedBatchIds });
5679
- if (!task) break;
5680
- if (!await TaskService.start(task.id)) continue;
5681
- if (task.batchId) {
5682
- this.activeBatchIds.add(task.batchId);
5683
- }
5684
5862
  const run = await TaskRunService.create({
5685
5863
  taskId: task.id,
5686
5864
  model: this.resolveModel(task.model),
5687
5865
  status: "running"
5688
5866
  });
5689
- const modelToUse = this.resolveModel(task.model);
5690
- const args = ["run", "--agent", "supertask-runner", "--format", "json"];
5691
- if (modelToUse) {
5692
- args.push("-m", modelToUse);
5867
+ if (this.stopped) {
5868
+ await TaskRunService.fail(run.id, "Gateway shutdown before spawn");
5869
+ await TaskService.resetRunningToPending([task.id]);
5870
+ this.releaseBatch(task);
5871
+ break;
5693
5872
  }
5694
- args.push(`\u6267\u884C\u4EFB\u52A1 ID: ${task.id}${modelToUse ? ` OVERRIDE_MODEL=${modelToUse}` : ""}`);
5695
- const cwd = task.cwd || process.cwd();
5696
- const child = spawn("opencode", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
5697
- await TaskRunService.updatePid(run.id, process.pid, child.pid ?? 0);
5698
- let output = "";
5699
- const handleData = (data) => {
5700
- const text2 = data.toString();
5701
- output += text2;
5702
- process.stdout.write(text2);
5703
- const match = text2.match(/"sessionID"\s*:\s*"(ses_[^"]+)"/);
5704
- if (match) {
5705
- TaskRunService.updateSessionId(run.id, match[1]).then(() => {
5706
- console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: "sessionId captured", taskId: task.id, sessionId: match[1] }));
5707
- });
5708
- }
5709
- };
5710
- child.stdout?.on("data", handleData);
5711
- child.stderr?.on("data", handleData);
5712
- const entry = { task, runId: run.id, child, startedAt: Date.now(), shutdown: false };
5713
- this.runningTasks.set(task.id, entry);
5714
- child.on("close", async (code) => {
5715
- this.runningTasks.delete(task.id);
5716
- if (task.batchId) this.activeBatchIds.delete(task.batchId);
5717
- if (entry.shutdown) return;
5718
- const currentRun = await TaskRunService.getById(run.id);
5719
- if (!currentRun || currentRun.status !== "running") return;
5720
- if (code === 0) {
5721
- await TaskRunService.done(run.id);
5722
- await TaskService.done(task.id);
5723
- console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: "task done", taskId: task.id }));
5724
- } else {
5725
- const lastOutput = output.slice(-2e3);
5726
- await TaskRunService.fail(run.id, lastOutput);
5727
- const currentStatus = await TaskService.getById(task.id);
5728
- if (currentStatus?.status === "running") {
5729
- await TaskService.fail(task.id, "Worker\u6267\u884C\u5F02\u5E38\uFF1AOpencode \u8FDB\u7A0B\u975E\u6B63\u5E38\u9000\u51FA");
5730
- }
5731
- console.error(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "error", msg: "task failed", taskId: task.id, code }));
5732
- }
5733
- });
5734
- child.on("error", async (err) => {
5735
- this.runningTasks.delete(task.id);
5736
- if (task.batchId) this.activeBatchIds.delete(task.batchId);
5737
- if (entry.shutdown) return;
5738
- const currentRun = await TaskRunService.getById(run.id);
5739
- if (!currentRun || currentRun.status !== "running") return;
5740
- await TaskRunService.fail(run.id, err.message);
5741
- const currentStatus = await TaskService.getById(task.id);
5742
- if (currentStatus?.status === "running") {
5743
- await TaskService.fail(task.id, `spawn \u5F02\u5E38: ${err.message}`);
5744
- }
5745
- console.error(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "error", msg: "task spawn error", taskId: task.id, error: err.message }));
5873
+ if (task.agent === FORBIDDEN_AGENT) {
5874
+ const message = `\u7981\u6B62\u6267\u884C\u9012\u5F52 Agent: ${FORBIDDEN_AGENT}`;
5875
+ await TaskRunService.fail(run.id, message);
5876
+ await TaskService.fail(task.id, message, {}, { setDeadLetter: true });
5877
+ this.releaseBatch(task);
5878
+ continue;
5879
+ }
5880
+ this.spawnTask(task, run.id);
5881
+ } catch (err) {
5882
+ this.releaseBatch(task);
5883
+ const message = `Worker \u542F\u52A8\u4EFB\u52A1\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`;
5884
+ try {
5885
+ await TaskService.fail(task.id, message);
5886
+ } catch (failErr) {
5887
+ this.logError("failed to compensate task startup", failErr, task.id);
5888
+ }
5889
+ this.logError("task dispatch failed", err, task.id);
5890
+ }
5891
+ }
5892
+ }
5893
+ spawnTask(task, runId) {
5894
+ const model = this.resolveModel(task.model);
5895
+ const args = ["run", "--agent", task.agent, "--format", "json"];
5896
+ if (model) args.push("-m", model);
5897
+ args.push(task.prompt);
5898
+ const child = spawn(this.opencodeBin, args, {
5899
+ cwd: task.cwd || process.cwd(),
5900
+ stdio: ["ignore", "pipe", "pipe"],
5901
+ detached: process.platform !== "win32"
5902
+ });
5903
+ const entry = {
5904
+ task,
5905
+ runId,
5906
+ child,
5907
+ output: "",
5908
+ sessionId: null,
5909
+ timeoutTimer: null,
5910
+ shutdown: false,
5911
+ settled: false
5912
+ };
5913
+ this.runningTasks.set(task.id, entry);
5914
+ const handleData = (data) => {
5915
+ const text2 = data.toString();
5916
+ entry.output = (entry.output + text2).slice(-this.maxOutputChars);
5917
+ process.stdout.write(text2);
5918
+ const match = entry.output.match(/"sessionID"\s*:\s*"(ses_[^"]+)"/);
5919
+ if (match?.[1] && match[1] !== entry.sessionId) {
5920
+ entry.sessionId = match[1];
5921
+ TaskRunService.updateSessionId(runId, match[1]).catch((err) => {
5922
+ this.logError("sessionId update failed", err, task.id);
5746
5923
  });
5924
+ }
5925
+ };
5926
+ child.stdout?.on("data", handleData);
5927
+ child.stderr?.on("data", handleData);
5928
+ child.once("error", (err) => {
5929
+ void this.finishEntry(entry, null, `\u65E0\u6CD5\u542F\u52A8 opencode\uFF1A${err.message}`);
5930
+ });
5931
+ child.once("close", (code, signal) => {
5932
+ const failure = code === 0 ? void 0 : `opencode \u9000\u51FA\u7801 ${code ?? "null"}${signal ? `\uFF0C\u4FE1\u53F7 ${signal}` : ""}`;
5933
+ void this.finishEntry(entry, code, failure);
5934
+ });
5935
+ const timeoutMs = task.timeoutMs ?? this.cfg.taskTimeoutMs;
5936
+ if (timeoutMs > 0) {
5937
+ entry.timeoutTimer = setTimeout(() => {
5938
+ this.signalEntry(entry, "SIGKILL");
5939
+ void this.finishEntry(entry, null, `\u4EFB\u52A1\u8D85\u65F6\uFF08${timeoutMs}ms\uFF09`);
5940
+ }, timeoutMs);
5941
+ }
5942
+ TaskRunService.updatePid(runId, process.pid, child.pid ?? 0).catch((err) => {
5943
+ this.signalEntry(entry, "SIGKILL");
5944
+ void this.finishEntry(entry, null, `\u8BB0\u5F55 Worker PID \u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`);
5945
+ });
5946
+ }
5947
+ async finishEntry(entry, code, failure) {
5948
+ if (entry.settled) return;
5949
+ entry.settled = true;
5950
+ if (entry.timeoutTimer) {
5951
+ clearTimeout(entry.timeoutTimer);
5952
+ entry.timeoutTimer = null;
5953
+ }
5954
+ try {
5955
+ if (entry.shutdown) return;
5956
+ const currentRun = await TaskRunService.getById(entry.runId);
5957
+ if (!currentRun || currentRun.status !== "running") return;
5958
+ const output = entry.output.trim();
5959
+ const log = failure ? `${failure}${output ? `
5960
+ ${output}` : ""}` : output;
5961
+ if (code === 0 && !failure) {
5962
+ const completed = await TaskService.done(entry.task.id, log);
5963
+ if (completed) {
5964
+ await TaskRunService.done(entry.runId, log);
5965
+ console.log(JSON.stringify({
5966
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
5967
+ level: "info",
5968
+ msg: "task done",
5969
+ taskId: entry.task.id
5970
+ }));
5971
+ return;
5972
+ }
5973
+ await TaskRunService.fail(entry.runId, "\u4EFB\u52A1\u72B6\u6001\u5DF2\u88AB\u5176\u4ED6\u64CD\u4F5C\u6539\u53D8");
5974
+ return;
5975
+ }
5976
+ await TaskRunService.fail(entry.runId, log);
5977
+ const failed = await TaskService.fail(entry.task.id, log);
5978
+ if (!failed) {
5979
+ this.logError("task failure state transition rejected", failure ?? "unknown failure", entry.task.id);
5980
+ }
5981
+ console.error(JSON.stringify({
5982
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
5983
+ level: "error",
5984
+ msg: "task failed",
5985
+ taskId: entry.task.id,
5986
+ error: failure
5987
+ }));
5988
+ } finally {
5989
+ this.runningTasks.delete(entry.task.id);
5990
+ this.releaseBatch(entry.task);
5991
+ }
5992
+ }
5993
+ async reconcileCancelledTasks() {
5994
+ for (const entry of [...this.runningTasks.values()]) {
5995
+ try {
5996
+ const task = await TaskService.getById(entry.task.id);
5997
+ if (task?.status === "cancelled") await this.cancelEntry(entry);
5747
5998
  } catch (err) {
5748
- console.error(JSON.stringify({
5749
- ts: (/* @__PURE__ */ new Date()).toISOString(),
5750
- level: "error",
5751
- msg: "tryDispatch iteration failed",
5752
- error: err instanceof Error ? err.message : String(err)
5753
- }));
5754
- break;
5999
+ this.logError("cancel reconciliation failed", err, entry.task.id);
5755
6000
  }
5756
6001
  }
5757
6002
  }
6003
+ async cancelEntry(entry) {
6004
+ if (entry.settled) return;
6005
+ entry.settled = true;
6006
+ if (entry.timeoutTimer) {
6007
+ clearTimeout(entry.timeoutTimer);
6008
+ entry.timeoutTimer = null;
6009
+ }
6010
+ try {
6011
+ await this.killEntry(entry);
6012
+ const output = entry.output.trim();
6013
+ const log = `\u4EFB\u52A1\u5DF2\u53D6\u6D88${output ? `
6014
+ ${output}` : ""}`;
6015
+ await TaskRunService.fail(entry.runId, log);
6016
+ console.log(JSON.stringify({
6017
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
6018
+ level: "info",
6019
+ msg: "running task cancelled",
6020
+ taskId: entry.task.id
6021
+ }));
6022
+ } finally {
6023
+ this.runningTasks.delete(entry.task.id);
6024
+ this.releaseBatch(entry.task);
6025
+ }
6026
+ }
5758
6027
  async updateHeartbeats() {
5759
- for (const [, entry] of this.runningTasks) {
6028
+ for (const entry of this.runningTasks.values()) {
5760
6029
  try {
5761
6030
  await TaskRunService.heartbeat(entry.runId);
5762
- } catch {
6031
+ } catch (err) {
6032
+ this.logError("heartbeat update failed", err, entry.task.id);
5763
6033
  }
5764
6034
  }
5765
6035
  }
5766
6036
  killEntry(entry) {
5767
- if (entry.child.exitCode !== null) {
6037
+ if (entry.child.exitCode !== null || entry.child.signalCode !== null) {
5768
6038
  return Promise.resolve();
5769
6039
  }
5770
6040
  return new Promise((resolve) => {
5771
6041
  const timeout = setTimeout(() => {
5772
- try {
5773
- if (entry.child.pid) process.kill(entry.child.pid, "SIGKILL");
5774
- } catch {
5775
- }
6042
+ this.signalEntry(entry, "SIGKILL");
5776
6043
  resolve();
5777
6044
  }, 5e3);
5778
- entry.child.on("close", () => {
6045
+ entry.child.once("close", () => {
5779
6046
  clearTimeout(timeout);
5780
6047
  resolve();
5781
6048
  });
5782
- try {
5783
- if (entry.child.pid) {
5784
- entry.child.kill("SIGTERM");
5785
- } else {
5786
- clearTimeout(timeout);
5787
- resolve();
5788
- }
5789
- } catch {
5790
- clearTimeout(timeout);
5791
- resolve();
5792
- }
6049
+ this.signalEntry(entry, "SIGTERM");
5793
6050
  });
5794
6051
  }
6052
+ signalEntry(entry, signal) {
6053
+ const pid = entry.child.pid;
6054
+ if (!pid) return;
6055
+ signalSpawnedProcessTree(pid, signal);
6056
+ }
6057
+ releaseBatch(task) {
6058
+ if (task.batchId) this.activeBatchIds.delete(task.batchId);
6059
+ }
5795
6060
  resolveModel(taskModel) {
5796
6061
  if (!taskModel || taskModel === "default") return null;
5797
6062
  return taskModel;
5798
6063
  }
6064
+ logError(message, error, taskId) {
6065
+ console.error(JSON.stringify({
6066
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
6067
+ level: "error",
6068
+ msg: message,
6069
+ taskId,
6070
+ error: error instanceof Error ? error.message : String(error)
6071
+ }));
6072
+ }
5799
6073
  };
5800
6074
  export {
5801
6075
  WorkerEngine