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.
@@ -1358,8 +1358,8 @@ function haveSameKeys(left, right) {
1358
1358
  if (leftKeys.length !== rightKeys.length) {
1359
1359
  return false;
1360
1360
  }
1361
- for (const [index, key] of leftKeys.entries()) {
1362
- if (key !== rightKeys[index]) {
1361
+ for (const [index2, key] of leftKeys.entries()) {
1362
+ if (key !== rightKeys[index2]) {
1363
1363
  return false;
1364
1364
  }
1365
1365
  }
@@ -3220,8 +3220,8 @@ var init_dialect = __esm({
3220
3220
  }
3221
3221
  const joinsArray = [];
3222
3222
  if (joins) {
3223
- for (const [index, joinMeta] of joins.entries()) {
3224
- if (index === 0) {
3223
+ for (const [index2, joinMeta] of joins.entries()) {
3224
+ if (index2 === 0) {
3225
3225
  joinsArray.push(sql` `);
3226
3226
  }
3227
3227
  const table = joinMeta.table;
@@ -3238,7 +3238,7 @@ var init_dialect = __esm({
3238
3238
  sql`${sql.raw(joinMeta.joinType)} join ${table} on ${joinMeta.on}`
3239
3239
  );
3240
3240
  }
3241
- if (index < joins.length - 1) {
3241
+ if (index2 < joins.length - 1) {
3242
3242
  joinsArray.push(sql` `);
3243
3243
  }
3244
3244
  }
@@ -3251,9 +3251,9 @@ var init_dialect = __esm({
3251
3251
  buildOrderBy(orderBy) {
3252
3252
  const orderByList = [];
3253
3253
  if (orderBy) {
3254
- for (const [index, orderByValue] of orderBy.entries()) {
3254
+ for (const [index2, orderByValue] of orderBy.entries()) {
3255
3255
  orderByList.push(orderByValue);
3256
- if (index < orderBy.length - 1) {
3256
+ if (index2 < orderBy.length - 1) {
3257
3257
  orderByList.push(sql`, `);
3258
3258
  }
3259
3259
  }
@@ -3302,9 +3302,9 @@ var init_dialect = __esm({
3302
3302
  const havingSql = having ? sql` having ${having}` : void 0;
3303
3303
  const groupByList = [];
3304
3304
  if (groupBy) {
3305
- for (const [index, groupByValue] of groupBy.entries()) {
3305
+ for (const [index2, groupByValue] of groupBy.entries()) {
3306
3306
  groupByList.push(groupByValue);
3307
- if (index < groupBy.length - 1) {
3307
+ if (index2 < groupBy.length - 1) {
3308
3308
  groupByList.push(sql`, `);
3309
3309
  }
3310
3310
  }
@@ -5393,6 +5393,9 @@ var init_checks = __esm({
5393
5393
  });
5394
5394
 
5395
5395
  // node_modules/drizzle-orm/sqlite-core/indexes.js
5396
+ function index(name) {
5397
+ return new IndexBuilderOn(name, false);
5398
+ }
5396
5399
  var IndexBuilderOn, IndexBuilder, Index;
5397
5400
  var init_indexes = __esm({
5398
5401
  "node_modules/drizzle-orm/sqlite-core/indexes.js"() {
@@ -6021,12 +6024,17 @@ var init_schema = __esm({
6021
6024
  resultLog: text("result_log"),
6022
6025
  retryCount: integer("retry_count").default(0),
6023
6026
  maxRetries: integer("max_retries").default(3),
6027
+ retryBackoffMs: integer("retry_backoff_ms").default(3e4),
6024
6028
  // Gateway 扩展字段(毫秒)
6025
6029
  retryAfter: integer("retry_after"),
6026
6030
  timeoutMs: integer("timeout_ms"),
6027
6031
  templateId: integer("template_id"),
6028
6032
  scheduledAt: integer("scheduled_at")
6029
- });
6033
+ }, (table) => [
6034
+ index("tasks_queue_idx").on(table.status, table.retryAfter, table.urgency, table.importance, table.createdAt, table.id),
6035
+ index("tasks_batch_status_idx").on(table.batchId, table.status),
6036
+ index("tasks_template_status_idx").on(table.templateId, table.status)
6037
+ ]);
6030
6038
  TASK_CATEGORIES = [
6031
6039
  "translate",
6032
6040
  "generate",
@@ -6036,7 +6044,7 @@ var init_schema = __esm({
6036
6044
  ];
6037
6045
  taskRuns = sqliteTable("task_runs", {
6038
6046
  id: integer("id").primaryKey({ autoIncrement: true }),
6039
- taskId: integer("task_id").notNull().references(() => tasks.id),
6047
+ taskId: integer("task_id").notNull().references(() => tasks.id, { onDelete: "cascade" }),
6040
6048
  sessionId: text("session_id"),
6041
6049
  model: text("model"),
6042
6050
  status: text("status").default("running"),
@@ -6049,7 +6057,10 @@ var init_schema = __esm({
6049
6057
  heartbeatAt: integer("heartbeat_at"),
6050
6058
  workerPid: integer("worker_pid"),
6051
6059
  childPid: integer("child_pid")
6052
- });
6060
+ }, (table) => [
6061
+ index("task_runs_task_started_idx").on(table.taskId, table.startedAt, table.id),
6062
+ index("task_runs_status_heartbeat_idx").on(table.status, table.heartbeatAt)
6063
+ ]);
6053
6064
  taskTemplates = sqliteTable("task_templates", {
6054
6065
  id: integer("id").primaryKey({ autoIncrement: true }),
6055
6066
  name: text("name").notNull(),
@@ -6060,6 +6071,7 @@ var init_schema = __esm({
6060
6071
  category: text("category").default("general"),
6061
6072
  importance: integer("importance").default(3),
6062
6073
  urgency: integer("urgency").default(3),
6074
+ batchId: text("batch_id"),
6063
6075
  scheduleType: text("schedule_type").notNull(),
6064
6076
  cronExpr: text("cron_expr"),
6065
6077
  intervalMs: integer("interval_ms"),
@@ -6067,12 +6079,15 @@ var init_schema = __esm({
6067
6079
  maxInstances: integer("max_instances").default(1),
6068
6080
  maxRetries: integer("max_retries").default(3),
6069
6081
  retryBackoffMs: integer("retry_backoff_ms").default(3e4),
6082
+ timeoutMs: integer("timeout_ms"),
6070
6083
  lastRunAt: integer("last_run_at"),
6071
6084
  nextRunAt: integer("next_run_at"),
6072
6085
  enabled: integer("enabled", { mode: "boolean" }).default(true),
6073
6086
  createdAt: integer("created_at").default(0),
6074
6087
  updatedAt: integer("updated_at").default(0)
6075
- });
6088
+ }, (table) => [
6089
+ index("task_templates_due_idx").on(table.enabled, table.nextRunAt, table.id)
6090
+ ]);
6076
6091
  }
6077
6092
  });
6078
6093
 
@@ -6107,16 +6122,30 @@ function initDb() {
6107
6122
  id INTEGER PRIMARY KEY CHECK (id = 1),
6108
6123
  pid INTEGER NOT NULL,
6109
6124
  acquired_at INTEGER NOT NULL,
6110
- heartbeat_at INTEGER NOT NULL
6125
+ heartbeat_at INTEGER NOT NULL,
6126
+ ready_at INTEGER
6111
6127
  );
6112
6128
  `);
6129
+ const gatewayLockColumns = _sqlite.query("PRAGMA table_info(gateway_lock)").all();
6130
+ if (!gatewayLockColumns.some((column) => column.name === "ready_at")) {
6131
+ _sqlite.exec("ALTER TABLE gateway_lock ADD COLUMN ready_at INTEGER;");
6132
+ }
6113
6133
  _db = drizzle(_sqlite, { schema: schema_exports });
6114
6134
  if (!_migrationRan) {
6115
- _migrationRan = true;
6116
6135
  try {
6117
6136
  migrate(_db, { migrationsFolder: getMigrationsFolder() });
6137
+ _sqlite.exec("PRAGMA foreign_keys = ON;");
6138
+ const violations = _sqlite.query("PRAGMA foreign_key_check;").all();
6139
+ if (violations.length > 0) {
6140
+ 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`);
6141
+ }
6142
+ _migrationRan = true;
6118
6143
  } catch (err) {
6119
6144
  const msg = err instanceof Error ? err.message : String(err);
6145
+ _migrationRan = false;
6146
+ _sqlite.close();
6147
+ _sqlite = null;
6148
+ _db = null;
6120
6149
  console.error(`[supertask] migration failed: ${msg}`);
6121
6150
  throw new Error(`[supertask] DB migration failed: ${msg}`);
6122
6151
  }
@@ -6136,6 +6165,7 @@ function closeDb() {
6136
6165
  _sqlite.close();
6137
6166
  _sqlite = null;
6138
6167
  _db = null;
6168
+ _migrationRan = false;
6139
6169
  }
6140
6170
  }
6141
6171
  var DEFAULT_DB_PATH, DB_FILE_PATH, _sqlite, _db, _migrationRan, db, sqlite;
@@ -6170,71 +6200,123 @@ var init_db2 = __esm({
6170
6200
  });
6171
6201
 
6172
6202
  // src/gateway/config.ts
6173
- import { readFileSync, existsSync as existsSync2 } from "fs";
6203
+ import { existsSync as existsSync2, readFileSync } from "fs";
6174
6204
  import { homedir as homedir2 } from "os";
6175
6205
  import { join as join2 } from "path";
6176
- function deepMerge(base, override) {
6177
- const result = { ...base };
6178
- for (const key of Object.keys(override)) {
6179
- const val = override[key];
6180
- if (val !== null && typeof val === "object" && !Array.isArray(val) && key in result) {
6181
- const baseVal = result[key];
6182
- if (baseVal !== null && typeof baseVal === "object" && !Array.isArray(baseVal)) {
6183
- result[key] = deepMerge(baseVal, val);
6184
- continue;
6185
- }
6186
- }
6187
- if (val !== void 0) {
6188
- result[key] = val;
6189
- }
6206
+ function getConfigPath() {
6207
+ return process.env.SUPERTASK_CONFIG_PATH ?? DEFAULT_CONFIG_PATH;
6208
+ }
6209
+ function objectAt(value, path) {
6210
+ if (value === void 0) return {};
6211
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
6212
+ throw new Error(`${path} \u5FC5\u987B\u662F\u5BF9\u8C61`);
6190
6213
  }
6191
- return result;
6214
+ return value;
6215
+ }
6216
+ function integerAt(source, key, fallback, min, max, path) {
6217
+ const value = source[key];
6218
+ if (value === void 0) return fallback;
6219
+ if (typeof value !== "number" || !Number.isInteger(value) || value < min || value > max) {
6220
+ throw new Error(`${path}.${key} \u5FC5\u987B\u662F ${min} \u5230 ${max} \u4E4B\u95F4\u7684\u6574\u6570`);
6221
+ }
6222
+ return value;
6223
+ }
6224
+ function booleanAt(source, key, fallback, path) {
6225
+ const value = source[key];
6226
+ if (value === void 0) return fallback;
6227
+ if (typeof value !== "boolean") throw new Error(`${path}.${key} \u5FC5\u987B\u662F\u5E03\u5C14\u503C`);
6228
+ return value;
6192
6229
  }
6193
- function loadConfig() {
6194
- if (!existsSync2(CONFIG_PATH)) {
6195
- return DEFAULT_CONFIG;
6230
+ function validateConfig(input) {
6231
+ const root = objectAt(input, "config");
6232
+ const version2 = root.configVersion ?? 1;
6233
+ if (version2 !== 1 && version2 !== 2) {
6234
+ throw new Error("config.configVersion \u53EA\u652F\u6301 1 \u6216 2");
6235
+ }
6236
+ const worker = objectAt(root.worker, "worker");
6237
+ const scheduler = objectAt(root.scheduler, "scheduler");
6238
+ const watchdog = objectAt(root.watchdog, "watchdog");
6239
+ const dashboard = objectAt(root.dashboard, "dashboard");
6240
+ const heartbeatIntervalMs = integerAt(worker, "heartbeatIntervalMs", DEFAULT_CONFIG.worker.heartbeatIntervalMs, 1e3, 36e5, "worker");
6241
+ const heartbeatTimeoutMs = integerAt(watchdog, "heartbeatTimeoutMs", DEFAULT_CONFIG.watchdog.heartbeatTimeoutMs, 1e3, 864e5, "watchdog");
6242
+ let checkIntervalMs;
6243
+ let cleanupIntervalMs;
6244
+ if (version2 === 1 && watchdog.checkIntervalMs === void 0 && watchdog.cleanupIntervalMs !== void 0) {
6245
+ checkIntervalMs = integerAt(watchdog, "cleanupIntervalMs", DEFAULT_CONFIG.watchdog.checkIntervalMs, 1e3, 36e5, "watchdog");
6246
+ cleanupIntervalMs = DEFAULT_CONFIG.watchdog.cleanupIntervalMs;
6247
+ } else {
6248
+ checkIntervalMs = integerAt(watchdog, "checkIntervalMs", DEFAULT_CONFIG.watchdog.checkIntervalMs, 1e3, 36e5, "watchdog");
6249
+ cleanupIntervalMs = integerAt(watchdog, "cleanupIntervalMs", DEFAULT_CONFIG.watchdog.cleanupIntervalMs, 6e4, 6048e5, "watchdog");
6196
6250
  }
6251
+ if (heartbeatIntervalMs >= heartbeatTimeoutMs) {
6252
+ throw new Error("worker.heartbeatIntervalMs \u5FC5\u987B\u5C0F\u4E8E watchdog.heartbeatTimeoutMs");
6253
+ }
6254
+ if (checkIntervalMs > heartbeatTimeoutMs) {
6255
+ throw new Error("watchdog.checkIntervalMs \u4E0D\u80FD\u5927\u4E8E watchdog.heartbeatTimeoutMs");
6256
+ }
6257
+ return {
6258
+ configVersion: 2,
6259
+ worker: {
6260
+ maxConcurrency: integerAt(worker, "maxConcurrency", DEFAULT_CONFIG.worker.maxConcurrency, 1, 64, "worker"),
6261
+ pollIntervalMs: integerAt(worker, "pollIntervalMs", DEFAULT_CONFIG.worker.pollIntervalMs, 50, 6e4, "worker"),
6262
+ heartbeatIntervalMs,
6263
+ taskTimeoutMs: integerAt(worker, "taskTimeoutMs", DEFAULT_CONFIG.worker.taskTimeoutMs, 1e3, 6048e5, "worker"),
6264
+ shutdownGracePeriodMs: integerAt(worker, "shutdownGracePeriodMs", DEFAULT_CONFIG.worker.shutdownGracePeriodMs, 0, 36e5, "worker")
6265
+ },
6266
+ scheduler: {
6267
+ enabled: booleanAt(scheduler, "enabled", DEFAULT_CONFIG.scheduler.enabled, "scheduler"),
6268
+ checkIntervalMs: integerAt(scheduler, "checkIntervalMs", DEFAULT_CONFIG.scheduler.checkIntervalMs, 100, 6e4, "scheduler")
6269
+ },
6270
+ watchdog: {
6271
+ heartbeatTimeoutMs,
6272
+ checkIntervalMs,
6273
+ cleanupIntervalMs,
6274
+ retentionDays: integerAt(watchdog, "retentionDays", DEFAULT_CONFIG.watchdog.retentionDays, 1, 3650, "watchdog")
6275
+ },
6276
+ dashboard: {
6277
+ enabled: booleanAt(dashboard, "enabled", DEFAULT_CONFIG.dashboard.enabled, "dashboard"),
6278
+ port: integerAt(dashboard, "port", DEFAULT_CONFIG.dashboard.port, 1, 65535, "dashboard")
6279
+ }
6280
+ };
6281
+ }
6282
+ function loadConfig(path = getConfigPath()) {
6283
+ if (!existsSync2(path)) return validateConfig({ configVersion: 2 });
6197
6284
  try {
6198
- const raw2 = readFileSync(CONFIG_PATH, "utf-8");
6199
- const userConfig = JSON.parse(raw2);
6200
- return deepMerge(DEFAULT_CONFIG, userConfig);
6201
- } catch (err) {
6202
- const msg = err instanceof Error ? err.message : String(err);
6203
- console.error(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "warn", msg: "config load failed, using defaults", path: CONFIG_PATH, error: msg }));
6204
- return DEFAULT_CONFIG;
6285
+ return validateConfig(JSON.parse(readFileSync(path, "utf-8")));
6286
+ } catch (error) {
6287
+ const message = error instanceof Error ? error.message : String(error);
6288
+ throw new Error(`\u65E0\u6CD5\u8BFB\u53D6\u914D\u7F6E ${path}: ${message}`);
6205
6289
  }
6206
6290
  }
6207
- var DEFAULT_CONFIG, CONFIG_PATH;
6291
+ var DEFAULT_CONFIG, DEFAULT_CONFIG_PATH;
6208
6292
  var init_config = __esm({
6209
6293
  "src/gateway/config.ts"() {
6210
6294
  "use strict";
6211
6295
  DEFAULT_CONFIG = {
6296
+ configVersion: 2,
6212
6297
  worker: {
6213
6298
  maxConcurrency: 2,
6214
6299
  pollIntervalMs: 1e3,
6215
6300
  heartbeatIntervalMs: 3e4,
6216
- taskTimeoutMs: 18e5
6301
+ taskTimeoutMs: 18e5,
6302
+ shutdownGracePeriodMs: 3e4
6217
6303
  },
6218
6304
  scheduler: {
6219
6305
  enabled: true,
6220
- checkIntervalMs: 1e3,
6221
- catchUp: "next"
6306
+ checkIntervalMs: 1e3
6222
6307
  },
6223
6308
  watchdog: {
6224
6309
  heartbeatTimeoutMs: 6e5,
6225
- cleanupIntervalMs: 6e4,
6310
+ checkIntervalMs: 6e4,
6311
+ cleanupIntervalMs: 864e5,
6226
6312
  retentionDays: 30
6227
6313
  },
6228
6314
  dashboard: {
6229
6315
  enabled: true,
6230
6316
  port: 4680
6231
- },
6232
- logging: {
6233
- level: "info",
6234
- format: "json"
6235
6317
  }
6236
6318
  };
6237
- CONFIG_PATH = join2(homedir2(), ".config/opencode/supertask.json");
6319
+ DEFAULT_CONFIG_PATH = join2(homedir2(), ".config/opencode/supertask.json");
6238
6320
  }
6239
6321
  });
6240
6322
 
@@ -6288,14 +6370,14 @@ var init_backoff = __esm({
6288
6370
  });
6289
6371
 
6290
6372
  // src/core/services/task.service.ts
6291
- var tasks2, TaskService;
6373
+ var tasks2, taskRuns2, TaskService;
6292
6374
  var init_task_service = __esm({
6293
6375
  "src/core/services/task.service.ts"() {
6294
6376
  "use strict";
6295
6377
  init_db2();
6296
6378
  init_drizzle_orm();
6297
6379
  init_backoff();
6298
- ({ tasks: tasks2 } = schema_exports);
6380
+ ({ tasks: tasks2, taskRuns: taskRuns2 } = schema_exports);
6299
6381
  TaskService = class {
6300
6382
  static buildScopeWhere(scope) {
6301
6383
  const conditions = [];
@@ -6305,9 +6387,27 @@ var init_task_service = __esm({
6305
6387
  return conditions;
6306
6388
  }
6307
6389
  static async add(data) {
6390
+ this.validateNewTask(data);
6308
6391
  const result = await db.insert(tasks2).values(data).returning();
6309
6392
  return result[0];
6310
6393
  }
6394
+ static validateNewTask(data) {
6395
+ if (!data.name.trim()) throw new Error("name \u4E0D\u80FD\u4E3A\u7A7A");
6396
+ if (!data.agent.trim()) throw new Error("agent \u4E0D\u80FD\u4E3A\u7A7A");
6397
+ if (!data.prompt.trim()) throw new Error("prompt \u4E0D\u80FD\u4E3A\u7A7A");
6398
+ this.validateInteger("importance", data.importance, 1, 5);
6399
+ this.validateInteger("urgency", data.urgency, 1, 5);
6400
+ this.validateInteger("maxRetries", data.maxRetries, 0, 1e3);
6401
+ this.validateInteger("retryBackoffMs", data.retryBackoffMs, 0, 864e5);
6402
+ this.validateInteger("timeoutMs", data.timeoutMs, 1e3, 6048e5);
6403
+ this.validateInteger("dependsOn", data.dependsOn, 1, Number.MAX_SAFE_INTEGER);
6404
+ }
6405
+ static validateInteger(name, value, min, max) {
6406
+ if (value === void 0 || value === null) return;
6407
+ if (!Number.isInteger(value) || value < min || value > max) {
6408
+ throw new Error(`${name} \u5FC5\u987B\u662F ${min} \u5230 ${max} \u4E4B\u95F4\u7684\u6574\u6570`);
6409
+ }
6410
+ }
6311
6411
  static async next(scope = {}) {
6312
6412
  const baseConditions = [...this.buildScopeWhere(scope)];
6313
6413
  const nowMs = Date.now();
@@ -6330,7 +6430,7 @@ var init_task_service = __esm({
6330
6430
  ),
6331
6431
  and(
6332
6432
  eq(tasks2.status, "failed"),
6333
- sql`${tasks2.retryCount} < ${tasks2.maxRetries}`,
6433
+ sql`${tasks2.retryCount} <= ${tasks2.maxRetries}`,
6334
6434
  retryAfterFilter
6335
6435
  )
6336
6436
  );
@@ -6344,7 +6444,8 @@ var init_task_service = __esm({
6344
6444
  const allTasks = await db.select().from(tasks2).where(and(...conditions)).orderBy(
6345
6445
  desc(tasks2.urgency),
6346
6446
  desc(tasks2.importance),
6347
- asc(tasks2.createdAt)
6447
+ asc(tasks2.createdAt),
6448
+ asc(tasks2.id)
6348
6449
  );
6349
6450
  for (const task of allTasks) {
6350
6451
  if (task.dependsOn) {
@@ -6365,7 +6466,7 @@ var init_task_service = __esm({
6365
6466
  eq(tasks2.status, "pending"),
6366
6467
  and(
6367
6468
  eq(tasks2.status, "failed"),
6368
- sql`${tasks2.retryCount} < ${tasks2.maxRetries}`
6469
+ sql`${tasks2.retryCount} <= ${tasks2.maxRetries}`
6369
6470
  )
6370
6471
  ),
6371
6472
  ...this.buildScopeWhere(scope)
@@ -6378,7 +6479,11 @@ var init_task_service = __esm({
6378
6479
  return result[0] || null;
6379
6480
  }
6380
6481
  static async done(id, log, scope = {}) {
6381
- const conditions = [eq(tasks2.id, id), ...this.buildScopeWhere(scope)];
6482
+ const conditions = [
6483
+ eq(tasks2.id, id),
6484
+ eq(tasks2.status, "running"),
6485
+ ...this.buildScopeWhere(scope)
6486
+ ];
6382
6487
  const result = await db.update(tasks2).set({
6383
6488
  status: "done",
6384
6489
  finishedAt: /* @__PURE__ */ new Date(),
@@ -6389,17 +6494,24 @@ var init_task_service = __esm({
6389
6494
  }
6390
6495
  static async fail(id, log, scope = {}, options) {
6391
6496
  const current = await this.getById(id, scope);
6392
- if (!current) return null;
6497
+ if (!current || current.status !== "running") return null;
6393
6498
  const newRetryCount = (current.retryCount ?? 0) + 1;
6394
6499
  const maxRetries = current.maxRetries ?? 3;
6395
- const isDeadLetter = options?.setDeadLetter ?? newRetryCount >= maxRetries;
6396
- const conditions = [eq(tasks2.id, id), ...this.buildScopeWhere(scope)];
6500
+ const isDeadLetter = options?.setDeadLetter ?? newRetryCount > maxRetries;
6501
+ const conditions = [
6502
+ eq(tasks2.id, id),
6503
+ eq(tasks2.status, "running"),
6504
+ ...this.buildScopeWhere(scope)
6505
+ ];
6397
6506
  const result = await db.update(tasks2).set({
6398
6507
  status: isDeadLetter ? "dead_letter" : "failed",
6399
6508
  finishedAt: /* @__PURE__ */ new Date(),
6400
6509
  resultLog: log,
6401
6510
  retryCount: newRetryCount,
6402
- retryAfter: isDeadLetter ? null : options?.retryAfterMs ?? Date.now() + computeBackoff(newRetryCount)
6511
+ retryAfter: isDeadLetter ? null : options?.retryAfterMs ?? Date.now() + computeBackoff(
6512
+ newRetryCount,
6513
+ current.retryBackoffMs ?? 3e4
6514
+ )
6403
6515
  }).where(and(...conditions)).returning();
6404
6516
  return result[0] || null;
6405
6517
  }
@@ -6432,8 +6544,20 @@ var init_task_service = __esm({
6432
6544
  return result.length;
6433
6545
  }
6434
6546
  static async cancel(id, scope = {}) {
6435
- const conditions = [eq(tasks2.id, id), ...this.buildScopeWhere(scope)];
6436
- const result = await db.update(tasks2).set({ status: "cancelled" }).where(and(...conditions)).returning();
6547
+ const conditions = [
6548
+ eq(tasks2.id, id),
6549
+ or(
6550
+ eq(tasks2.status, "pending"),
6551
+ eq(tasks2.status, "running"),
6552
+ eq(tasks2.status, "failed")
6553
+ ),
6554
+ ...this.buildScopeWhere(scope)
6555
+ ];
6556
+ const result = await db.update(tasks2).set({
6557
+ status: "cancelled",
6558
+ finishedAt: /* @__PURE__ */ new Date(),
6559
+ retryAfter: null
6560
+ }).where(and(...conditions)).returning();
6437
6561
  return result[0] || null;
6438
6562
  }
6439
6563
  static async retry(id, scope = {}) {
@@ -6445,7 +6569,8 @@ var init_task_service = __esm({
6445
6569
  status: "pending",
6446
6570
  startedAt: null,
6447
6571
  finishedAt: null,
6448
- retryAfter: null
6572
+ retryAfter: null,
6573
+ retryCount: 0
6449
6574
  }).where(and(...conditions)).returning();
6450
6575
  return result[0] || null;
6451
6576
  }
@@ -6459,7 +6584,8 @@ var init_task_service = __esm({
6459
6584
  status: "pending",
6460
6585
  startedAt: null,
6461
6586
  finishedAt: null,
6462
- retryAfter: null
6587
+ retryAfter: null,
6588
+ retryCount: 0
6463
6589
  }).where(and(...conditions)).returning();
6464
6590
  return result.length;
6465
6591
  }
@@ -6527,6 +6653,9 @@ var init_task_service = __esm({
6527
6653
  }
6528
6654
  static async delete(id, scope = {}) {
6529
6655
  const conditions = [eq(tasks2.id, id), ...this.buildScopeWhere(scope)];
6656
+ const existing = await db.select({ id: tasks2.id }).from(tasks2).where(and(...conditions)).limit(1);
6657
+ if (!existing[0]) return false;
6658
+ await db.delete(taskRuns2).where(eq(taskRuns2.taskId, id));
6530
6659
  const result = await db.delete(tasks2).where(and(...conditions)).returning();
6531
6660
  return result.length > 0;
6532
6661
  }
@@ -6546,65 +6675,65 @@ var init_task_service = __esm({
6546
6675
  });
6547
6676
 
6548
6677
  // src/core/services/task-run.service.ts
6549
- var taskRuns2, TaskRunService;
6678
+ var taskRuns3, TaskRunService;
6550
6679
  var init_task_run_service = __esm({
6551
6680
  "src/core/services/task-run.service.ts"() {
6552
6681
  "use strict";
6553
6682
  init_db2();
6554
6683
  init_drizzle_orm();
6555
- ({ taskRuns: taskRuns2 } = schema_exports);
6684
+ ({ taskRuns: taskRuns3 } = schema_exports);
6556
6685
  TaskRunService = class {
6557
6686
  static async create(data) {
6558
- const result = await db.insert(taskRuns2).values(data).returning();
6687
+ const result = await db.insert(taskRuns3).values(data).returning();
6559
6688
  return result[0];
6560
6689
  }
6561
6690
  static async updateSessionId(id, sessionId) {
6562
- const result = await db.update(taskRuns2).set({ sessionId }).where(eq(taskRuns2.id, id)).returning();
6691
+ const result = await db.update(taskRuns3).set({ sessionId }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
6563
6692
  return result[0] || null;
6564
6693
  }
6565
6694
  static async done(id, log) {
6566
- const result = await db.update(taskRuns2).set({
6695
+ const result = await db.update(taskRuns3).set({
6567
6696
  status: "done",
6568
6697
  finishedAt: /* @__PURE__ */ new Date(),
6569
6698
  log
6570
- }).where(eq(taskRuns2.id, id)).returning();
6699
+ }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
6571
6700
  return result[0] || null;
6572
6701
  }
6573
6702
  static async fail(id, log) {
6574
- const result = await db.update(taskRuns2).set({
6703
+ const result = await db.update(taskRuns3).set({
6575
6704
  status: "failed",
6576
6705
  finishedAt: /* @__PURE__ */ new Date(),
6577
6706
  log
6578
- }).where(eq(taskRuns2.id, id)).returning();
6707
+ }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
6579
6708
  return result[0] || null;
6580
6709
  }
6581
6710
  static async heartbeat(id) {
6582
- const result = await db.update(taskRuns2).set({ heartbeatAt: Date.now() }).where(and(eq(taskRuns2.id, id), eq(taskRuns2.status, "running"))).returning();
6711
+ const result = await db.update(taskRuns3).set({ heartbeatAt: Date.now() }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
6583
6712
  return result[0] || null;
6584
6713
  }
6585
6714
  static async updatePid(id, workerPid, childPid) {
6586
- const result = await db.update(taskRuns2).set({
6715
+ const result = await db.update(taskRuns3).set({
6587
6716
  workerPid,
6588
6717
  childPid,
6589
6718
  lockedAt: Date.now(),
6590
6719
  lockedBy: `gateway-${process.pid}`
6591
- }).where(eq(taskRuns2.id, id)).returning();
6720
+ }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
6592
6721
  return result[0] || null;
6593
6722
  }
6594
6723
  static async getById(id) {
6595
- const result = await db.select().from(taskRuns2).where(eq(taskRuns2.id, id));
6724
+ const result = await db.select().from(taskRuns3).where(eq(taskRuns3.id, id));
6596
6725
  return result[0] || null;
6597
6726
  }
6598
6727
  static async listByTaskId(taskId) {
6599
- return await db.select().from(taskRuns2).where(eq(taskRuns2.taskId, taskId)).orderBy(desc(taskRuns2.startedAt), desc(taskRuns2.id));
6728
+ return await db.select().from(taskRuns3).where(eq(taskRuns3.taskId, taskId)).orderBy(desc(taskRuns3.startedAt), desc(taskRuns3.id));
6600
6729
  }
6601
6730
  static async getLatestByTaskId(taskId) {
6602
- const result = await db.select().from(taskRuns2).where(eq(taskRuns2.taskId, taskId)).orderBy(desc(taskRuns2.startedAt), desc(taskRuns2.id)).limit(1);
6731
+ const result = await db.select().from(taskRuns3).where(eq(taskRuns3.taskId, taskId)).orderBy(desc(taskRuns3.startedAt), desc(taskRuns3.id)).limit(1);
6603
6732
  return result[0] || null;
6604
6733
  }
6605
6734
  static async getLatestByTaskIds(taskIds) {
6606
6735
  if (taskIds.length === 0) return /* @__PURE__ */ new Map();
6607
- const latestRuns = await db.select().from(taskRuns2).where(inArray(taskRuns2.taskId, taskIds)).orderBy(desc(taskRuns2.startedAt));
6736
+ const latestRuns = await db.select().from(taskRuns3).where(inArray(taskRuns3.taskId, taskIds)).orderBy(desc(taskRuns3.startedAt), desc(taskRuns3.id));
6608
6737
  const result = /* @__PURE__ */ new Map();
6609
6738
  for (const run of latestRuns) {
6610
6739
  if (!result.has(run.taskId)) {
@@ -6618,22 +6747,23 @@ var init_task_run_service = __esm({
6618
6747
  const cutoffSec = Math.floor(cutoffMs / 1e3);
6619
6748
  const { tasks: tasksTable } = schema_exports;
6620
6749
  const result = await db.select({
6621
- runId: taskRuns2.id,
6622
- taskId: taskRuns2.taskId,
6623
- childPid: taskRuns2.childPid,
6750
+ runId: taskRuns3.id,
6751
+ taskId: taskRuns3.taskId,
6752
+ childPid: taskRuns3.childPid,
6624
6753
  taskRetryCount: tasksTable.retryCount,
6625
- taskMaxRetries: tasksTable.maxRetries
6626
- }).from(taskRuns2).innerJoin(tasksTable, eq(taskRuns2.taskId, tasksTable.id)).where(
6754
+ taskMaxRetries: tasksTable.maxRetries,
6755
+ taskRetryBackoffMs: tasksTable.retryBackoffMs
6756
+ }).from(taskRuns3).innerJoin(tasksTable, eq(taskRuns3.taskId, tasksTable.id)).where(
6627
6757
  and(
6628
- eq(taskRuns2.status, "running"),
6758
+ eq(taskRuns3.status, "running"),
6629
6759
  or(
6630
6760
  and(
6631
- sql`${taskRuns2.heartbeatAt} IS NULL`,
6632
- sql`${taskRuns2.startedAt} < ${cutoffSec}`
6761
+ sql`${taskRuns3.heartbeatAt} IS NULL`,
6762
+ sql`${taskRuns3.startedAt} < ${cutoffSec}`
6633
6763
  ),
6634
6764
  and(
6635
- sql`${taskRuns2.heartbeatAt} IS NOT NULL`,
6636
- sql`${taskRuns2.heartbeatAt} < ${cutoffMs}`
6765
+ sql`${taskRuns3.heartbeatAt} IS NOT NULL`,
6766
+ sql`${taskRuns3.heartbeatAt} < ${cutoffMs}`
6637
6767
  )
6638
6768
  )
6639
6769
  )
@@ -6643,25 +6773,111 @@ var init_task_run_service = __esm({
6643
6773
  taskId: row.taskId,
6644
6774
  childPid: row.childPid,
6645
6775
  taskRetryCount: row.taskRetryCount ?? 0,
6646
- taskMaxRetries: row.taskMaxRetries ?? 3
6776
+ taskMaxRetries: row.taskMaxRetries ?? 3,
6777
+ taskRetryBackoffMs: row.taskRetryBackoffMs ?? 3e4
6647
6778
  }));
6648
6779
  }
6649
6780
  static async getRunningRunByTaskId(taskId) {
6650
- 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);
6781
+ 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);
6651
6782
  return result[0] || null;
6652
6783
  }
6653
6784
  static async deleteByTaskIds(taskIds) {
6654
6785
  if (taskIds.length === 0) return 0;
6655
- const result = await db.delete(taskRuns2).where(inArray(taskRuns2.taskId, taskIds)).returning();
6786
+ const result = await db.delete(taskRuns3).where(inArray(taskRuns3.taskId, taskIds)).returning();
6656
6787
  return result.length;
6657
6788
  }
6658
6789
  static async getAllRunningRuns() {
6659
- return await db.select().from(taskRuns2).where(eq(taskRuns2.status, "running"));
6790
+ return await db.select().from(taskRuns3).where(eq(taskRuns3.status, "running"));
6660
6791
  }
6661
6792
  };
6662
6793
  }
6663
6794
  });
6664
6795
 
6796
+ // src/gateway/health.ts
6797
+ function initializeGatewayHealth(config) {
6798
+ const now = Date.now();
6799
+ state = {
6800
+ startedAt: now,
6801
+ config,
6802
+ lastActivityAt: {
6803
+ worker: now,
6804
+ scheduler: now,
6805
+ watchdog: now
6806
+ }
6807
+ };
6808
+ }
6809
+ function markGatewayActivity(component) {
6810
+ if (state) state.lastActivityAt[component] = Date.now();
6811
+ }
6812
+ function resetGatewayHealth() {
6813
+ state = null;
6814
+ }
6815
+ function componentStatus(component, enabled, maxAgeMs, now) {
6816
+ const lastActivityAt = state?.lastActivityAt[component] ?? null;
6817
+ const ageMs = lastActivityAt == null ? null : Math.max(0, now - lastActivityAt);
6818
+ return {
6819
+ enabled,
6820
+ healthy: !enabled || ageMs != null && ageMs <= maxAgeMs,
6821
+ lastActivityAt,
6822
+ ageMs,
6823
+ maxAgeMs
6824
+ };
6825
+ }
6826
+ function getGatewayHealth(now = Date.now()) {
6827
+ const worker = componentStatus(
6828
+ "worker",
6829
+ true,
6830
+ Math.max((state?.config.workerPollIntervalMs ?? 1e3) * 5, 5e3),
6831
+ now
6832
+ );
6833
+ const scheduler = componentStatus(
6834
+ "scheduler",
6835
+ state?.config.schedulerEnabled ?? false,
6836
+ Math.max((state?.config.schedulerCheckIntervalMs ?? 1e3) * 5, 5e3),
6837
+ now
6838
+ );
6839
+ const watchdog = componentStatus(
6840
+ "watchdog",
6841
+ true,
6842
+ Math.max((state?.config.watchdogCheckIntervalMs ?? 6e4) * 3, 5e3),
6843
+ now
6844
+ );
6845
+ let lock = { pid: null, heartbeatAt: null, readyAt: null, ageMs: null, healthy: false };
6846
+ try {
6847
+ const row = sqlite.prepare(
6848
+ "SELECT pid, heartbeat_at, ready_at FROM gateway_lock WHERE id = 1"
6849
+ ).get();
6850
+ if (row) {
6851
+ const ageMs = Math.max(0, now - row.heartbeat_at);
6852
+ lock = {
6853
+ pid: row.pid,
6854
+ heartbeatAt: row.heartbeat_at,
6855
+ readyAt: row.ready_at,
6856
+ ageMs,
6857
+ healthy: row.pid === process.pid && row.ready_at != null && ageMs < LOCK_STALE_MS
6858
+ };
6859
+ }
6860
+ } catch {
6861
+ }
6862
+ const healthy = state != null && lock.healthy && worker.healthy && scheduler.healthy && watchdog.healthy;
6863
+ return {
6864
+ status: healthy ? "ok" : "degraded",
6865
+ pid: process.pid,
6866
+ uptimeMs: state ? Math.max(0, now - state.startedAt) : 0,
6867
+ lock,
6868
+ components: { worker, scheduler, watchdog }
6869
+ };
6870
+ }
6871
+ var LOCK_STALE_MS, state;
6872
+ var init_health = __esm({
6873
+ "src/gateway/health.ts"() {
6874
+ "use strict";
6875
+ init_db2();
6876
+ LOCK_STALE_MS = 3e4;
6877
+ state = null;
6878
+ }
6879
+ });
6880
+
6665
6881
  // node_modules/cron-parser/dist/fields/types.js
6666
6882
  var require_types = __commonJS({
6667
6883
  "node_modules/cron-parser/dist/fields/types.js"(exports) {
@@ -6862,7 +7078,7 @@ var require_CronField = __commonJS({
6862
7078
  if (!isValidRange) {
6863
7079
  throw new Error(`${this.constructor.name} Validation error, got value ${badValue} expected range ${this.min}-${this.max}${charsString}`);
6864
7080
  }
6865
- const duplicate = this.#values.find((value, index) => this.#values.indexOf(value) !== index);
7081
+ const duplicate = this.#values.find((value, index2) => this.#values.indexOf(value) !== index2);
6866
7082
  if (duplicate) {
6867
7083
  throw new Error(`${this.constructor.name} Validation error, duplicate values found: ${duplicate}`);
6868
7084
  }
@@ -14707,11 +14923,11 @@ var require_CronFieldCollection = __commonJS({
14707
14923
  throw new Error("Unexpected range end");
14708
14924
  }
14709
14925
  if (step * multiplier > range.end) {
14710
- const mapFn = (_, index) => {
14926
+ const mapFn = (_, index2) => {
14711
14927
  if (typeof range.start !== "number") {
14712
14928
  throw new Error("Unexpected range start");
14713
14929
  }
14714
- return index % step === 0 ? range.start + index : null;
14930
+ return index2 % step === 0 ? range.start + index2 : null;
14715
14931
  };
14716
14932
  if (typeof range.start !== "number") {
14717
14933
  throw new Error("Unexpected range start");
@@ -15603,9 +15819,9 @@ var require_CronExpressionParser = __commonJS({
15603
15819
  if (field === CronUnit.DayOfWeek && max % 7 === 0) {
15604
15820
  stack.push(0);
15605
15821
  }
15606
- for (let index = min; index <= max; index += repeatInterval) {
15607
- if (stack.indexOf(index) === -1) {
15608
- stack.push(index);
15822
+ for (let index2 = min; index2 <= max; index2 += repeatInterval) {
15823
+ if (stack.indexOf(index2) === -1) {
15824
+ stack.push(index2);
15609
15825
  }
15610
15826
  }
15611
15827
  return stack;
@@ -15828,11 +16044,6 @@ var require_dist = __commonJS({
15828
16044
  });
15829
16045
 
15830
16046
  // src/core/cron-parser.ts
15831
- var cron_parser_exports = {};
15832
- __export(cron_parser_exports, {
15833
- getNextCronRun: () => getNextCronRun,
15834
- isValidCronExpr: () => isValidCronExpr
15835
- });
15836
16047
  function getNextCronRun(expr, afterMs) {
15837
16048
  try {
15838
16049
  const fromDate = afterMs != null ? new Date(afterMs) : /* @__PURE__ */ new Date();
@@ -15870,6 +16081,7 @@ var init_task_template_service = __esm({
15870
16081
  ({ taskTemplates: taskTemplates2 } = schema_exports);
15871
16082
  TaskTemplateService = class {
15872
16083
  static async create(data) {
16084
+ this.validate(data);
15873
16085
  const now = Date.now();
15874
16086
  const result = await db.insert(taskTemplates2).values({ ...data, createdAt: now, updatedAt: now }).returning();
15875
16087
  const tmpl = result[0];
@@ -15885,8 +16097,38 @@ var init_task_template_service = __esm({
15885
16097
  }
15886
16098
  return tmpl;
15887
16099
  }
16100
+ static validate(data) {
16101
+ if (!data.name.trim()) throw new Error("name \u4E0D\u80FD\u4E3A\u7A7A");
16102
+ if (!data.agent.trim()) throw new Error("agent \u4E0D\u80FD\u4E3A\u7A7A");
16103
+ if (!data.prompt.trim()) throw new Error("prompt \u4E0D\u80FD\u4E3A\u7A7A");
16104
+ const scheduleType = data.scheduleType;
16105
+ if (!["cron", "delayed", "recurring"].includes(scheduleType)) {
16106
+ throw new Error("scheduleType \u5FC5\u987B\u662F cron\u3001delayed \u6216 recurring");
16107
+ }
16108
+ if (scheduleType === "cron" && (!data.cronExpr || !isValidCronExpr(data.cronExpr))) {
16109
+ throw new Error("cronExpr \u7F3A\u5931\u6216\u683C\u5F0F\u65E0\u6548");
16110
+ }
16111
+ if (scheduleType === "recurring" && (!Number.isInteger(data.intervalMs) || (data.intervalMs ?? 0) <= 0)) {
16112
+ throw new Error("intervalMs \u5FC5\u987B\u662F\u6B63\u6574\u6570");
16113
+ }
16114
+ if (scheduleType === "delayed" && (!Number.isInteger(data.runAt) || (data.runAt ?? 0) <= 0)) {
16115
+ throw new Error("runAt \u5FC5\u987B\u662F\u6B63\u6574\u6570\u65F6\u95F4\u6233");
16116
+ }
16117
+ this.validateInteger("importance", data.importance, 1, 5);
16118
+ this.validateInteger("urgency", data.urgency, 1, 5);
16119
+ this.validateInteger("maxInstances", data.maxInstances, 1, 1e3);
16120
+ this.validateInteger("maxRetries", data.maxRetries, 0, 1e3);
16121
+ this.validateInteger("retryBackoffMs", data.retryBackoffMs, 0, 864e5);
16122
+ this.validateInteger("timeoutMs", data.timeoutMs, 1e3, 6048e5);
16123
+ }
16124
+ static validateInteger(name, value, min, max) {
16125
+ if (value === void 0 || value === null) return;
16126
+ if (!Number.isInteger(value) || value < min || value > max) {
16127
+ throw new Error(`${name} \u5FC5\u987B\u662F ${min} \u5230 ${max} \u4E4B\u95F4\u7684\u6574\u6570`);
16128
+ }
16129
+ }
15888
16130
  static async list(limit = 50) {
15889
- return await db.select().from(taskTemplates2).orderBy(desc(taskTemplates2.createdAt)).limit(limit);
16131
+ return await db.select().from(taskTemplates2).orderBy(desc(taskTemplates2.createdAt), desc(taskTemplates2.id)).limit(limit);
15890
16132
  }
15891
16133
  static async getById(id) {
15892
16134
  const result = await db.select().from(taskTemplates2).where(eq(taskTemplates2.id, id));
@@ -15933,13 +16175,13 @@ var init_compose = __esm({
15933
16175
  "use strict";
15934
16176
  compose = (middleware, onError, onNotFound) => {
15935
16177
  return (context, next) => {
15936
- let index = -1;
16178
+ let index2 = -1;
15937
16179
  return dispatch(0);
15938
16180
  async function dispatch(i) {
15939
- if (i <= index) {
16181
+ if (i <= index2) {
15940
16182
  throw new Error("next() called multiple times");
15941
16183
  }
15942
- index = i;
16184
+ index2 = i;
15943
16185
  let res;
15944
16186
  let isError = false;
15945
16187
  let handler;
@@ -16054,8 +16296,8 @@ var init_body = __esm({
16054
16296
  handleParsingNestedValues = (form, key, value) => {
16055
16297
  let nestedForm = form;
16056
16298
  const keys = key.split(".");
16057
- keys.forEach((key2, index) => {
16058
- if (index === keys.length - 1) {
16299
+ keys.forEach((key2, index2) => {
16300
+ if (index2 === keys.length - 1) {
16059
16301
  nestedForm[key2] = value;
16060
16302
  } else {
16061
16303
  if (!nestedForm[key2] || typeof nestedForm[key2] !== "object" || Array.isArray(nestedForm[key2]) || nestedForm[key2] instanceof File) {
@@ -16087,8 +16329,8 @@ var init_url = __esm({
16087
16329
  };
16088
16330
  extractGroupsFromPath = (path) => {
16089
16331
  const groups = [];
16090
- path = path.replace(/\{[^}]+\}/g, (match2, index) => {
16091
- const mark = `@${index}`;
16332
+ path = path.replace(/\{[^}]+\}/g, (match2, index2) => {
16333
+ const mark = `@${index2}`;
16092
16334
  groups.push([mark, match2]);
16093
16335
  return mark;
16094
16336
  });
@@ -16607,10 +16849,10 @@ var init_html = __esm({
16607
16849
  return;
16608
16850
  }
16609
16851
  let escape;
16610
- let index;
16852
+ let index2;
16611
16853
  let lastIndex = 0;
16612
- for (index = match2; index < str.length; index++) {
16613
- switch (str.charCodeAt(index)) {
16854
+ for (index2 = match2; index2 < str.length; index2++) {
16855
+ switch (str.charCodeAt(index2)) {
16614
16856
  case 34:
16615
16857
  escape = "&quot;";
16616
16858
  break;
@@ -16629,10 +16871,10 @@ var init_html = __esm({
16629
16871
  default:
16630
16872
  continue;
16631
16873
  }
16632
- buffer[0] += str.substring(lastIndex, index) + escape;
16633
- lastIndex = index + 1;
16874
+ buffer[0] += str.substring(lastIndex, index2) + escape;
16875
+ lastIndex = index2 + 1;
16634
16876
  }
16635
- buffer[0] += str.substring(lastIndex, index);
16877
+ buffer[0] += str.substring(lastIndex, index2);
16636
16878
  };
16637
16879
  resolveCallbackSync = (str) => {
16638
16880
  const callbacks = str.callbacks;
@@ -17508,8 +17750,8 @@ function match(method, path) {
17508
17750
  if (!match3) {
17509
17751
  return [[], emptyParam];
17510
17752
  }
17511
- const index = match3.indexOf("", 1);
17512
- return [matcher[1][index], match3];
17753
+ const index2 = match3.indexOf("", 1);
17754
+ return [matcher[1][index2], match3];
17513
17755
  });
17514
17756
  this.match = match2;
17515
17757
  return match2(method, path);
@@ -17556,7 +17798,7 @@ var init_node = __esm({
17556
17798
  #index;
17557
17799
  #varIndex;
17558
17800
  #children = /* @__PURE__ */ Object.create(null);
17559
- insert(tokens, index, paramMap, context, pathErrorCheckOnly) {
17801
+ insert(tokens, index2, paramMap, context, pathErrorCheckOnly) {
17560
17802
  if (tokens.length === 0) {
17561
17803
  if (this.#index !== void 0) {
17562
17804
  throw PATH_ERROR;
@@ -17564,7 +17806,7 @@ var init_node = __esm({
17564
17806
  if (pathErrorCheckOnly) {
17565
17807
  return;
17566
17808
  }
17567
- this.#index = index;
17809
+ this.#index = index2;
17568
17810
  return;
17569
17811
  }
17570
17812
  const [token, ...restTokens] = tokens;
@@ -17614,7 +17856,7 @@ var init_node = __esm({
17614
17856
  node = this.#children[token] = new _Node();
17615
17857
  }
17616
17858
  }
17617
- node.insert(restTokens, index, paramMap, context, pathErrorCheckOnly);
17859
+ node.insert(restTokens, index2, paramMap, context, pathErrorCheckOnly);
17618
17860
  }
17619
17861
  buildRegExpStr() {
17620
17862
  const childKeys = Object.keys(this.#children).sort(compareKey);
@@ -17646,7 +17888,7 @@ var init_trie = __esm({
17646
17888
  Trie = class {
17647
17889
  #context = { varIndex: 0 };
17648
17890
  #root = new Node();
17649
- insert(path, index, pathErrorCheckOnly) {
17891
+ insert(path, index2, pathErrorCheckOnly) {
17650
17892
  const paramAssoc = [];
17651
17893
  const groups = [];
17652
17894
  for (let i = 0; ; ) {
@@ -17672,7 +17914,7 @@ var init_trie = __esm({
17672
17914
  }
17673
17915
  }
17674
17916
  }
17675
- this.#root.insert(tokens, index, paramAssoc, this.#context, pathErrorCheckOnly);
17917
+ this.#root.insert(tokens, index2, paramAssoc, this.#context, pathErrorCheckOnly);
17676
17918
  return paramAssoc;
17677
17919
  }
17678
17920
  buildRegExp() {
@@ -18266,8 +18508,19 @@ __export(web_exports, {
18266
18508
  dashboardApp: () => dashboardApp,
18267
18509
  default: () => web_default
18268
18510
  });
18269
- import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
18511
+ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync, mkdirSync as mkdirSync2, renameSync } from "fs";
18270
18512
  import { dirname as dirname2 } from "path";
18513
+ function parsePositiveInteger(value) {
18514
+ if (!/^\d+$/.test(value)) return null;
18515
+ const parsed = Number(value);
18516
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
18517
+ }
18518
+ function parseTaskStatus(value) {
18519
+ return TASK_STATUSES.has(value) ? value : null;
18520
+ }
18521
+ function safeStatus(value) {
18522
+ return value && TASK_STATUSES.has(value) ? value : "unknown";
18523
+ }
18271
18524
  function formatDuration(startAt, endAt) {
18272
18525
  if (!startAt) return "-";
18273
18526
  const start = new Date(startAt).getTime();
@@ -18305,17 +18558,21 @@ function esc(s) {
18305
18558
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
18306
18559
  }
18307
18560
  function readCurrentConfig() {
18308
- if (!existsSync3(CONFIG_PATH)) return {};
18561
+ const configPath = getConfigPath();
18562
+ if (!existsSync3(configPath)) return {};
18309
18563
  try {
18310
- return JSON.parse(readFileSync2(CONFIG_PATH, "utf-8"));
18564
+ return JSON.parse(readFileSync2(configPath, "utf-8"));
18311
18565
  } catch {
18312
18566
  return {};
18313
18567
  }
18314
18568
  }
18315
18569
  function writeConfig(cfg) {
18316
- const dir = dirname2(CONFIG_PATH);
18570
+ const configPath = getConfigPath();
18571
+ const dir = dirname2(configPath);
18317
18572
  if (!existsSync3(dir)) mkdirSync2(dir, { recursive: true });
18318
- writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2) + "\n");
18573
+ const tempPath = `${configPath}.${process.pid}.tmp`;
18574
+ writeFileSync(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
18575
+ renameSync(tempPath, configPath);
18319
18576
  }
18320
18577
  function renderLayout(title, activeTab, body) {
18321
18578
  return `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>${title} - SuperTask</title>${SHARED_STYLES}
@@ -18354,11 +18611,11 @@ async function saveConfig(){
18354
18611
  scheduler:{
18355
18612
  enabled:form.se.checked,
18356
18613
  checkIntervalMs:Number(form.si.value),
18357
- catchUp:form.cu.value,
18358
18614
  },
18359
18615
  watchdog:{
18360
18616
  heartbeatTimeoutMs:Number(form.wt.value)*1000,
18361
- cleanupIntervalMs:Number(form.wc.value)*1000,
18617
+ checkIntervalMs:Number(form.wci.value)*1000,
18618
+ cleanupIntervalMs:Number(form.wcl.value)*3600000,
18362
18619
  retentionDays:Number(form.rd.value),
18363
18620
  }
18364
18621
  };
@@ -18389,7 +18646,7 @@ async function saveConfig(){
18389
18646
  <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>
18390
18647
  </body></html>`;
18391
18648
  }
18392
- var app, SHARED_STYLES, dashboardApp, web_default;
18649
+ var app, TASK_STATUSES, SHARED_STYLES, dashboardApp, web_default;
18393
18650
  var init_web = __esm({
18394
18651
  "src/web/index.tsx"() {
18395
18652
  "use strict";
@@ -18401,7 +18658,47 @@ var init_web = __esm({
18401
18658
  init_drizzle_orm();
18402
18659
  init_db2();
18403
18660
  init_config();
18661
+ init_health();
18404
18662
  app = new Hono2();
18663
+ TASK_STATUSES = /* @__PURE__ */ new Set([
18664
+ "pending",
18665
+ "running",
18666
+ "done",
18667
+ "failed",
18668
+ "dead_letter",
18669
+ "cancelled"
18670
+ ]);
18671
+ app.use("*", async (c, next) => {
18672
+ await next();
18673
+ c.header("X-Content-Type-Options", "nosniff");
18674
+ c.header("X-Frame-Options", "DENY");
18675
+ c.header("Referrer-Policy", "no-referrer");
18676
+ });
18677
+ app.use("/api/*", async (c, next) => {
18678
+ if (["GET", "HEAD", "OPTIONS"].includes(c.req.method)) return next();
18679
+ const fetchSite = c.req.header("Sec-Fetch-Site");
18680
+ if (fetchSite && !["same-origin", "none"].includes(fetchSite)) {
18681
+ return c.json({ error: "cross-site request rejected" }, 403);
18682
+ }
18683
+ const origin = c.req.header("Origin");
18684
+ if (origin) {
18685
+ try {
18686
+ const originUrl = new URL(origin);
18687
+ const requestUrl = new URL(c.req.url);
18688
+ const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(originUrl.hostname);
18689
+ if (!loopback || originUrl.origin !== requestUrl.origin) {
18690
+ return c.json({ error: "cross-site request rejected" }, 403);
18691
+ }
18692
+ } catch {
18693
+ return c.json({ error: "invalid origin" }, 403);
18694
+ }
18695
+ }
18696
+ return next();
18697
+ });
18698
+ app.get("/health", (c) => {
18699
+ const health = getGatewayHealth();
18700
+ return c.json(health, health.status === "ok" ? 200 : 503);
18701
+ });
18405
18702
  SHARED_STYLES = html`
18406
18703
  <style>
18407
18704
  :root { --bg:#0d1117; --card:#161b22; --border:#30363d; --t1:#c9d1d9; --t2:#8b949e; --green:#238636; --red:#da3633; --yellow:#d29922; --blue:#1f6feb; --purple:#8957e5; }
@@ -18478,12 +18775,16 @@ var init_web = __esm({
18478
18775
  </style>
18479
18776
  `;
18480
18777
  app.get("/", async (c) => {
18481
- const page = Number(c.req.query("page") || "1");
18778
+ const pageParam = c.req.query("page") || "1";
18779
+ const page = parsePositiveInteger(pageParam);
18780
+ if (page === null) return c.text("invalid page", 400);
18482
18781
  const statusFilter = c.req.query("status") || "";
18782
+ const parsedStatus = statusFilter ? parseTaskStatus(statusFilter) : null;
18783
+ if (statusFilter && !parsedStatus) return c.text("invalid status", 400);
18483
18784
  const limit = 50;
18484
18785
  const offset = (page - 1) * limit;
18485
18786
  const [tasks3, statsData] = await Promise.all([
18486
- TaskService.list({ limit, offset, ...statusFilter ? { status: statusFilter } : {} }),
18787
+ TaskService.list({ limit, offset, ...parsedStatus ? { status: parsedStatus } : {} }),
18487
18788
  TaskService.stats({})
18488
18789
  ]);
18489
18790
  const taskIds = tasks3.map((t) => t.id);
@@ -18506,12 +18807,13 @@ var init_web = __esm({
18506
18807
  </div>`;
18507
18808
  let rows = "";
18508
18809
  for (const task of tasks3) {
18509
- const st = (task.status ?? "").toUpperCase();
18810
+ const status = safeStatus(task.status);
18811
+ const st = status.toUpperCase();
18510
18812
  rows += `<tr>
18511
18813
  <td class="mu">#${task.id}</td>
18512
18814
  <td><div style="font-weight:500">${esc(task.name)}</div><div class="mu sm el">${esc(task.prompt.substring(0, 120))}</div></td>
18513
18815
  <td><span class="tag">${esc(task.agent)}</span></td>
18514
- <td><span class="badge b-${task.status}">${st}</span></td>
18816
+ <td><span class="badge b-${status}">${st}</span></td>
18515
18817
  <td class="sm ${task.status === "running" ? "" : "mu"}">${formatDuration(task.startedAt, task.finishedAt)}</td>
18516
18818
  <td class="mu sm">${(task.retryCount ?? 0) > 0 ? task.retryCount : "-"}</td>
18517
18819
  <td>
@@ -18543,21 +18845,13 @@ var init_web = __esm({
18543
18845
  });
18544
18846
  app.get("/templates", async (c) => {
18545
18847
  const templates = await TaskTemplateService.list(100);
18546
- for (const tmpl of templates) {
18547
- if (tmpl.enabled && tmpl.scheduleType === "cron" && tmpl.cronExpr) {
18548
- try {
18549
- const { getNextCronRun: getNextCronRun2 } = await Promise.resolve().then(() => (init_cron_parser(), cron_parser_exports));
18550
- tmpl.nextRunAt = getNextCronRun2(tmpl.cronExpr, Date.now());
18551
- } catch {
18552
- }
18553
- }
18554
- }
18555
18848
  const enabled = templates.filter((t) => t.enabled).length;
18556
18849
  const disabled = templates.length - enabled;
18557
18850
  let rows = "";
18558
18851
  for (const t of templates) {
18559
- const typeLabel = t.scheduleType === "cron" ? "Cron" : t.scheduleType === "recurring" ? "\u5FAA\u73AF" : "\u5B9A\u65F6";
18560
- const typeClass = "tag t-" + t.scheduleType;
18852
+ const scheduleType = ["cron", "recurring", "delayed"].includes(t.scheduleType) ? t.scheduleType : "unknown";
18853
+ const typeLabel = scheduleType === "cron" ? "Cron" : scheduleType === "recurring" ? "\u5FAA\u73AF" : scheduleType === "delayed" ? "\u5B9A\u65F6" : "\u672A\u77E5";
18854
+ const typeClass = "tag t-" + scheduleType;
18561
18855
  let rule = "-";
18562
18856
  if (t.scheduleType === "cron") rule = t.cronExpr || "-";
18563
18857
  else if (t.scheduleType === "recurring") rule = t.intervalMs ? `${Math.floor(t.intervalMs / 6e4)}\u5206\u949F` : "-";
@@ -18569,7 +18863,7 @@ var init_web = __esm({
18569
18863
  <td><div style="font-weight:500">${esc(t.name)}</div><div class="mu sm el">${esc(t.prompt.substring(0, 100))}</div>
18570
18864
  <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>
18571
18865
  <td><span class="${typeClass}">${typeLabel}</span></td>
18572
- <td class="m sm">${rule}</td>
18866
+ <td class="m sm">${esc(rule)}</td>
18573
18867
  <td>${statusBadge}</td>
18574
18868
  <td class="sm">${t.lastRunAt ? timeAgo(t.lastRunAt) : "-"}</td>
18575
18869
  <td class="sm">${t.nextRunAt ? timeUntil(t.nextRunAt) : "-"}</td>
@@ -18597,7 +18891,8 @@ var init_web = __esm({
18597
18891
  return c.html(renderLayout("\u5B9A\u65F6\u4EFB\u52A1", "templates", body));
18598
18892
  });
18599
18893
  app.get("/runs", async (c) => {
18600
- const page = Number(c.req.query("page") || "1");
18894
+ const page = parsePositiveInteger(c.req.query("page") || "1");
18895
+ if (page === null) return c.text("invalid page", 400);
18601
18896
  const limit = 50;
18602
18897
  const offset = (page - 1) * limit;
18603
18898
  const { taskRuns: tr, tasks: tk } = schema_exports;
@@ -18615,13 +18910,14 @@ var init_web = __esm({
18615
18910
  childPid: tr.childPid,
18616
18911
  taskName: tk.name,
18617
18912
  taskAgent: tk.agent
18618
- }).from(tr).innerJoin(tk, eq(tr.taskId, tk.id)).orderBy(desc(tr.startedAt)).limit(limit).offset(offset);
18913
+ }).from(tr).innerJoin(tk, eq(tr.taskId, tk.id)).orderBy(desc(tr.startedAt), desc(tr.id)).limit(limit).offset(offset);
18619
18914
  const totalResult = await db.select({ count: sql`count(*)` }).from(tr);
18620
18915
  const total = Number(totalResult[0]?.count ?? 0);
18621
18916
  const totalPages = Math.ceil(total / limit);
18622
18917
  let rows = "";
18623
18918
  const logsHtml = [];
18624
18919
  for (const run of runs) {
18920
+ const status = safeStatus(run.status);
18625
18921
  const shortSession = run.sessionId ? run.sessionId.slice(4, 7) + "***" + run.sessionId.slice(-3) : "-";
18626
18922
  const logBtn = run.log ? `<button class="btn btn-sm" onclick="toggleLog(${run.id})">\u65E5\u5FD7</button>` : "";
18627
18923
  rows += `<tr>
@@ -18629,15 +18925,15 @@ var init_web = __esm({
18629
18925
  <td><div style="font-weight:500">${esc(run.taskName)} <span class="mu">(#${run.taskId})</span></div>
18630
18926
  ${run.model ? `<div class="sm"><span class="tag">${esc(run.model)}</span></div>` : ""}</td>
18631
18927
  <td><span class="tag">${esc(run.taskAgent)}</span></td>
18632
- <td><span class="badge b-${run.status}">${(run.status ?? "").toUpperCase()}</span></td>
18928
+ <td><span class="badge b-${status}">${status.toUpperCase()}</span></td>
18633
18929
  <td class="sm">${formatDuration(run.startedAt, run.finishedAt)}</td>
18634
18930
  <td class="sm mu">${run.heartbeatAt ? timeAgo(run.heartbeatAt) : "-"}</td>
18635
18931
  <td><button class="btn btn-sm" onclick="showRunDetail(${run.id})">\u8BE6\u60C5</button>${logBtn}</td>
18636
18932
  </tr>`;
18637
18933
  if (run.log) {
18638
18934
  logsHtml.push(`<div id="log-${run.id}" style="display:none" class="mt8">
18639
- <div class="panel"><div class="ph"><h3>Run #${run.id} \u65E5\u5FD7 \u2014 ${run.taskName}</h3></div>
18640
- <div class="log-box">${run.log.replace(/</g, "&lt;")}</div></div></div>`);
18935
+ <div class="panel"><div class="ph"><h3>Run #${run.id} \u65E5\u5FD7 \u2014 ${esc(run.taskName)}</h3></div>
18936
+ <div class="log-box">${esc(run.log)}</div></div></div>`);
18641
18937
  }
18642
18938
  }
18643
18939
  let paging = `<div class="pn">`;
@@ -18663,17 +18959,18 @@ var init_web = __esm({
18663
18959
  });
18664
18960
  app.get("/system", async (c) => {
18665
18961
  const config = loadConfig();
18962
+ const configPath = getConfigPath();
18666
18963
  const stats = await TaskService.stats({});
18667
18964
  const runningRuns = await TaskRunService.getAllRunningRuns();
18668
18965
  const templates = await TaskTemplateService.list(100);
18669
- const configExists = existsSync3(CONFIG_PATH);
18966
+ const configExists = existsSync3(configPath);
18670
18967
  let runRows = "";
18671
18968
  if (runningRuns.length > 0) {
18672
18969
  for (const run of runningRuns) {
18673
18970
  const shortS = run.sessionId ? run.sessionId.slice(4, 7) + "***" + run.sessionId.slice(-3) : "-";
18674
18971
  runRows += `<tr>
18675
18972
  <td class="mu">#${run.id}</td><td>#${run.taskId}</td>
18676
- <td class="m sm">${shortS}</td>
18973
+ <td class="m sm">${esc(shortS)}</td>
18677
18974
  <td class="sm">${esc(run.model) || "-"}</td>
18678
18975
  <td class="sm">${formatDate(run.startedAt)}</td>
18679
18976
  <td class="sm">${run.heartbeatAt ? timeAgo(run.heartbeatAt) : "-"}</td>
@@ -18698,19 +18995,14 @@ var init_web = __esm({
18698
18995
  <h3 style="margin:0 0 12px;font-size:14px">Scheduler \u914D\u7F6E</h3>
18699
18996
  <div class="form-row"><label>\u542F\u7528\u8C03\u5EA6</label><input type="checkbox" name="se" ${config.scheduler.enabled ? "checked" : ""}></div>
18700
18997
  <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>
18701
- <div class="form-row"><label>\u8FFD\u8D76\u6A21\u5F0F</label><select name="cu" style="width:100px">
18702
- <option value="next" ${config.scheduler.catchUp === "next" ? "selected" : ""}>next</option>
18703
- <option value="all" ${config.scheduler.catchUp === "all" ? "selected" : ""}>all</option>
18704
- <option value="latest" ${config.scheduler.catchUp === "latest" ? "selected" : ""}>latest</option>
18705
- </select></div>
18706
18998
  <div class="ir"><span class="ik">\u6D3B\u8DC3\u6A21\u677F</span><span class="iv">${templates.filter((t) => t.enabled).length} / ${templates.length}</span></div>
18707
18999
  </div>
18708
19000
  <div class="card">
18709
19001
  <h3 style="margin:0 0 12px;font-size:14px">Watchdog \u914D\u7F6E</h3>
18710
19002
  <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>
18711
- <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>
19003
+ <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>
19004
+ <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>
18712
19005
  <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>
18713
- <div class="ir"><span class="ik">\u65E5\u5FD7\u683C\u5F0F</span><span class="iv">${config.logging.format}</span></div>
18714
19006
  </div>
18715
19007
  </div>
18716
19008
  <div style="text-align:center;margin-bottom:24px">
@@ -18739,7 +19031,7 @@ var init_web = __esm({
18739
19031
 
18740
19032
  <div class="card mt16">
18741
19033
  <h3 style="margin:0 0 12px;font-size:14px">\u914D\u7F6E\u6587\u4EF6</h3>
18742
- <div class="ir"><span class="ik">\u8DEF\u5F84</span><span class="iv m sm">${CONFIG_PATH}</span></div>
19034
+ <div class="ir"><span class="ik">\u8DEF\u5F84</span><span class="iv m sm">${esc(configPath)}</span></div>
18743
19035
  <div class="ir"><span class="ik">\u6587\u4EF6\u5B58\u5728</span><span class="iv">${configFileStatus}</span></div>
18744
19036
  </div>
18745
19037
 
@@ -18751,46 +19043,61 @@ var init_web = __esm({
18751
19043
  return c.html(renderLayout("\u7CFB\u7EDF\u72B6\u6001", "system", body));
18752
19044
  });
18753
19045
  app.get("/api/tasks/:id", async (c) => {
18754
- const id = Number(c.req.param("id"));
19046
+ const id = parsePositiveInteger(c.req.param("id"));
19047
+ if (id === null) return c.json({ error: "invalid id" }, 400);
18755
19048
  const task = await TaskService.getById(id);
18756
19049
  if (!task) return c.json({ error: "not found" }, 404);
18757
19050
  const runs = await TaskRunService.listByTaskId(id);
18758
19051
  return c.json({ ...task, _runs: runs });
18759
19052
  });
18760
19053
  app.get("/api/runs/:id", async (c) => {
18761
- const id = Number(c.req.param("id"));
19054
+ const id = parsePositiveInteger(c.req.param("id"));
19055
+ if (id === null) return c.json({ error: "invalid id" }, 400);
18762
19056
  const run = await TaskRunService.getById(id);
18763
19057
  if (!run) return c.json({ error: "not found" }, 404);
18764
19058
  return c.json(run);
18765
19059
  });
18766
19060
  app.get("/api/templates/:id", async (c) => {
18767
- const id = Number(c.req.param("id"));
19061
+ const id = parsePositiveInteger(c.req.param("id"));
19062
+ if (id === null) return c.json({ error: "invalid id" }, 400);
18768
19063
  const tmpl = await TaskTemplateService.getById(id);
18769
19064
  if (!tmpl) return c.json({ error: "not found" }, 404);
18770
19065
  return c.json(tmpl);
18771
19066
  });
18772
19067
  app.post("/api/tasks/:id/retry", async (c) => {
18773
- await TaskService.retry(Number(c.req.param("id")));
18774
- return c.json({ success: true });
19068
+ const id = parsePositiveInteger(c.req.param("id"));
19069
+ if (id === null) return c.json({ error: "invalid id" }, 400);
19070
+ const task = await TaskService.retry(id);
19071
+ if (task) return c.json({ success: true });
19072
+ return await TaskService.getById(id) ? c.json({ error: "task status does not allow retry" }, 409) : c.json({ error: "not found" }, 404);
18775
19073
  });
18776
19074
  app.delete("/api/tasks/:id", async (c) => {
18777
- await TaskService.delete(Number(c.req.param("id")));
18778
- return c.json({ success: true });
19075
+ const id = parsePositiveInteger(c.req.param("id"));
19076
+ if (id === null) return c.json({ error: "invalid id" }, 400);
19077
+ const deleted = await TaskService.delete(id);
19078
+ return deleted ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
18779
19079
  });
18780
19080
  app.post("/api/templates/:id/enable", async (c) => {
18781
- const result = await TaskTemplateService.enable(Number(c.req.param("id")));
18782
- return c.json({ success: !!result });
19081
+ const id = parsePositiveInteger(c.req.param("id"));
19082
+ if (id === null) return c.json({ error: "invalid id" }, 400);
19083
+ const result = await TaskTemplateService.enable(id);
19084
+ return result ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
18783
19085
  });
18784
19086
  app.post("/api/templates/:id/disable", async (c) => {
18785
- const result = await TaskTemplateService.disable(Number(c.req.param("id")));
18786
- return c.json({ success: !!result });
19087
+ const id = parsePositiveInteger(c.req.param("id"));
19088
+ if (id === null) return c.json({ error: "invalid id" }, 400);
19089
+ const result = await TaskTemplateService.disable(id);
19090
+ return result ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
18787
19091
  });
18788
19092
  app.delete("/api/templates/:id", async (c) => {
18789
- const ok = await TaskTemplateService.delete(Number(c.req.param("id")));
18790
- return c.json({ success: ok });
19093
+ const id = parsePositiveInteger(c.req.param("id"));
19094
+ if (id === null) return c.json({ error: "invalid id" }, 400);
19095
+ const ok = await TaskTemplateService.delete(id);
19096
+ return ok ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
18791
19097
  });
18792
19098
  app.post("/api/templates/:id/trigger", async (c) => {
18793
- const id = Number(c.req.param("id"));
19099
+ const id = parsePositiveInteger(c.req.param("id"));
19100
+ if (id === null) return c.json({ error: "invalid id" }, 400);
18794
19101
  const tmpl = await TaskTemplateService.getById(id);
18795
19102
  if (!tmpl) return c.json({ error: "not found" }, 404);
18796
19103
  const task = await TaskService.add({
@@ -18802,7 +19109,10 @@ var init_web = __esm({
18802
19109
  category: tmpl.category,
18803
19110
  importance: tmpl.importance,
18804
19111
  urgency: tmpl.urgency,
19112
+ batchId: tmpl.batchId,
18805
19113
  maxRetries: tmpl.maxRetries,
19114
+ retryBackoffMs: tmpl.retryBackoffMs,
19115
+ timeoutMs: tmpl.timeoutMs,
18806
19116
  templateId: tmpl.id
18807
19117
  });
18808
19118
  return c.json({ success: true, taskId: task.id });
@@ -18817,22 +19127,29 @@ var init_web = __esm({
18817
19127
  const bW = body.worker ?? {};
18818
19128
  const bS = body.scheduler ?? {};
18819
19129
  const bD = body.watchdog ?? {};
18820
- const merged = { ...current, ...body, worker: { ...curW, ...bW }, scheduler: { ...curS, ...bS }, watchdog: { ...curD, ...bD } };
18821
- writeConfig(merged);
19130
+ const merged = {
19131
+ ...current,
19132
+ ...body,
19133
+ configVersion: 2,
19134
+ worker: { ...curW, ...bW },
19135
+ scheduler: { ...curS, ...bS },
19136
+ watchdog: { ...curD, ...bD }
19137
+ };
19138
+ writeConfig(validateConfig(merged));
18822
19139
  return c.json({ success: true });
18823
19140
  } catch (err) {
18824
- return c.json({ success: false, error: err instanceof Error ? err.message : String(err) });
19141
+ return c.json({ success: false, error: err instanceof Error ? err.message : String(err) }, 400);
18825
19142
  }
18826
19143
  });
18827
19144
  app.post("/api/database/clear", async (c) => {
18828
19145
  try {
18829
- const { tasks: tasks3, taskRuns: taskRuns3, taskTemplates: taskTemplates4 } = schema_exports;
18830
- await db.delete(taskRuns3);
19146
+ const { tasks: tasks3, taskRuns: taskRuns4, taskTemplates: taskTemplates4 } = schema_exports;
19147
+ await db.delete(taskRuns4);
18831
19148
  await db.delete(taskTemplates4);
18832
19149
  await db.delete(tasks3);
18833
19150
  return c.json({ success: true });
18834
19151
  } catch (err) {
18835
- return c.json({ success: false, error: err instanceof Error ? err.message : String(err) });
19152
+ return c.json({ success: false, error: err instanceof Error ? err.message : String(err) }, 500);
18836
19153
  }
18837
19154
  });
18838
19155
  dashboardApp = app;
@@ -18850,39 +19167,118 @@ init_config();
18850
19167
  // src/worker/index.ts
18851
19168
  init_task_service();
18852
19169
  init_task_run_service();
19170
+ init_health();
18853
19171
  import { spawn } from "child_process";
19172
+
19173
+ // src/core/process-control.ts
19174
+ import { spawnSync } from "child_process";
19175
+ import { basename } from "path";
19176
+ function isSafePid(pid) {
19177
+ return Number.isInteger(pid) && pid > 1 && pid !== process.pid;
19178
+ }
19179
+ function inspectUnixProcess(pid) {
19180
+ const result = spawnSync("ps", ["-o", "pgid=", "-o", "command=", "-p", String(pid)], {
19181
+ encoding: "utf8",
19182
+ stdio: ["ignore", "pipe", "ignore"]
19183
+ });
19184
+ if (result.status !== 0) return null;
19185
+ const match2 = result.stdout.trim().match(/^(\d+)\s+(.+)$/s);
19186
+ if (!match2) return null;
19187
+ return { processGroupId: Number(match2[1]), command: match2[2] };
19188
+ }
19189
+ function commandMatches(command, expectedExecutable) {
19190
+ const expectedName = basename(expectedExecutable).trim().toLowerCase();
19191
+ if (!expectedName) return false;
19192
+ return command.toLowerCase().includes(expectedName);
19193
+ }
19194
+ function signalSpawnedProcessTree(pid, signal) {
19195
+ if (!isSafePid(pid)) return false;
19196
+ if (process.platform !== "win32") {
19197
+ try {
19198
+ process.kill(-pid, signal);
19199
+ return true;
19200
+ } catch {
19201
+ }
19202
+ }
19203
+ try {
19204
+ process.kill(pid, signal);
19205
+ return true;
19206
+ } catch {
19207
+ return false;
19208
+ }
19209
+ }
19210
+ function signalRecordedProcessTree(pid, signal, expectedExecutable) {
19211
+ if (!isSafePid(pid)) return false;
19212
+ if (process.platform === "win32") {
19213
+ const list = spawnSync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
19214
+ encoding: "utf8",
19215
+ stdio: ["ignore", "pipe", "ignore"]
19216
+ });
19217
+ if (list.status !== 0 || !commandMatches(list.stdout, expectedExecutable)) return false;
19218
+ const args = ["/PID", String(pid), "/T"];
19219
+ if (signal === "SIGKILL") args.push("/F");
19220
+ return spawnSync("taskkill", args, { stdio: "ignore" }).status === 0;
19221
+ }
19222
+ const info = inspectUnixProcess(pid);
19223
+ if (!info || !commandMatches(info.command, expectedExecutable)) return false;
19224
+ try {
19225
+ if (info.processGroupId === pid) process.kill(-pid, signal);
19226
+ else process.kill(pid, signal);
19227
+ return true;
19228
+ } catch {
19229
+ return false;
19230
+ }
19231
+ }
19232
+
19233
+ // src/worker/index.ts
19234
+ var DEFAULT_MAX_OUTPUT_CHARS = 64 * 1024;
19235
+ var FORBIDDEN_AGENT = "supertask-runner";
18854
19236
  var WorkerEngine = class {
18855
19237
  activeBatchIds = /* @__PURE__ */ new Set();
18856
19238
  runningTasks = /* @__PURE__ */ new Map();
18857
19239
  stopped = false;
18858
19240
  pollTimer = null;
18859
19241
  heartbeatTimer = null;
19242
+ pollCyclePromise = null;
18860
19243
  cfg;
18861
- constructor(cfg) {
19244
+ opencodeBin;
19245
+ maxOutputChars;
19246
+ constructor(cfg, options = {}) {
18862
19247
  this.cfg = cfg.worker;
19248
+ this.opencodeBin = options.opencodeBin ?? process.env.SUPERTASK_OPENCODE_BIN ?? "opencode";
19249
+ this.maxOutputChars = options.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
18863
19250
  }
18864
19251
  start() {
18865
19252
  this.stopped = false;
19253
+ markGatewayActivity("worker");
18866
19254
  this.poll();
18867
19255
  this.heartbeatTimer = setInterval(() => this.updateHeartbeats(), this.cfg.heartbeatIntervalMs);
18868
19256
  }
18869
- stop() {
19257
+ async stop(gracePeriodMs = 0) {
18870
19258
  this.stopped = true;
18871
19259
  if (this.pollTimer) {
18872
19260
  clearTimeout(this.pollTimer);
18873
19261
  this.pollTimer = null;
18874
19262
  }
19263
+ if (this.pollCyclePromise) await this.pollCyclePromise;
19264
+ if (gracePeriodMs > 0 && this.runningTasks.size > 0) {
19265
+ const deadline = Date.now() + gracePeriodMs;
19266
+ while (this.runningTasks.size > 0 && Date.now() < deadline) {
19267
+ await Bun.sleep(Math.min(50, deadline - Date.now()));
19268
+ }
19269
+ }
18875
19270
  if (this.heartbeatTimer) {
18876
19271
  clearInterval(this.heartbeatTimer);
18877
19272
  this.heartbeatTimer = null;
18878
19273
  }
19274
+ const interruptedTaskIds = [...this.runningTasks.keys()];
18879
19275
  const killPromises = [];
18880
- for (const [, entry] of this.runningTasks) {
19276
+ for (const entry of this.runningTasks.values()) {
18881
19277
  entry.shutdown = true;
18882
19278
  killPromises.push(this.killEntry(entry));
18883
19279
  }
18884
- return Promise.allSettled(killPromises).then(() => {
18885
- });
19280
+ await Promise.allSettled(killPromises);
19281
+ return interruptedTaskIds;
18886
19282
  }
18887
19283
  getRunningTaskIds() {
18888
19284
  return [...this.runningTasks.keys()];
@@ -18892,136 +19288,244 @@ var WorkerEngine = class {
18892
19288
  }
18893
19289
  poll() {
18894
19290
  if (this.stopped) return;
18895
- this.tryDispatch().then(() => {
19291
+ markGatewayActivity("worker");
19292
+ this.pollCyclePromise = this.tryDispatch().finally(() => {
19293
+ this.pollCyclePromise = null;
18896
19294
  if (this.stopped) return;
18897
19295
  this.pollTimer = setTimeout(() => this.poll(), this.cfg.pollIntervalMs);
18898
19296
  });
18899
19297
  }
18900
19298
  async tryDispatch() {
19299
+ await this.reconcileCancelledTasks();
18901
19300
  while (!this.stopped && this.runningTasks.size < this.cfg.maxConcurrency) {
19301
+ let task;
19302
+ try {
19303
+ task = await TaskService.next({ excludedBatchIds: [...this.activeBatchIds] });
19304
+ } catch (err) {
19305
+ this.logError("task claim failed", err);
19306
+ break;
19307
+ }
19308
+ if (!task) break;
19309
+ if (this.stopped) break;
19310
+ if (!await TaskService.start(task.id)) continue;
19311
+ if (task.batchId) this.activeBatchIds.add(task.batchId);
19312
+ if (this.stopped) {
19313
+ await TaskService.resetRunningToPending([task.id]);
19314
+ this.releaseBatch(task);
19315
+ break;
19316
+ }
18902
19317
  try {
18903
- const excludedBatchIds = [...this.activeBatchIds];
18904
- const task = await TaskService.next({ excludedBatchIds });
18905
- if (!task) break;
18906
- if (!await TaskService.start(task.id)) continue;
18907
- if (task.batchId) {
18908
- this.activeBatchIds.add(task.batchId);
18909
- }
18910
19318
  const run = await TaskRunService.create({
18911
19319
  taskId: task.id,
18912
19320
  model: this.resolveModel(task.model),
18913
19321
  status: "running"
18914
19322
  });
18915
- const modelToUse = this.resolveModel(task.model);
18916
- const args = ["run", "--agent", "supertask-runner", "--format", "json"];
18917
- if (modelToUse) {
18918
- args.push("-m", modelToUse);
18919
- }
18920
- args.push(`\u6267\u884C\u4EFB\u52A1 ID: ${task.id}${modelToUse ? ` OVERRIDE_MODEL=${modelToUse}` : ""}`);
18921
- const cwd = task.cwd || process.cwd();
18922
- const child = spawn("opencode", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
18923
- await TaskRunService.updatePid(run.id, process.pid, child.pid ?? 0);
18924
- let output = "";
18925
- const handleData = (data) => {
18926
- const text2 = data.toString();
18927
- output += text2;
18928
- process.stdout.write(text2);
18929
- const match2 = text2.match(/"sessionID"\s*:\s*"(ses_[^"]+)"/);
18930
- if (match2) {
18931
- TaskRunService.updateSessionId(run.id, match2[1]).then(() => {
18932
- console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: "sessionId captured", taskId: task.id, sessionId: match2[1] }));
18933
- });
18934
- }
18935
- };
18936
- child.stdout?.on("data", handleData);
18937
- child.stderr?.on("data", handleData);
18938
- const entry = { task, runId: run.id, child, startedAt: Date.now(), shutdown: false };
18939
- this.runningTasks.set(task.id, entry);
18940
- child.on("close", async (code) => {
18941
- this.runningTasks.delete(task.id);
18942
- if (task.batchId) this.activeBatchIds.delete(task.batchId);
18943
- if (entry.shutdown) return;
18944
- const currentRun = await TaskRunService.getById(run.id);
18945
- if (!currentRun || currentRun.status !== "running") return;
18946
- if (code === 0) {
18947
- await TaskRunService.done(run.id);
18948
- await TaskService.done(task.id);
18949
- console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: "task done", taskId: task.id }));
18950
- } else {
18951
- const lastOutput = output.slice(-2e3);
18952
- await TaskRunService.fail(run.id, lastOutput);
18953
- const currentStatus = await TaskService.getById(task.id);
18954
- if (currentStatus?.status === "running") {
18955
- await TaskService.fail(task.id, "Worker\u6267\u884C\u5F02\u5E38\uFF1AOpencode \u8FDB\u7A0B\u975E\u6B63\u5E38\u9000\u51FA");
18956
- }
18957
- console.error(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "error", msg: "task failed", taskId: task.id, code }));
18958
- }
18959
- });
18960
- child.on("error", async (err) => {
18961
- this.runningTasks.delete(task.id);
18962
- if (task.batchId) this.activeBatchIds.delete(task.batchId);
18963
- if (entry.shutdown) return;
18964
- const currentRun = await TaskRunService.getById(run.id);
18965
- if (!currentRun || currentRun.status !== "running") return;
18966
- await TaskRunService.fail(run.id, err.message);
18967
- const currentStatus = await TaskService.getById(task.id);
18968
- if (currentStatus?.status === "running") {
18969
- await TaskService.fail(task.id, `spawn \u5F02\u5E38: ${err.message}`);
18970
- }
18971
- console.error(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "error", msg: "task spawn error", taskId: task.id, error: err.message }));
19323
+ if (this.stopped) {
19324
+ await TaskRunService.fail(run.id, "Gateway shutdown before spawn");
19325
+ await TaskService.resetRunningToPending([task.id]);
19326
+ this.releaseBatch(task);
19327
+ break;
19328
+ }
19329
+ if (task.agent === FORBIDDEN_AGENT) {
19330
+ const message = `\u7981\u6B62\u6267\u884C\u9012\u5F52 Agent: ${FORBIDDEN_AGENT}`;
19331
+ await TaskRunService.fail(run.id, message);
19332
+ await TaskService.fail(task.id, message, {}, { setDeadLetter: true });
19333
+ this.releaseBatch(task);
19334
+ continue;
19335
+ }
19336
+ this.spawnTask(task, run.id);
19337
+ } catch (err) {
19338
+ this.releaseBatch(task);
19339
+ const message = `Worker \u542F\u52A8\u4EFB\u52A1\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`;
19340
+ try {
19341
+ await TaskService.fail(task.id, message);
19342
+ } catch (failErr) {
19343
+ this.logError("failed to compensate task startup", failErr, task.id);
19344
+ }
19345
+ this.logError("task dispatch failed", err, task.id);
19346
+ }
19347
+ }
19348
+ }
19349
+ spawnTask(task, runId) {
19350
+ const model = this.resolveModel(task.model);
19351
+ const args = ["run", "--agent", task.agent, "--format", "json"];
19352
+ if (model) args.push("-m", model);
19353
+ args.push(task.prompt);
19354
+ const child = spawn(this.opencodeBin, args, {
19355
+ cwd: task.cwd || process.cwd(),
19356
+ stdio: ["ignore", "pipe", "pipe"],
19357
+ detached: process.platform !== "win32"
19358
+ });
19359
+ const entry = {
19360
+ task,
19361
+ runId,
19362
+ child,
19363
+ output: "",
19364
+ sessionId: null,
19365
+ timeoutTimer: null,
19366
+ shutdown: false,
19367
+ settled: false
19368
+ };
19369
+ this.runningTasks.set(task.id, entry);
19370
+ const handleData = (data) => {
19371
+ const text2 = data.toString();
19372
+ entry.output = (entry.output + text2).slice(-this.maxOutputChars);
19373
+ process.stdout.write(text2);
19374
+ const match2 = entry.output.match(/"sessionID"\s*:\s*"(ses_[^"]+)"/);
19375
+ if (match2?.[1] && match2[1] !== entry.sessionId) {
19376
+ entry.sessionId = match2[1];
19377
+ TaskRunService.updateSessionId(runId, match2[1]).catch((err) => {
19378
+ this.logError("sessionId update failed", err, task.id);
18972
19379
  });
19380
+ }
19381
+ };
19382
+ child.stdout?.on("data", handleData);
19383
+ child.stderr?.on("data", handleData);
19384
+ child.once("error", (err) => {
19385
+ void this.finishEntry(entry, null, `\u65E0\u6CD5\u542F\u52A8 opencode\uFF1A${err.message}`);
19386
+ });
19387
+ child.once("close", (code, signal) => {
19388
+ const failure = code === 0 ? void 0 : `opencode \u9000\u51FA\u7801 ${code ?? "null"}${signal ? `\uFF0C\u4FE1\u53F7 ${signal}` : ""}`;
19389
+ void this.finishEntry(entry, code, failure);
19390
+ });
19391
+ const timeoutMs = task.timeoutMs ?? this.cfg.taskTimeoutMs;
19392
+ if (timeoutMs > 0) {
19393
+ entry.timeoutTimer = setTimeout(() => {
19394
+ this.signalEntry(entry, "SIGKILL");
19395
+ void this.finishEntry(entry, null, `\u4EFB\u52A1\u8D85\u65F6\uFF08${timeoutMs}ms\uFF09`);
19396
+ }, timeoutMs);
19397
+ }
19398
+ TaskRunService.updatePid(runId, process.pid, child.pid ?? 0).catch((err) => {
19399
+ this.signalEntry(entry, "SIGKILL");
19400
+ void this.finishEntry(entry, null, `\u8BB0\u5F55 Worker PID \u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`);
19401
+ });
19402
+ }
19403
+ async finishEntry(entry, code, failure) {
19404
+ if (entry.settled) return;
19405
+ entry.settled = true;
19406
+ if (entry.timeoutTimer) {
19407
+ clearTimeout(entry.timeoutTimer);
19408
+ entry.timeoutTimer = null;
19409
+ }
19410
+ try {
19411
+ if (entry.shutdown) return;
19412
+ const currentRun = await TaskRunService.getById(entry.runId);
19413
+ if (!currentRun || currentRun.status !== "running") return;
19414
+ const output = entry.output.trim();
19415
+ const log = failure ? `${failure}${output ? `
19416
+ ${output}` : ""}` : output;
19417
+ if (code === 0 && !failure) {
19418
+ const completed = await TaskService.done(entry.task.id, log);
19419
+ if (completed) {
19420
+ await TaskRunService.done(entry.runId, log);
19421
+ console.log(JSON.stringify({
19422
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
19423
+ level: "info",
19424
+ msg: "task done",
19425
+ taskId: entry.task.id
19426
+ }));
19427
+ return;
19428
+ }
19429
+ await TaskRunService.fail(entry.runId, "\u4EFB\u52A1\u72B6\u6001\u5DF2\u88AB\u5176\u4ED6\u64CD\u4F5C\u6539\u53D8");
19430
+ return;
19431
+ }
19432
+ await TaskRunService.fail(entry.runId, log);
19433
+ const failed = await TaskService.fail(entry.task.id, log);
19434
+ if (!failed) {
19435
+ this.logError("task failure state transition rejected", failure ?? "unknown failure", entry.task.id);
19436
+ }
19437
+ console.error(JSON.stringify({
19438
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
19439
+ level: "error",
19440
+ msg: "task failed",
19441
+ taskId: entry.task.id,
19442
+ error: failure
19443
+ }));
19444
+ } finally {
19445
+ this.runningTasks.delete(entry.task.id);
19446
+ this.releaseBatch(entry.task);
19447
+ }
19448
+ }
19449
+ async reconcileCancelledTasks() {
19450
+ for (const entry of [...this.runningTasks.values()]) {
19451
+ try {
19452
+ const task = await TaskService.getById(entry.task.id);
19453
+ if (task?.status === "cancelled") await this.cancelEntry(entry);
18973
19454
  } catch (err) {
18974
- console.error(JSON.stringify({
18975
- ts: (/* @__PURE__ */ new Date()).toISOString(),
18976
- level: "error",
18977
- msg: "tryDispatch iteration failed",
18978
- error: err instanceof Error ? err.message : String(err)
18979
- }));
18980
- break;
19455
+ this.logError("cancel reconciliation failed", err, entry.task.id);
18981
19456
  }
18982
19457
  }
18983
19458
  }
19459
+ async cancelEntry(entry) {
19460
+ if (entry.settled) return;
19461
+ entry.settled = true;
19462
+ if (entry.timeoutTimer) {
19463
+ clearTimeout(entry.timeoutTimer);
19464
+ entry.timeoutTimer = null;
19465
+ }
19466
+ try {
19467
+ await this.killEntry(entry);
19468
+ const output = entry.output.trim();
19469
+ const log = `\u4EFB\u52A1\u5DF2\u53D6\u6D88${output ? `
19470
+ ${output}` : ""}`;
19471
+ await TaskRunService.fail(entry.runId, log);
19472
+ console.log(JSON.stringify({
19473
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
19474
+ level: "info",
19475
+ msg: "running task cancelled",
19476
+ taskId: entry.task.id
19477
+ }));
19478
+ } finally {
19479
+ this.runningTasks.delete(entry.task.id);
19480
+ this.releaseBatch(entry.task);
19481
+ }
19482
+ }
18984
19483
  async updateHeartbeats() {
18985
- for (const [, entry] of this.runningTasks) {
19484
+ for (const entry of this.runningTasks.values()) {
18986
19485
  try {
18987
19486
  await TaskRunService.heartbeat(entry.runId);
18988
- } catch {
19487
+ } catch (err) {
19488
+ this.logError("heartbeat update failed", err, entry.task.id);
18989
19489
  }
18990
19490
  }
18991
19491
  }
18992
19492
  killEntry(entry) {
18993
- if (entry.child.exitCode !== null) {
19493
+ if (entry.child.exitCode !== null || entry.child.signalCode !== null) {
18994
19494
  return Promise.resolve();
18995
19495
  }
18996
19496
  return new Promise((resolve) => {
18997
19497
  const timeout = setTimeout(() => {
18998
- try {
18999
- if (entry.child.pid) process.kill(entry.child.pid, "SIGKILL");
19000
- } catch {
19001
- }
19498
+ this.signalEntry(entry, "SIGKILL");
19002
19499
  resolve();
19003
19500
  }, 5e3);
19004
- entry.child.on("close", () => {
19501
+ entry.child.once("close", () => {
19005
19502
  clearTimeout(timeout);
19006
19503
  resolve();
19007
19504
  });
19008
- try {
19009
- if (entry.child.pid) {
19010
- entry.child.kill("SIGTERM");
19011
- } else {
19012
- clearTimeout(timeout);
19013
- resolve();
19014
- }
19015
- } catch {
19016
- clearTimeout(timeout);
19017
- resolve();
19018
- }
19505
+ this.signalEntry(entry, "SIGTERM");
19019
19506
  });
19020
19507
  }
19508
+ signalEntry(entry, signal) {
19509
+ const pid = entry.child.pid;
19510
+ if (!pid) return;
19511
+ signalSpawnedProcessTree(pid, signal);
19512
+ }
19513
+ releaseBatch(task) {
19514
+ if (task.batchId) this.activeBatchIds.delete(task.batchId);
19515
+ }
19021
19516
  resolveModel(taskModel) {
19022
19517
  if (!taskModel || taskModel === "default") return null;
19023
19518
  return taskModel;
19024
19519
  }
19520
+ logError(message, error, taskId) {
19521
+ console.error(JSON.stringify({
19522
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
19523
+ level: "error",
19524
+ msg: message,
19525
+ taskId,
19526
+ error: error instanceof Error ? error.message : String(error)
19527
+ }));
19528
+ }
19025
19529
  };
19026
19530
 
19027
19531
  // src/gateway/watchdog/heartbeat.ts
@@ -19034,15 +19538,23 @@ async function checkHeartbeats(heartbeatTimeoutMs) {
19034
19538
  for (const run of staleRuns) {
19035
19539
  try {
19036
19540
  if (run.childPid != null && run.childPid > 0) {
19037
- try {
19038
- process.kill(run.childPid, "SIGKILL");
19039
- } catch {
19541
+ const expectedExecutable = process.env.SUPERTASK_OPENCODE_BIN ?? "opencode";
19542
+ const killed = signalRecordedProcessTree(run.childPid, "SIGKILL", expectedExecutable);
19543
+ if (!killed) {
19544
+ console.warn(JSON.stringify({
19545
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
19546
+ level: "warn",
19547
+ msg: "stale child process was not killed because identity validation failed",
19548
+ taskId: run.taskId,
19549
+ runId: run.runId,
19550
+ childPid: run.childPid
19551
+ }));
19040
19552
  }
19041
19553
  }
19042
19554
  await TaskRunService.fail(run.runId, `\u5FC3\u8DF3\u8D85\u65F6 (${heartbeatTimeoutMs / 1e3}s)\uFF0CWatchdog kill`);
19043
19555
  const newRetryCount = run.taskRetryCount + 1;
19044
19556
  const maxRetries = run.taskMaxRetries;
19045
- if (newRetryCount >= maxRetries) {
19557
+ if (newRetryCount > maxRetries) {
19046
19558
  await TaskService.markDeadLetter(run.taskId, newRetryCount);
19047
19559
  console.log(JSON.stringify({
19048
19560
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -19054,7 +19566,7 @@ async function checkHeartbeats(heartbeatTimeoutMs) {
19054
19566
  maxRetries
19055
19567
  }));
19056
19568
  } else {
19057
- const backoffMs = computeBackoff(newRetryCount);
19569
+ const backoffMs = computeBackoff(newRetryCount, run.taskRetryBackoffMs);
19058
19570
  const retryAfter = Date.now() + backoffMs;
19059
19571
  await TaskService.markPendingForRetry(run.taskId, retryAfter, newRetryCount);
19060
19572
  console.log(JSON.stringify({
@@ -19110,6 +19622,7 @@ async function cleanupOldRecords(retentionDays) {
19110
19622
  }
19111
19623
 
19112
19624
  // src/gateway/watchdog/index.ts
19625
+ init_health();
19113
19626
  var Watchdog = class {
19114
19627
  constructor(cfg) {
19115
19628
  this.cfg = cfg;
@@ -19120,13 +19633,14 @@ var Watchdog = class {
19120
19633
  cleanupTimer = null;
19121
19634
  start() {
19122
19635
  this.stopped = false;
19636
+ markGatewayActivity("watchdog");
19123
19637
  this.heartbeatTimer = setInterval(
19124
19638
  () => this.runHeartbeatCheck(),
19125
- this.cfg.watchdog.cleanupIntervalMs
19639
+ this.cfg.watchdog.checkIntervalMs
19126
19640
  );
19127
19641
  this.cleanupTimer = setInterval(
19128
19642
  () => this.runCleanup(),
19129
- this.cfg.watchdog.cleanupIntervalMs * 24 * 60
19643
+ this.cfg.watchdog.cleanupIntervalMs
19130
19644
  );
19131
19645
  }
19132
19646
  stop() {
@@ -19142,6 +19656,7 @@ var Watchdog = class {
19142
19656
  }
19143
19657
  async runHeartbeatCheck() {
19144
19658
  if (this.stopped) return;
19659
+ markGatewayActivity("watchdog");
19145
19660
  try {
19146
19661
  await checkHeartbeats(this.cfg.watchdog.heartbeatTimeoutMs);
19147
19662
  } catch (err) {
@@ -19177,7 +19692,7 @@ var { taskTemplates: taskTemplates3 } = schema_exports;
19177
19692
  async function cloneTaskFromTemplate(templateId) {
19178
19693
  const rows = await db.select().from(taskTemplates3).where(eq(taskTemplates3.id, templateId)).limit(1);
19179
19694
  const tmpl = rows[0];
19180
- if (!tmpl) return null;
19695
+ if (!tmpl || !tmpl.enabled) return null;
19181
19696
  const activeCount = await db.select({ count: sql`count(*)` }).from(schema_exports.tasks).where(
19182
19697
  and(
19183
19698
  eq(schema_exports.tasks.templateId, templateId),
@@ -19186,7 +19701,8 @@ async function cloneTaskFromTemplate(templateId) {
19186
19701
  ).then((r) => r[0].count);
19187
19702
  if (activeCount >= (tmpl.maxInstances ?? 1)) return null;
19188
19703
  const nowMs = Date.now();
19189
- const nextRunAt = TaskTemplateService.calculateNextRunAt(
19704
+ const isDelayed = tmpl.scheduleType === "delayed";
19705
+ const nextRunAt = isDelayed ? null : TaskTemplateService.calculateNextRunAt(
19190
19706
  tmpl.scheduleType,
19191
19707
  tmpl,
19192
19708
  nowMs
@@ -19200,13 +19716,17 @@ async function cloneTaskFromTemplate(templateId) {
19200
19716
  category: tmpl.category ?? "general",
19201
19717
  importance: tmpl.importance ?? 3,
19202
19718
  urgency: tmpl.urgency ?? 3,
19719
+ batchId: tmpl.batchId,
19203
19720
  maxRetries: tmpl.maxRetries ?? 3,
19721
+ retryBackoffMs: tmpl.retryBackoffMs ?? 3e4,
19722
+ timeoutMs: tmpl.timeoutMs,
19204
19723
  templateId: tmpl.id,
19205
19724
  scheduledAt: nowMs
19206
19725
  });
19207
19726
  await db.update(taskTemplates3).set({
19208
19727
  lastRunAt: nowMs,
19209
19728
  nextRunAt,
19729
+ enabled: isDelayed ? false : tmpl.enabled,
19210
19730
  updatedAt: nowMs
19211
19731
  }).where(eq(taskTemplates3.id, templateId));
19212
19732
  return task;
@@ -19219,7 +19739,7 @@ async function getDueTemplates() {
19219
19739
  sql`${taskTemplates3.nextRunAt} IS NOT NULL`,
19220
19740
  sql`${taskTemplates3.nextRunAt} <= ${nowMs}`
19221
19741
  )
19222
- );
19742
+ ).orderBy(asc(taskTemplates3.nextRunAt), asc(taskTemplates3.id));
19223
19743
  }
19224
19744
  async function initializeNextRunAt(templateId) {
19225
19745
  const rows = await db.select().from(taskTemplates3).where(eq(taskTemplates3.id, templateId)).limit(1);
@@ -19237,6 +19757,7 @@ async function initializeNextRunAt(templateId) {
19237
19757
  // src/gateway/scheduler/index.ts
19238
19758
  init_db2();
19239
19759
  init_drizzle_orm();
19760
+ init_health();
19240
19761
  var Scheduler = class {
19241
19762
  constructor(cfg) {
19242
19763
  this.cfg = cfg;
@@ -19248,6 +19769,7 @@ var Scheduler = class {
19248
19769
  async start() {
19249
19770
  if (!this.cfg.scheduler.enabled) return;
19250
19771
  this.stopped = false;
19772
+ markGatewayActivity("scheduler");
19251
19773
  await this.initializeTemplates();
19252
19774
  this.timer = setInterval(() => this.tick(), this.cfg.scheduler.checkIntervalMs);
19253
19775
  }
@@ -19260,6 +19782,7 @@ var Scheduler = class {
19260
19782
  }
19261
19783
  async tick() {
19262
19784
  if (this.stopped || this.ticking) return;
19785
+ markGatewayActivity("scheduler");
19263
19786
  this.ticking = true;
19264
19787
  try {
19265
19788
  const dueTemplates = await getDueTemplates();
@@ -19309,6 +19832,7 @@ var Scheduler = class {
19309
19832
  init_db2();
19310
19833
  init_task_service();
19311
19834
  init_task_run_service();
19835
+ init_health();
19312
19836
  var STALE_THRESHOLD_MS = 3e4;
19313
19837
  function acquireLock() {
19314
19838
  const now = Date.now();
@@ -19330,7 +19854,7 @@ function acquireLock() {
19330
19854
  sqlite.exec("DELETE FROM gateway_lock WHERE id = 1");
19331
19855
  }
19332
19856
  sqlite.exec(
19333
- "INSERT INTO gateway_lock (id, pid, acquired_at, heartbeat_at) VALUES (1, ?, ?, ?)",
19857
+ "INSERT INTO gateway_lock (id, pid, acquired_at, heartbeat_at, ready_at) VALUES (1, ?, ?, ?, NULL)",
19334
19858
  [pid, now, now]
19335
19859
  );
19336
19860
  sqlite.exec("COMMIT");
@@ -19364,6 +19888,18 @@ function updateLockHeartbeat() {
19364
19888
  } catch {
19365
19889
  }
19366
19890
  }
19891
+ function markGatewayReady() {
19892
+ sqlite.exec(
19893
+ "UPDATE gateway_lock SET heartbeat_at = ?, ready_at = ? WHERE pid = ?",
19894
+ [Date.now(), Date.now(), process.pid]
19895
+ );
19896
+ }
19897
+ function markGatewayNotReady() {
19898
+ try {
19899
+ sqlite.exec("UPDATE gateway_lock SET ready_at = NULL WHERE pid = ?", [process.pid]);
19900
+ } catch {
19901
+ }
19902
+ }
19367
19903
  async function main() {
19368
19904
  console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: "SuperTask Gateway starting", pid: process.pid }));
19369
19905
  if (!acquireLock()) {
@@ -19375,6 +19911,12 @@ async function main() {
19375
19911
  const worker = new WorkerEngine(cfg);
19376
19912
  const watchdog = new Watchdog(cfg);
19377
19913
  const scheduler = new Scheduler(cfg);
19914
+ initializeGatewayHealth({
19915
+ workerPollIntervalMs: cfg.worker.pollIntervalMs,
19916
+ schedulerEnabled: cfg.scheduler.enabled,
19917
+ schedulerCheckIntervalMs: cfg.scheduler.checkIntervalMs,
19918
+ watchdogCheckIntervalMs: cfg.watchdog.checkIntervalMs
19919
+ });
19378
19920
  worker.start();
19379
19921
  watchdog.start();
19380
19922
  await scheduler.start();
@@ -19392,6 +19934,7 @@ async function main() {
19392
19934
  url: `http://localhost:${cfg.dashboard.port}`
19393
19935
  }));
19394
19936
  }
19937
+ markGatewayReady();
19395
19938
  console.log(JSON.stringify({
19396
19939
  ts: (/* @__PURE__ */ new Date()).toISOString(),
19397
19940
  level: "info",
@@ -19405,10 +19948,10 @@ async function main() {
19405
19948
  shuttingDown = true;
19406
19949
  console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: `received ${signal}, shutting down...` }));
19407
19950
  clearInterval(heartbeatTimer);
19951
+ markGatewayNotReady();
19408
19952
  scheduler.stop();
19409
19953
  watchdog.stop();
19410
- const runningIds = worker.getRunningTaskIds();
19411
- await worker.stop();
19954
+ const runningIds = await worker.stop(cfg.worker.shutdownGracePeriodMs);
19412
19955
  if (runningIds.length > 0) {
19413
19956
  const resetCount = await TaskService.resetRunningToPending(runningIds);
19414
19957
  console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: "reset running tasks to pending", count: resetCount }));
@@ -19418,6 +19961,7 @@ async function main() {
19418
19961
  await TaskRunService.fail(run.id, "Gateway shutdown");
19419
19962
  }
19420
19963
  releaseLock();
19964
+ resetGatewayHealth();
19421
19965
  closeDb();
19422
19966
  console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: "Gateway stopped" }));
19423
19967
  process.exit(0);