opencode-supertask 0.1.20 → 0.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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;
6192
6215
  }
6193
- function loadConfig() {
6194
- if (!existsSync2(CONFIG_PATH)) {
6195
- return DEFAULT_CONFIG;
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`);
6196
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;
6229
+ }
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");
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
  }
@@ -6431,9 +6543,38 @@ var init_task_service = __esm({
6431
6543
  ).returning();
6432
6544
  return result.length;
6433
6545
  }
6546
+ static async resetOrphanRunningToPending() {
6547
+ const result = await db.update(tasks2).set({
6548
+ status: "pending",
6549
+ startedAt: null,
6550
+ finishedAt: null
6551
+ }).where(
6552
+ and(
6553
+ eq(tasks2.status, "running"),
6554
+ sql`NOT EXISTS (
6555
+ SELECT 1 FROM ${taskRuns2}
6556
+ WHERE ${taskRuns2.taskId} = ${tasks2.id}
6557
+ AND ${taskRuns2.status} = 'running'
6558
+ )`
6559
+ )
6560
+ ).returning();
6561
+ return result.length;
6562
+ }
6434
6563
  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();
6564
+ const conditions = [
6565
+ eq(tasks2.id, id),
6566
+ or(
6567
+ eq(tasks2.status, "pending"),
6568
+ eq(tasks2.status, "running"),
6569
+ eq(tasks2.status, "failed")
6570
+ ),
6571
+ ...this.buildScopeWhere(scope)
6572
+ ];
6573
+ const result = await db.update(tasks2).set({
6574
+ status: "cancelled",
6575
+ finishedAt: /* @__PURE__ */ new Date(),
6576
+ retryAfter: null
6577
+ }).where(and(...conditions)).returning();
6437
6578
  return result[0] || null;
6438
6579
  }
6439
6580
  static async retry(id, scope = {}) {
@@ -6445,7 +6586,8 @@ var init_task_service = __esm({
6445
6586
  status: "pending",
6446
6587
  startedAt: null,
6447
6588
  finishedAt: null,
6448
- retryAfter: null
6589
+ retryAfter: null,
6590
+ retryCount: 0
6449
6591
  }).where(and(...conditions)).returning();
6450
6592
  return result[0] || null;
6451
6593
  }
@@ -6459,7 +6601,8 @@ var init_task_service = __esm({
6459
6601
  status: "pending",
6460
6602
  startedAt: null,
6461
6603
  finishedAt: null,
6462
- retryAfter: null
6604
+ retryAfter: null,
6605
+ retryCount: 0
6463
6606
  }).where(and(...conditions)).returning();
6464
6607
  return result.length;
6465
6608
  }
@@ -6527,6 +6670,9 @@ var init_task_service = __esm({
6527
6670
  }
6528
6671
  static async delete(id, scope = {}) {
6529
6672
  const conditions = [eq(tasks2.id, id), ...this.buildScopeWhere(scope)];
6673
+ const existing = await db.select({ id: tasks2.id }).from(tasks2).where(and(...conditions)).limit(1);
6674
+ if (!existing[0]) return false;
6675
+ await db.delete(taskRuns2).where(eq(taskRuns2.taskId, id));
6530
6676
  const result = await db.delete(tasks2).where(and(...conditions)).returning();
6531
6677
  return result.length > 0;
6532
6678
  }
@@ -6546,65 +6692,65 @@ var init_task_service = __esm({
6546
6692
  });
6547
6693
 
6548
6694
  // src/core/services/task-run.service.ts
6549
- var taskRuns2, TaskRunService;
6695
+ var taskRuns3, TaskRunService;
6550
6696
  var init_task_run_service = __esm({
6551
6697
  "src/core/services/task-run.service.ts"() {
6552
6698
  "use strict";
6553
6699
  init_db2();
6554
6700
  init_drizzle_orm();
6555
- ({ taskRuns: taskRuns2 } = schema_exports);
6701
+ ({ taskRuns: taskRuns3 } = schema_exports);
6556
6702
  TaskRunService = class {
6557
6703
  static async create(data) {
6558
- const result = await db.insert(taskRuns2).values(data).returning();
6704
+ const result = await db.insert(taskRuns3).values(data).returning();
6559
6705
  return result[0];
6560
6706
  }
6561
6707
  static async updateSessionId(id, sessionId) {
6562
- const result = await db.update(taskRuns2).set({ sessionId }).where(eq(taskRuns2.id, id)).returning();
6708
+ const result = await db.update(taskRuns3).set({ sessionId }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
6563
6709
  return result[0] || null;
6564
6710
  }
6565
6711
  static async done(id, log) {
6566
- const result = await db.update(taskRuns2).set({
6712
+ const result = await db.update(taskRuns3).set({
6567
6713
  status: "done",
6568
6714
  finishedAt: /* @__PURE__ */ new Date(),
6569
6715
  log
6570
- }).where(eq(taskRuns2.id, id)).returning();
6716
+ }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
6571
6717
  return result[0] || null;
6572
6718
  }
6573
6719
  static async fail(id, log) {
6574
- const result = await db.update(taskRuns2).set({
6720
+ const result = await db.update(taskRuns3).set({
6575
6721
  status: "failed",
6576
6722
  finishedAt: /* @__PURE__ */ new Date(),
6577
6723
  log
6578
- }).where(eq(taskRuns2.id, id)).returning();
6724
+ }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
6579
6725
  return result[0] || null;
6580
6726
  }
6581
6727
  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();
6728
+ const result = await db.update(taskRuns3).set({ heartbeatAt: Date.now() }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
6583
6729
  return result[0] || null;
6584
6730
  }
6585
6731
  static async updatePid(id, workerPid, childPid) {
6586
- const result = await db.update(taskRuns2).set({
6732
+ const result = await db.update(taskRuns3).set({
6587
6733
  workerPid,
6588
6734
  childPid,
6589
6735
  lockedAt: Date.now(),
6590
6736
  lockedBy: `gateway-${process.pid}`
6591
- }).where(eq(taskRuns2.id, id)).returning();
6737
+ }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
6592
6738
  return result[0] || null;
6593
6739
  }
6594
6740
  static async getById(id) {
6595
- const result = await db.select().from(taskRuns2).where(eq(taskRuns2.id, id));
6741
+ const result = await db.select().from(taskRuns3).where(eq(taskRuns3.id, id));
6596
6742
  return result[0] || null;
6597
6743
  }
6598
6744
  static async listByTaskId(taskId) {
6599
- return await db.select().from(taskRuns2).where(eq(taskRuns2.taskId, taskId)).orderBy(desc(taskRuns2.startedAt), desc(taskRuns2.id));
6745
+ return await db.select().from(taskRuns3).where(eq(taskRuns3.taskId, taskId)).orderBy(desc(taskRuns3.startedAt), desc(taskRuns3.id));
6600
6746
  }
6601
6747
  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);
6748
+ const result = await db.select().from(taskRuns3).where(eq(taskRuns3.taskId, taskId)).orderBy(desc(taskRuns3.startedAt), desc(taskRuns3.id)).limit(1);
6603
6749
  return result[0] || null;
6604
6750
  }
6605
6751
  static async getLatestByTaskIds(taskIds) {
6606
6752
  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));
6753
+ const latestRuns = await db.select().from(taskRuns3).where(inArray(taskRuns3.taskId, taskIds)).orderBy(desc(taskRuns3.startedAt), desc(taskRuns3.id));
6608
6754
  const result = /* @__PURE__ */ new Map();
6609
6755
  for (const run of latestRuns) {
6610
6756
  if (!result.has(run.taskId)) {
@@ -6618,22 +6764,23 @@ var init_task_run_service = __esm({
6618
6764
  const cutoffSec = Math.floor(cutoffMs / 1e3);
6619
6765
  const { tasks: tasksTable } = schema_exports;
6620
6766
  const result = await db.select({
6621
- runId: taskRuns2.id,
6622
- taskId: taskRuns2.taskId,
6623
- childPid: taskRuns2.childPid,
6767
+ runId: taskRuns3.id,
6768
+ taskId: taskRuns3.taskId,
6769
+ childPid: taskRuns3.childPid,
6624
6770
  taskRetryCount: tasksTable.retryCount,
6625
- taskMaxRetries: tasksTable.maxRetries
6626
- }).from(taskRuns2).innerJoin(tasksTable, eq(taskRuns2.taskId, tasksTable.id)).where(
6771
+ taskMaxRetries: tasksTable.maxRetries,
6772
+ taskRetryBackoffMs: tasksTable.retryBackoffMs
6773
+ }).from(taskRuns3).innerJoin(tasksTable, eq(taskRuns3.taskId, tasksTable.id)).where(
6627
6774
  and(
6628
- eq(taskRuns2.status, "running"),
6775
+ eq(taskRuns3.status, "running"),
6629
6776
  or(
6630
6777
  and(
6631
- sql`${taskRuns2.heartbeatAt} IS NULL`,
6632
- sql`${taskRuns2.startedAt} < ${cutoffSec}`
6778
+ sql`${taskRuns3.heartbeatAt} IS NULL`,
6779
+ sql`${taskRuns3.startedAt} < ${cutoffSec}`
6633
6780
  ),
6634
6781
  and(
6635
- sql`${taskRuns2.heartbeatAt} IS NOT NULL`,
6636
- sql`${taskRuns2.heartbeatAt} < ${cutoffMs}`
6782
+ sql`${taskRuns3.heartbeatAt} IS NOT NULL`,
6783
+ sql`${taskRuns3.heartbeatAt} < ${cutoffMs}`
6637
6784
  )
6638
6785
  )
6639
6786
  )
@@ -6643,25 +6790,111 @@ var init_task_run_service = __esm({
6643
6790
  taskId: row.taskId,
6644
6791
  childPid: row.childPid,
6645
6792
  taskRetryCount: row.taskRetryCount ?? 0,
6646
- taskMaxRetries: row.taskMaxRetries ?? 3
6793
+ taskMaxRetries: row.taskMaxRetries ?? 3,
6794
+ taskRetryBackoffMs: row.taskRetryBackoffMs ?? 3e4
6647
6795
  }));
6648
6796
  }
6649
6797
  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);
6798
+ 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
6799
  return result[0] || null;
6652
6800
  }
6653
6801
  static async deleteByTaskIds(taskIds) {
6654
6802
  if (taskIds.length === 0) return 0;
6655
- const result = await db.delete(taskRuns2).where(inArray(taskRuns2.taskId, taskIds)).returning();
6803
+ const result = await db.delete(taskRuns3).where(inArray(taskRuns3.taskId, taskIds)).returning();
6656
6804
  return result.length;
6657
6805
  }
6658
6806
  static async getAllRunningRuns() {
6659
- return await db.select().from(taskRuns2).where(eq(taskRuns2.status, "running"));
6807
+ return await db.select().from(taskRuns3).where(eq(taskRuns3.status, "running"));
6660
6808
  }
6661
6809
  };
6662
6810
  }
6663
6811
  });
6664
6812
 
6813
+ // src/gateway/health.ts
6814
+ function initializeGatewayHealth(config) {
6815
+ const now = Date.now();
6816
+ state = {
6817
+ startedAt: now,
6818
+ config,
6819
+ lastActivityAt: {
6820
+ worker: now,
6821
+ scheduler: now,
6822
+ watchdog: now
6823
+ }
6824
+ };
6825
+ }
6826
+ function markGatewayActivity(component) {
6827
+ if (state) state.lastActivityAt[component] = Date.now();
6828
+ }
6829
+ function resetGatewayHealth() {
6830
+ state = null;
6831
+ }
6832
+ function componentStatus(component, enabled, maxAgeMs, now) {
6833
+ const lastActivityAt = state?.lastActivityAt[component] ?? null;
6834
+ const ageMs = lastActivityAt == null ? null : Math.max(0, now - lastActivityAt);
6835
+ return {
6836
+ enabled,
6837
+ healthy: !enabled || ageMs != null && ageMs <= maxAgeMs,
6838
+ lastActivityAt,
6839
+ ageMs,
6840
+ maxAgeMs
6841
+ };
6842
+ }
6843
+ function getGatewayHealth(now = Date.now()) {
6844
+ const worker = componentStatus(
6845
+ "worker",
6846
+ true,
6847
+ Math.max((state?.config.workerPollIntervalMs ?? 1e3) * 5, 5e3),
6848
+ now
6849
+ );
6850
+ const scheduler = componentStatus(
6851
+ "scheduler",
6852
+ state?.config.schedulerEnabled ?? false,
6853
+ Math.max((state?.config.schedulerCheckIntervalMs ?? 1e3) * 5, 5e3),
6854
+ now
6855
+ );
6856
+ const watchdog = componentStatus(
6857
+ "watchdog",
6858
+ true,
6859
+ Math.max((state?.config.watchdogCheckIntervalMs ?? 6e4) * 3, 5e3),
6860
+ now
6861
+ );
6862
+ let lock = { pid: null, heartbeatAt: null, readyAt: null, ageMs: null, healthy: false };
6863
+ try {
6864
+ const row = sqlite.prepare(
6865
+ "SELECT pid, heartbeat_at, ready_at FROM gateway_lock WHERE id = 1"
6866
+ ).get();
6867
+ if (row) {
6868
+ const ageMs = Math.max(0, now - row.heartbeat_at);
6869
+ lock = {
6870
+ pid: row.pid,
6871
+ heartbeatAt: row.heartbeat_at,
6872
+ readyAt: row.ready_at,
6873
+ ageMs,
6874
+ healthy: row.pid === process.pid && row.ready_at != null && ageMs < LOCK_STALE_MS
6875
+ };
6876
+ }
6877
+ } catch {
6878
+ }
6879
+ const healthy = state != null && lock.healthy && worker.healthy && scheduler.healthy && watchdog.healthy;
6880
+ return {
6881
+ status: healthy ? "ok" : "degraded",
6882
+ pid: process.pid,
6883
+ uptimeMs: state ? Math.max(0, now - state.startedAt) : 0,
6884
+ lock,
6885
+ components: { worker, scheduler, watchdog }
6886
+ };
6887
+ }
6888
+ var LOCK_STALE_MS, state;
6889
+ var init_health = __esm({
6890
+ "src/gateway/health.ts"() {
6891
+ "use strict";
6892
+ init_db2();
6893
+ LOCK_STALE_MS = 3e4;
6894
+ state = null;
6895
+ }
6896
+ });
6897
+
6665
6898
  // node_modules/cron-parser/dist/fields/types.js
6666
6899
  var require_types = __commonJS({
6667
6900
  "node_modules/cron-parser/dist/fields/types.js"(exports) {
@@ -6862,7 +7095,7 @@ var require_CronField = __commonJS({
6862
7095
  if (!isValidRange) {
6863
7096
  throw new Error(`${this.constructor.name} Validation error, got value ${badValue} expected range ${this.min}-${this.max}${charsString}`);
6864
7097
  }
6865
- const duplicate = this.#values.find((value, index) => this.#values.indexOf(value) !== index);
7098
+ const duplicate = this.#values.find((value, index2) => this.#values.indexOf(value) !== index2);
6866
7099
  if (duplicate) {
6867
7100
  throw new Error(`${this.constructor.name} Validation error, duplicate values found: ${duplicate}`);
6868
7101
  }
@@ -14707,11 +14940,11 @@ var require_CronFieldCollection = __commonJS({
14707
14940
  throw new Error("Unexpected range end");
14708
14941
  }
14709
14942
  if (step * multiplier > range.end) {
14710
- const mapFn = (_, index) => {
14943
+ const mapFn = (_, index2) => {
14711
14944
  if (typeof range.start !== "number") {
14712
14945
  throw new Error("Unexpected range start");
14713
14946
  }
14714
- return index % step === 0 ? range.start + index : null;
14947
+ return index2 % step === 0 ? range.start + index2 : null;
14715
14948
  };
14716
14949
  if (typeof range.start !== "number") {
14717
14950
  throw new Error("Unexpected range start");
@@ -15603,9 +15836,9 @@ var require_CronExpressionParser = __commonJS({
15603
15836
  if (field === CronUnit.DayOfWeek && max % 7 === 0) {
15604
15837
  stack.push(0);
15605
15838
  }
15606
- for (let index = min; index <= max; index += repeatInterval) {
15607
- if (stack.indexOf(index) === -1) {
15608
- stack.push(index);
15839
+ for (let index2 = min; index2 <= max; index2 += repeatInterval) {
15840
+ if (stack.indexOf(index2) === -1) {
15841
+ stack.push(index2);
15609
15842
  }
15610
15843
  }
15611
15844
  return stack;
@@ -15828,11 +16061,6 @@ var require_dist = __commonJS({
15828
16061
  });
15829
16062
 
15830
16063
  // src/core/cron-parser.ts
15831
- var cron_parser_exports = {};
15832
- __export(cron_parser_exports, {
15833
- getNextCronRun: () => getNextCronRun,
15834
- isValidCronExpr: () => isValidCronExpr
15835
- });
15836
16064
  function getNextCronRun(expr, afterMs) {
15837
16065
  try {
15838
16066
  const fromDate = afterMs != null ? new Date(afterMs) : /* @__PURE__ */ new Date();
@@ -15870,6 +16098,7 @@ var init_task_template_service = __esm({
15870
16098
  ({ taskTemplates: taskTemplates2 } = schema_exports);
15871
16099
  TaskTemplateService = class {
15872
16100
  static async create(data) {
16101
+ this.validate(data);
15873
16102
  const now = Date.now();
15874
16103
  const result = await db.insert(taskTemplates2).values({ ...data, createdAt: now, updatedAt: now }).returning();
15875
16104
  const tmpl = result[0];
@@ -15885,8 +16114,38 @@ var init_task_template_service = __esm({
15885
16114
  }
15886
16115
  return tmpl;
15887
16116
  }
16117
+ static validate(data) {
16118
+ if (!data.name.trim()) throw new Error("name \u4E0D\u80FD\u4E3A\u7A7A");
16119
+ if (!data.agent.trim()) throw new Error("agent \u4E0D\u80FD\u4E3A\u7A7A");
16120
+ if (!data.prompt.trim()) throw new Error("prompt \u4E0D\u80FD\u4E3A\u7A7A");
16121
+ const scheduleType = data.scheduleType;
16122
+ if (!["cron", "delayed", "recurring"].includes(scheduleType)) {
16123
+ throw new Error("scheduleType \u5FC5\u987B\u662F cron\u3001delayed \u6216 recurring");
16124
+ }
16125
+ if (scheduleType === "cron" && (!data.cronExpr || !isValidCronExpr(data.cronExpr))) {
16126
+ throw new Error("cronExpr \u7F3A\u5931\u6216\u683C\u5F0F\u65E0\u6548");
16127
+ }
16128
+ if (scheduleType === "recurring" && (!Number.isInteger(data.intervalMs) || (data.intervalMs ?? 0) <= 0)) {
16129
+ throw new Error("intervalMs \u5FC5\u987B\u662F\u6B63\u6574\u6570");
16130
+ }
16131
+ if (scheduleType === "delayed" && (!Number.isInteger(data.runAt) || (data.runAt ?? 0) <= 0)) {
16132
+ throw new Error("runAt \u5FC5\u987B\u662F\u6B63\u6574\u6570\u65F6\u95F4\u6233");
16133
+ }
16134
+ this.validateInteger("importance", data.importance, 1, 5);
16135
+ this.validateInteger("urgency", data.urgency, 1, 5);
16136
+ this.validateInteger("maxInstances", data.maxInstances, 1, 1e3);
16137
+ this.validateInteger("maxRetries", data.maxRetries, 0, 1e3);
16138
+ this.validateInteger("retryBackoffMs", data.retryBackoffMs, 0, 864e5);
16139
+ this.validateInteger("timeoutMs", data.timeoutMs, 1e3, 6048e5);
16140
+ }
16141
+ static validateInteger(name, value, min, max) {
16142
+ if (value === void 0 || value === null) return;
16143
+ if (!Number.isInteger(value) || value < min || value > max) {
16144
+ throw new Error(`${name} \u5FC5\u987B\u662F ${min} \u5230 ${max} \u4E4B\u95F4\u7684\u6574\u6570`);
16145
+ }
16146
+ }
15888
16147
  static async list(limit = 50) {
15889
- return await db.select().from(taskTemplates2).orderBy(desc(taskTemplates2.createdAt)).limit(limit);
16148
+ return await db.select().from(taskTemplates2).orderBy(desc(taskTemplates2.createdAt), desc(taskTemplates2.id)).limit(limit);
15890
16149
  }
15891
16150
  static async getById(id) {
15892
16151
  const result = await db.select().from(taskTemplates2).where(eq(taskTemplates2.id, id));
@@ -15933,13 +16192,13 @@ var init_compose = __esm({
15933
16192
  "use strict";
15934
16193
  compose = (middleware, onError, onNotFound) => {
15935
16194
  return (context, next) => {
15936
- let index = -1;
16195
+ let index2 = -1;
15937
16196
  return dispatch(0);
15938
16197
  async function dispatch(i) {
15939
- if (i <= index) {
16198
+ if (i <= index2) {
15940
16199
  throw new Error("next() called multiple times");
15941
16200
  }
15942
- index = i;
16201
+ index2 = i;
15943
16202
  let res;
15944
16203
  let isError = false;
15945
16204
  let handler;
@@ -16054,8 +16313,8 @@ var init_body = __esm({
16054
16313
  handleParsingNestedValues = (form, key, value) => {
16055
16314
  let nestedForm = form;
16056
16315
  const keys = key.split(".");
16057
- keys.forEach((key2, index) => {
16058
- if (index === keys.length - 1) {
16316
+ keys.forEach((key2, index2) => {
16317
+ if (index2 === keys.length - 1) {
16059
16318
  nestedForm[key2] = value;
16060
16319
  } else {
16061
16320
  if (!nestedForm[key2] || typeof nestedForm[key2] !== "object" || Array.isArray(nestedForm[key2]) || nestedForm[key2] instanceof File) {
@@ -16087,8 +16346,8 @@ var init_url = __esm({
16087
16346
  };
16088
16347
  extractGroupsFromPath = (path) => {
16089
16348
  const groups = [];
16090
- path = path.replace(/\{[^}]+\}/g, (match2, index) => {
16091
- const mark = `@${index}`;
16349
+ path = path.replace(/\{[^}]+\}/g, (match2, index2) => {
16350
+ const mark = `@${index2}`;
16092
16351
  groups.push([mark, match2]);
16093
16352
  return mark;
16094
16353
  });
@@ -16607,10 +16866,10 @@ var init_html = __esm({
16607
16866
  return;
16608
16867
  }
16609
16868
  let escape;
16610
- let index;
16869
+ let index2;
16611
16870
  let lastIndex = 0;
16612
- for (index = match2; index < str.length; index++) {
16613
- switch (str.charCodeAt(index)) {
16871
+ for (index2 = match2; index2 < str.length; index2++) {
16872
+ switch (str.charCodeAt(index2)) {
16614
16873
  case 34:
16615
16874
  escape = "&quot;";
16616
16875
  break;
@@ -16629,10 +16888,10 @@ var init_html = __esm({
16629
16888
  default:
16630
16889
  continue;
16631
16890
  }
16632
- buffer[0] += str.substring(lastIndex, index) + escape;
16633
- lastIndex = index + 1;
16891
+ buffer[0] += str.substring(lastIndex, index2) + escape;
16892
+ lastIndex = index2 + 1;
16634
16893
  }
16635
- buffer[0] += str.substring(lastIndex, index);
16894
+ buffer[0] += str.substring(lastIndex, index2);
16636
16895
  };
16637
16896
  resolveCallbackSync = (str) => {
16638
16897
  const callbacks = str.callbacks;
@@ -17508,8 +17767,8 @@ function match(method, path) {
17508
17767
  if (!match3) {
17509
17768
  return [[], emptyParam];
17510
17769
  }
17511
- const index = match3.indexOf("", 1);
17512
- return [matcher[1][index], match3];
17770
+ const index2 = match3.indexOf("", 1);
17771
+ return [matcher[1][index2], match3];
17513
17772
  });
17514
17773
  this.match = match2;
17515
17774
  return match2(method, path);
@@ -17556,7 +17815,7 @@ var init_node = __esm({
17556
17815
  #index;
17557
17816
  #varIndex;
17558
17817
  #children = /* @__PURE__ */ Object.create(null);
17559
- insert(tokens, index, paramMap, context, pathErrorCheckOnly) {
17818
+ insert(tokens, index2, paramMap, context, pathErrorCheckOnly) {
17560
17819
  if (tokens.length === 0) {
17561
17820
  if (this.#index !== void 0) {
17562
17821
  throw PATH_ERROR;
@@ -17564,7 +17823,7 @@ var init_node = __esm({
17564
17823
  if (pathErrorCheckOnly) {
17565
17824
  return;
17566
17825
  }
17567
- this.#index = index;
17826
+ this.#index = index2;
17568
17827
  return;
17569
17828
  }
17570
17829
  const [token, ...restTokens] = tokens;
@@ -17614,7 +17873,7 @@ var init_node = __esm({
17614
17873
  node = this.#children[token] = new _Node();
17615
17874
  }
17616
17875
  }
17617
- node.insert(restTokens, index, paramMap, context, pathErrorCheckOnly);
17876
+ node.insert(restTokens, index2, paramMap, context, pathErrorCheckOnly);
17618
17877
  }
17619
17878
  buildRegExpStr() {
17620
17879
  const childKeys = Object.keys(this.#children).sort(compareKey);
@@ -17646,7 +17905,7 @@ var init_trie = __esm({
17646
17905
  Trie = class {
17647
17906
  #context = { varIndex: 0 };
17648
17907
  #root = new Node();
17649
- insert(path, index, pathErrorCheckOnly) {
17908
+ insert(path, index2, pathErrorCheckOnly) {
17650
17909
  const paramAssoc = [];
17651
17910
  const groups = [];
17652
17911
  for (let i = 0; ; ) {
@@ -17672,7 +17931,7 @@ var init_trie = __esm({
17672
17931
  }
17673
17932
  }
17674
17933
  }
17675
- this.#root.insert(tokens, index, paramAssoc, this.#context, pathErrorCheckOnly);
17934
+ this.#root.insert(tokens, index2, paramAssoc, this.#context, pathErrorCheckOnly);
17676
17935
  return paramAssoc;
17677
17936
  }
17678
17937
  buildRegExp() {
@@ -18266,8 +18525,19 @@ __export(web_exports, {
18266
18525
  dashboardApp: () => dashboardApp,
18267
18526
  default: () => web_default
18268
18527
  });
18269
- import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
18528
+ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync, mkdirSync as mkdirSync2, renameSync } from "fs";
18270
18529
  import { dirname as dirname2 } from "path";
18530
+ function parsePositiveInteger(value) {
18531
+ if (!/^\d+$/.test(value)) return null;
18532
+ const parsed = Number(value);
18533
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
18534
+ }
18535
+ function parseTaskStatus(value) {
18536
+ return TASK_STATUSES.has(value) ? value : null;
18537
+ }
18538
+ function safeStatus(value) {
18539
+ return value && TASK_STATUSES.has(value) ? value : "unknown";
18540
+ }
18271
18541
  function formatDuration(startAt, endAt) {
18272
18542
  if (!startAt) return "-";
18273
18543
  const start = new Date(startAt).getTime();
@@ -18305,17 +18575,21 @@ function esc(s) {
18305
18575
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
18306
18576
  }
18307
18577
  function readCurrentConfig() {
18308
- if (!existsSync3(CONFIG_PATH)) return {};
18578
+ const configPath = getConfigPath();
18579
+ if (!existsSync3(configPath)) return {};
18309
18580
  try {
18310
- return JSON.parse(readFileSync2(CONFIG_PATH, "utf-8"));
18581
+ return JSON.parse(readFileSync2(configPath, "utf-8"));
18311
18582
  } catch {
18312
18583
  return {};
18313
18584
  }
18314
18585
  }
18315
18586
  function writeConfig(cfg) {
18316
- const dir = dirname2(CONFIG_PATH);
18587
+ const configPath = getConfigPath();
18588
+ const dir = dirname2(configPath);
18317
18589
  if (!existsSync3(dir)) mkdirSync2(dir, { recursive: true });
18318
- writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2) + "\n");
18590
+ const tempPath = `${configPath}.${process.pid}.tmp`;
18591
+ writeFileSync(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
18592
+ renameSync(tempPath, configPath);
18319
18593
  }
18320
18594
  function renderLayout(title, activeTab, body) {
18321
18595
  return `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>${title} - SuperTask</title>${SHARED_STYLES}
@@ -18354,11 +18628,11 @@ async function saveConfig(){
18354
18628
  scheduler:{
18355
18629
  enabled:form.se.checked,
18356
18630
  checkIntervalMs:Number(form.si.value),
18357
- catchUp:form.cu.value,
18358
18631
  },
18359
18632
  watchdog:{
18360
18633
  heartbeatTimeoutMs:Number(form.wt.value)*1000,
18361
- cleanupIntervalMs:Number(form.wc.value)*1000,
18634
+ checkIntervalMs:Number(form.wci.value)*1000,
18635
+ cleanupIntervalMs:Number(form.wcl.value)*3600000,
18362
18636
  retentionDays:Number(form.rd.value),
18363
18637
  }
18364
18638
  };
@@ -18389,7 +18663,7 @@ async function saveConfig(){
18389
18663
  <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
18664
  </body></html>`;
18391
18665
  }
18392
- var app, SHARED_STYLES, dashboardApp, web_default;
18666
+ var app, TASK_STATUSES, SHARED_STYLES, dashboardApp, web_default;
18393
18667
  var init_web = __esm({
18394
18668
  "src/web/index.tsx"() {
18395
18669
  "use strict";
@@ -18401,7 +18675,47 @@ var init_web = __esm({
18401
18675
  init_drizzle_orm();
18402
18676
  init_db2();
18403
18677
  init_config();
18678
+ init_health();
18404
18679
  app = new Hono2();
18680
+ TASK_STATUSES = /* @__PURE__ */ new Set([
18681
+ "pending",
18682
+ "running",
18683
+ "done",
18684
+ "failed",
18685
+ "dead_letter",
18686
+ "cancelled"
18687
+ ]);
18688
+ app.use("*", async (c, next) => {
18689
+ await next();
18690
+ c.header("X-Content-Type-Options", "nosniff");
18691
+ c.header("X-Frame-Options", "DENY");
18692
+ c.header("Referrer-Policy", "no-referrer");
18693
+ });
18694
+ app.use("/api/*", async (c, next) => {
18695
+ if (["GET", "HEAD", "OPTIONS"].includes(c.req.method)) return next();
18696
+ const fetchSite = c.req.header("Sec-Fetch-Site");
18697
+ if (fetchSite && !["same-origin", "none"].includes(fetchSite)) {
18698
+ return c.json({ error: "cross-site request rejected" }, 403);
18699
+ }
18700
+ const origin = c.req.header("Origin");
18701
+ if (origin) {
18702
+ try {
18703
+ const originUrl = new URL(origin);
18704
+ const requestUrl = new URL(c.req.url);
18705
+ const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(originUrl.hostname);
18706
+ if (!loopback || originUrl.origin !== requestUrl.origin) {
18707
+ return c.json({ error: "cross-site request rejected" }, 403);
18708
+ }
18709
+ } catch {
18710
+ return c.json({ error: "invalid origin" }, 403);
18711
+ }
18712
+ }
18713
+ return next();
18714
+ });
18715
+ app.get("/health", (c) => {
18716
+ const health = getGatewayHealth();
18717
+ return c.json(health, health.status === "ok" ? 200 : 503);
18718
+ });
18405
18719
  SHARED_STYLES = html`
18406
18720
  <style>
18407
18721
  :root { --bg:#0d1117; --card:#161b22; --border:#30363d; --t1:#c9d1d9; --t2:#8b949e; --green:#238636; --red:#da3633; --yellow:#d29922; --blue:#1f6feb; --purple:#8957e5; }
@@ -18478,12 +18792,16 @@ var init_web = __esm({
18478
18792
  </style>
18479
18793
  `;
18480
18794
  app.get("/", async (c) => {
18481
- const page = Number(c.req.query("page") || "1");
18795
+ const pageParam = c.req.query("page") || "1";
18796
+ const page = parsePositiveInteger(pageParam);
18797
+ if (page === null) return c.text("invalid page", 400);
18482
18798
  const statusFilter = c.req.query("status") || "";
18799
+ const parsedStatus = statusFilter ? parseTaskStatus(statusFilter) : null;
18800
+ if (statusFilter && !parsedStatus) return c.text("invalid status", 400);
18483
18801
  const limit = 50;
18484
18802
  const offset = (page - 1) * limit;
18485
18803
  const [tasks3, statsData] = await Promise.all([
18486
- TaskService.list({ limit, offset, ...statusFilter ? { status: statusFilter } : {} }),
18804
+ TaskService.list({ limit, offset, ...parsedStatus ? { status: parsedStatus } : {} }),
18487
18805
  TaskService.stats({})
18488
18806
  ]);
18489
18807
  const taskIds = tasks3.map((t) => t.id);
@@ -18506,12 +18824,13 @@ var init_web = __esm({
18506
18824
  </div>`;
18507
18825
  let rows = "";
18508
18826
  for (const task of tasks3) {
18509
- const st = (task.status ?? "").toUpperCase();
18827
+ const status = safeStatus(task.status);
18828
+ const st = status.toUpperCase();
18510
18829
  rows += `<tr>
18511
18830
  <td class="mu">#${task.id}</td>
18512
18831
  <td><div style="font-weight:500">${esc(task.name)}</div><div class="mu sm el">${esc(task.prompt.substring(0, 120))}</div></td>
18513
18832
  <td><span class="tag">${esc(task.agent)}</span></td>
18514
- <td><span class="badge b-${task.status}">${st}</span></td>
18833
+ <td><span class="badge b-${status}">${st}</span></td>
18515
18834
  <td class="sm ${task.status === "running" ? "" : "mu"}">${formatDuration(task.startedAt, task.finishedAt)}</td>
18516
18835
  <td class="mu sm">${(task.retryCount ?? 0) > 0 ? task.retryCount : "-"}</td>
18517
18836
  <td>
@@ -18543,21 +18862,13 @@ var init_web = __esm({
18543
18862
  });
18544
18863
  app.get("/templates", async (c) => {
18545
18864
  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
18865
  const enabled = templates.filter((t) => t.enabled).length;
18556
18866
  const disabled = templates.length - enabled;
18557
18867
  let rows = "";
18558
18868
  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;
18869
+ const scheduleType = ["cron", "recurring", "delayed"].includes(t.scheduleType) ? t.scheduleType : "unknown";
18870
+ const typeLabel = scheduleType === "cron" ? "Cron" : scheduleType === "recurring" ? "\u5FAA\u73AF" : scheduleType === "delayed" ? "\u5B9A\u65F6" : "\u672A\u77E5";
18871
+ const typeClass = "tag t-" + scheduleType;
18561
18872
  let rule = "-";
18562
18873
  if (t.scheduleType === "cron") rule = t.cronExpr || "-";
18563
18874
  else if (t.scheduleType === "recurring") rule = t.intervalMs ? `${Math.floor(t.intervalMs / 6e4)}\u5206\u949F` : "-";
@@ -18569,7 +18880,7 @@ var init_web = __esm({
18569
18880
  <td><div style="font-weight:500">${esc(t.name)}</div><div class="mu sm el">${esc(t.prompt.substring(0, 100))}</div>
18570
18881
  <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
18882
  <td><span class="${typeClass}">${typeLabel}</span></td>
18572
- <td class="m sm">${rule}</td>
18883
+ <td class="m sm">${esc(rule)}</td>
18573
18884
  <td>${statusBadge}</td>
18574
18885
  <td class="sm">${t.lastRunAt ? timeAgo(t.lastRunAt) : "-"}</td>
18575
18886
  <td class="sm">${t.nextRunAt ? timeUntil(t.nextRunAt) : "-"}</td>
@@ -18597,7 +18908,8 @@ var init_web = __esm({
18597
18908
  return c.html(renderLayout("\u5B9A\u65F6\u4EFB\u52A1", "templates", body));
18598
18909
  });
18599
18910
  app.get("/runs", async (c) => {
18600
- const page = Number(c.req.query("page") || "1");
18911
+ const page = parsePositiveInteger(c.req.query("page") || "1");
18912
+ if (page === null) return c.text("invalid page", 400);
18601
18913
  const limit = 50;
18602
18914
  const offset = (page - 1) * limit;
18603
18915
  const { taskRuns: tr, tasks: tk } = schema_exports;
@@ -18615,13 +18927,14 @@ var init_web = __esm({
18615
18927
  childPid: tr.childPid,
18616
18928
  taskName: tk.name,
18617
18929
  taskAgent: tk.agent
18618
- }).from(tr).innerJoin(tk, eq(tr.taskId, tk.id)).orderBy(desc(tr.startedAt)).limit(limit).offset(offset);
18930
+ }).from(tr).innerJoin(tk, eq(tr.taskId, tk.id)).orderBy(desc(tr.startedAt), desc(tr.id)).limit(limit).offset(offset);
18619
18931
  const totalResult = await db.select({ count: sql`count(*)` }).from(tr);
18620
18932
  const total = Number(totalResult[0]?.count ?? 0);
18621
18933
  const totalPages = Math.ceil(total / limit);
18622
18934
  let rows = "";
18623
18935
  const logsHtml = [];
18624
18936
  for (const run of runs) {
18937
+ const status = safeStatus(run.status);
18625
18938
  const shortSession = run.sessionId ? run.sessionId.slice(4, 7) + "***" + run.sessionId.slice(-3) : "-";
18626
18939
  const logBtn = run.log ? `<button class="btn btn-sm" onclick="toggleLog(${run.id})">\u65E5\u5FD7</button>` : "";
18627
18940
  rows += `<tr>
@@ -18629,15 +18942,15 @@ var init_web = __esm({
18629
18942
  <td><div style="font-weight:500">${esc(run.taskName)} <span class="mu">(#${run.taskId})</span></div>
18630
18943
  ${run.model ? `<div class="sm"><span class="tag">${esc(run.model)}</span></div>` : ""}</td>
18631
18944
  <td><span class="tag">${esc(run.taskAgent)}</span></td>
18632
- <td><span class="badge b-${run.status}">${(run.status ?? "").toUpperCase()}</span></td>
18945
+ <td><span class="badge b-${status}">${status.toUpperCase()}</span></td>
18633
18946
  <td class="sm">${formatDuration(run.startedAt, run.finishedAt)}</td>
18634
18947
  <td class="sm mu">${run.heartbeatAt ? timeAgo(run.heartbeatAt) : "-"}</td>
18635
18948
  <td><button class="btn btn-sm" onclick="showRunDetail(${run.id})">\u8BE6\u60C5</button>${logBtn}</td>
18636
18949
  </tr>`;
18637
18950
  if (run.log) {
18638
18951
  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>`);
18952
+ <div class="panel"><div class="ph"><h3>Run #${run.id} \u65E5\u5FD7 \u2014 ${esc(run.taskName)}</h3></div>
18953
+ <div class="log-box">${esc(run.log)}</div></div></div>`);
18641
18954
  }
18642
18955
  }
18643
18956
  let paging = `<div class="pn">`;
@@ -18663,17 +18976,18 @@ var init_web = __esm({
18663
18976
  });
18664
18977
  app.get("/system", async (c) => {
18665
18978
  const config = loadConfig();
18979
+ const configPath = getConfigPath();
18666
18980
  const stats = await TaskService.stats({});
18667
18981
  const runningRuns = await TaskRunService.getAllRunningRuns();
18668
18982
  const templates = await TaskTemplateService.list(100);
18669
- const configExists = existsSync3(CONFIG_PATH);
18983
+ const configExists = existsSync3(configPath);
18670
18984
  let runRows = "";
18671
18985
  if (runningRuns.length > 0) {
18672
18986
  for (const run of runningRuns) {
18673
18987
  const shortS = run.sessionId ? run.sessionId.slice(4, 7) + "***" + run.sessionId.slice(-3) : "-";
18674
18988
  runRows += `<tr>
18675
18989
  <td class="mu">#${run.id}</td><td>#${run.taskId}</td>
18676
- <td class="m sm">${shortS}</td>
18990
+ <td class="m sm">${esc(shortS)}</td>
18677
18991
  <td class="sm">${esc(run.model) || "-"}</td>
18678
18992
  <td class="sm">${formatDate(run.startedAt)}</td>
18679
18993
  <td class="sm">${run.heartbeatAt ? timeAgo(run.heartbeatAt) : "-"}</td>
@@ -18698,19 +19012,14 @@ var init_web = __esm({
18698
19012
  <h3 style="margin:0 0 12px;font-size:14px">Scheduler \u914D\u7F6E</h3>
18699
19013
  <div class="form-row"><label>\u542F\u7528\u8C03\u5EA6</label><input type="checkbox" name="se" ${config.scheduler.enabled ? "checked" : ""}></div>
18700
19014
  <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
19015
  <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
19016
  </div>
18708
19017
  <div class="card">
18709
19018
  <h3 style="margin:0 0 12px;font-size:14px">Watchdog \u914D\u7F6E</h3>
18710
19019
  <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>
19020
+ <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>
19021
+ <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
19022
  <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
19023
  </div>
18715
19024
  </div>
18716
19025
  <div style="text-align:center;margin-bottom:24px">
@@ -18739,7 +19048,7 @@ var init_web = __esm({
18739
19048
 
18740
19049
  <div class="card mt16">
18741
19050
  <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>
19051
+ <div class="ir"><span class="ik">\u8DEF\u5F84</span><span class="iv m sm">${esc(configPath)}</span></div>
18743
19052
  <div class="ir"><span class="ik">\u6587\u4EF6\u5B58\u5728</span><span class="iv">${configFileStatus}</span></div>
18744
19053
  </div>
18745
19054
 
@@ -18751,46 +19060,61 @@ var init_web = __esm({
18751
19060
  return c.html(renderLayout("\u7CFB\u7EDF\u72B6\u6001", "system", body));
18752
19061
  });
18753
19062
  app.get("/api/tasks/:id", async (c) => {
18754
- const id = Number(c.req.param("id"));
19063
+ const id = parsePositiveInteger(c.req.param("id"));
19064
+ if (id === null) return c.json({ error: "invalid id" }, 400);
18755
19065
  const task = await TaskService.getById(id);
18756
19066
  if (!task) return c.json({ error: "not found" }, 404);
18757
19067
  const runs = await TaskRunService.listByTaskId(id);
18758
19068
  return c.json({ ...task, _runs: runs });
18759
19069
  });
18760
19070
  app.get("/api/runs/:id", async (c) => {
18761
- const id = Number(c.req.param("id"));
19071
+ const id = parsePositiveInteger(c.req.param("id"));
19072
+ if (id === null) return c.json({ error: "invalid id" }, 400);
18762
19073
  const run = await TaskRunService.getById(id);
18763
19074
  if (!run) return c.json({ error: "not found" }, 404);
18764
19075
  return c.json(run);
18765
19076
  });
18766
19077
  app.get("/api/templates/:id", async (c) => {
18767
- const id = Number(c.req.param("id"));
19078
+ const id = parsePositiveInteger(c.req.param("id"));
19079
+ if (id === null) return c.json({ error: "invalid id" }, 400);
18768
19080
  const tmpl = await TaskTemplateService.getById(id);
18769
19081
  if (!tmpl) return c.json({ error: "not found" }, 404);
18770
19082
  return c.json(tmpl);
18771
19083
  });
18772
19084
  app.post("/api/tasks/:id/retry", async (c) => {
18773
- await TaskService.retry(Number(c.req.param("id")));
18774
- return c.json({ success: true });
19085
+ const id = parsePositiveInteger(c.req.param("id"));
19086
+ if (id === null) return c.json({ error: "invalid id" }, 400);
19087
+ const task = await TaskService.retry(id);
19088
+ if (task) return c.json({ success: true });
19089
+ return await TaskService.getById(id) ? c.json({ error: "task status does not allow retry" }, 409) : c.json({ error: "not found" }, 404);
18775
19090
  });
18776
19091
  app.delete("/api/tasks/:id", async (c) => {
18777
- await TaskService.delete(Number(c.req.param("id")));
18778
- return c.json({ success: true });
19092
+ const id = parsePositiveInteger(c.req.param("id"));
19093
+ if (id === null) return c.json({ error: "invalid id" }, 400);
19094
+ const deleted = await TaskService.delete(id);
19095
+ return deleted ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
18779
19096
  });
18780
19097
  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 });
19098
+ const id = parsePositiveInteger(c.req.param("id"));
19099
+ if (id === null) return c.json({ error: "invalid id" }, 400);
19100
+ const result = await TaskTemplateService.enable(id);
19101
+ return result ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
18783
19102
  });
18784
19103
  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 });
19104
+ const id = parsePositiveInteger(c.req.param("id"));
19105
+ if (id === null) return c.json({ error: "invalid id" }, 400);
19106
+ const result = await TaskTemplateService.disable(id);
19107
+ return result ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
18787
19108
  });
18788
19109
  app.delete("/api/templates/:id", async (c) => {
18789
- const ok = await TaskTemplateService.delete(Number(c.req.param("id")));
18790
- return c.json({ success: ok });
19110
+ const id = parsePositiveInteger(c.req.param("id"));
19111
+ if (id === null) return c.json({ error: "invalid id" }, 400);
19112
+ const ok = await TaskTemplateService.delete(id);
19113
+ return ok ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
18791
19114
  });
18792
19115
  app.post("/api/templates/:id/trigger", async (c) => {
18793
- const id = Number(c.req.param("id"));
19116
+ const id = parsePositiveInteger(c.req.param("id"));
19117
+ if (id === null) return c.json({ error: "invalid id" }, 400);
18794
19118
  const tmpl = await TaskTemplateService.getById(id);
18795
19119
  if (!tmpl) return c.json({ error: "not found" }, 404);
18796
19120
  const task = await TaskService.add({
@@ -18802,7 +19126,10 @@ var init_web = __esm({
18802
19126
  category: tmpl.category,
18803
19127
  importance: tmpl.importance,
18804
19128
  urgency: tmpl.urgency,
19129
+ batchId: tmpl.batchId,
18805
19130
  maxRetries: tmpl.maxRetries,
19131
+ retryBackoffMs: tmpl.retryBackoffMs,
19132
+ timeoutMs: tmpl.timeoutMs,
18806
19133
  templateId: tmpl.id
18807
19134
  });
18808
19135
  return c.json({ success: true, taskId: task.id });
@@ -18817,22 +19144,29 @@ var init_web = __esm({
18817
19144
  const bW = body.worker ?? {};
18818
19145
  const bS = body.scheduler ?? {};
18819
19146
  const bD = body.watchdog ?? {};
18820
- const merged = { ...current, ...body, worker: { ...curW, ...bW }, scheduler: { ...curS, ...bS }, watchdog: { ...curD, ...bD } };
18821
- writeConfig(merged);
19147
+ const merged = {
19148
+ ...current,
19149
+ ...body,
19150
+ configVersion: 2,
19151
+ worker: { ...curW, ...bW },
19152
+ scheduler: { ...curS, ...bS },
19153
+ watchdog: { ...curD, ...bD }
19154
+ };
19155
+ writeConfig(validateConfig(merged));
18822
19156
  return c.json({ success: true });
18823
19157
  } catch (err) {
18824
- return c.json({ success: false, error: err instanceof Error ? err.message : String(err) });
19158
+ return c.json({ success: false, error: err instanceof Error ? err.message : String(err) }, 400);
18825
19159
  }
18826
19160
  });
18827
19161
  app.post("/api/database/clear", async (c) => {
18828
19162
  try {
18829
- const { tasks: tasks3, taskRuns: taskRuns3, taskTemplates: taskTemplates4 } = schema_exports;
18830
- await db.delete(taskRuns3);
19163
+ const { tasks: tasks3, taskRuns: taskRuns4, taskTemplates: taskTemplates4 } = schema_exports;
19164
+ await db.delete(taskRuns4);
18831
19165
  await db.delete(taskTemplates4);
18832
19166
  await db.delete(tasks3);
18833
19167
  return c.json({ success: true });
18834
19168
  } catch (err) {
18835
- return c.json({ success: false, error: err instanceof Error ? err.message : String(err) });
19169
+ return c.json({ success: false, error: err instanceof Error ? err.message : String(err) }, 500);
18836
19170
  }
18837
19171
  });
18838
19172
  dashboardApp = app;
@@ -18850,39 +19184,127 @@ init_config();
18850
19184
  // src/worker/index.ts
18851
19185
  init_task_service();
18852
19186
  init_task_run_service();
19187
+ init_health();
18853
19188
  import { spawn } from "child_process";
19189
+
19190
+ // src/core/process-control.ts
19191
+ import { spawnSync } from "child_process";
19192
+ import { basename } from "path";
19193
+ function isSafePid(pid) {
19194
+ return Number.isInteger(pid) && pid > 1 && pid !== process.pid;
19195
+ }
19196
+ function isProcessAlive(pid) {
19197
+ if (!Number.isInteger(pid) || pid <= 0) return false;
19198
+ try {
19199
+ process.kill(pid, 0);
19200
+ return true;
19201
+ } catch (error) {
19202
+ return error instanceof Error && "code" in error && error.code === "EPERM";
19203
+ }
19204
+ }
19205
+ function inspectUnixProcess(pid) {
19206
+ const result = spawnSync("ps", ["-o", "pgid=", "-o", "command=", "-p", String(pid)], {
19207
+ encoding: "utf8",
19208
+ stdio: ["ignore", "pipe", "ignore"]
19209
+ });
19210
+ if (result.status !== 0) return null;
19211
+ const match2 = result.stdout.trim().match(/^(\d+)\s+(.+)$/s);
19212
+ if (!match2) return null;
19213
+ return { processGroupId: Number(match2[1]), command: match2[2] };
19214
+ }
19215
+ function commandMatches(command, expectedExecutable) {
19216
+ const expectedName = basename(expectedExecutable).trim().toLowerCase();
19217
+ if (!expectedName) return false;
19218
+ return command.toLowerCase().includes(expectedName);
19219
+ }
19220
+ function signalSpawnedProcessTree(pid, signal) {
19221
+ if (!isSafePid(pid)) return false;
19222
+ if (process.platform !== "win32") {
19223
+ try {
19224
+ process.kill(-pid, signal);
19225
+ return true;
19226
+ } catch {
19227
+ }
19228
+ }
19229
+ try {
19230
+ process.kill(pid, signal);
19231
+ return true;
19232
+ } catch {
19233
+ return false;
19234
+ }
19235
+ }
19236
+ function signalRecordedProcessTree(pid, signal, expectedExecutable) {
19237
+ if (!isSafePid(pid)) return false;
19238
+ if (process.platform === "win32") {
19239
+ const list = spawnSync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
19240
+ encoding: "utf8",
19241
+ stdio: ["ignore", "pipe", "ignore"]
19242
+ });
19243
+ if (list.status !== 0 || !commandMatches(list.stdout, expectedExecutable)) return false;
19244
+ const args = ["/PID", String(pid), "/T"];
19245
+ if (signal === "SIGKILL") args.push("/F");
19246
+ return spawnSync("taskkill", args, { stdio: "ignore" }).status === 0;
19247
+ }
19248
+ const info = inspectUnixProcess(pid);
19249
+ if (!info || !commandMatches(info.command, expectedExecutable)) return false;
19250
+ try {
19251
+ if (info.processGroupId === pid) process.kill(-pid, signal);
19252
+ else process.kill(pid, signal);
19253
+ return true;
19254
+ } catch {
19255
+ return false;
19256
+ }
19257
+ }
19258
+
19259
+ // src/worker/index.ts
19260
+ var DEFAULT_MAX_OUTPUT_CHARS = 64 * 1024;
19261
+ var FORBIDDEN_AGENT = "supertask-runner";
18854
19262
  var WorkerEngine = class {
18855
19263
  activeBatchIds = /* @__PURE__ */ new Set();
18856
19264
  runningTasks = /* @__PURE__ */ new Map();
18857
19265
  stopped = false;
18858
19266
  pollTimer = null;
18859
19267
  heartbeatTimer = null;
19268
+ pollCyclePromise = null;
18860
19269
  cfg;
18861
- constructor(cfg) {
19270
+ opencodeBin;
19271
+ maxOutputChars;
19272
+ constructor(cfg, options = {}) {
18862
19273
  this.cfg = cfg.worker;
19274
+ this.opencodeBin = options.opencodeBin ?? process.env.SUPERTASK_OPENCODE_BIN ?? "opencode";
19275
+ this.maxOutputChars = options.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
18863
19276
  }
18864
19277
  start() {
18865
19278
  this.stopped = false;
19279
+ markGatewayActivity("worker");
18866
19280
  this.poll();
18867
19281
  this.heartbeatTimer = setInterval(() => this.updateHeartbeats(), this.cfg.heartbeatIntervalMs);
18868
19282
  }
18869
- stop() {
19283
+ async stop(gracePeriodMs = 0) {
18870
19284
  this.stopped = true;
18871
19285
  if (this.pollTimer) {
18872
19286
  clearTimeout(this.pollTimer);
18873
19287
  this.pollTimer = null;
18874
19288
  }
19289
+ if (this.pollCyclePromise) await this.pollCyclePromise;
19290
+ if (gracePeriodMs > 0 && this.runningTasks.size > 0) {
19291
+ const deadline = Date.now() + gracePeriodMs;
19292
+ while (this.runningTasks.size > 0 && Date.now() < deadline) {
19293
+ await Bun.sleep(Math.min(50, deadline - Date.now()));
19294
+ }
19295
+ }
18875
19296
  if (this.heartbeatTimer) {
18876
19297
  clearInterval(this.heartbeatTimer);
18877
19298
  this.heartbeatTimer = null;
18878
19299
  }
19300
+ const interruptedTaskIds = [...this.runningTasks.keys()];
18879
19301
  const killPromises = [];
18880
- for (const [, entry] of this.runningTasks) {
19302
+ for (const entry of this.runningTasks.values()) {
18881
19303
  entry.shutdown = true;
18882
19304
  killPromises.push(this.killEntry(entry));
18883
19305
  }
18884
- return Promise.allSettled(killPromises).then(() => {
18885
- });
19306
+ await Promise.allSettled(killPromises);
19307
+ return interruptedTaskIds;
18886
19308
  }
18887
19309
  getRunningTaskIds() {
18888
19310
  return [...this.runningTasks.keys()];
@@ -18892,136 +19314,244 @@ var WorkerEngine = class {
18892
19314
  }
18893
19315
  poll() {
18894
19316
  if (this.stopped) return;
18895
- this.tryDispatch().then(() => {
19317
+ markGatewayActivity("worker");
19318
+ this.pollCyclePromise = this.tryDispatch().finally(() => {
19319
+ this.pollCyclePromise = null;
18896
19320
  if (this.stopped) return;
18897
19321
  this.pollTimer = setTimeout(() => this.poll(), this.cfg.pollIntervalMs);
18898
19322
  });
18899
19323
  }
18900
19324
  async tryDispatch() {
19325
+ await this.reconcileCancelledTasks();
18901
19326
  while (!this.stopped && this.runningTasks.size < this.cfg.maxConcurrency) {
19327
+ let task;
19328
+ try {
19329
+ task = await TaskService.next({ excludedBatchIds: [...this.activeBatchIds] });
19330
+ } catch (err) {
19331
+ this.logError("task claim failed", err);
19332
+ break;
19333
+ }
19334
+ if (!task) break;
19335
+ if (this.stopped) break;
19336
+ if (!await TaskService.start(task.id)) continue;
19337
+ if (task.batchId) this.activeBatchIds.add(task.batchId);
19338
+ if (this.stopped) {
19339
+ await TaskService.resetRunningToPending([task.id]);
19340
+ this.releaseBatch(task);
19341
+ break;
19342
+ }
18902
19343
  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
19344
  const run = await TaskRunService.create({
18911
19345
  taskId: task.id,
18912
19346
  model: this.resolveModel(task.model),
18913
19347
  status: "running"
18914
19348
  });
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 }));
19349
+ if (this.stopped) {
19350
+ await TaskRunService.fail(run.id, "Gateway shutdown before spawn");
19351
+ await TaskService.resetRunningToPending([task.id]);
19352
+ this.releaseBatch(task);
19353
+ break;
19354
+ }
19355
+ if (task.agent === FORBIDDEN_AGENT) {
19356
+ const message = `\u7981\u6B62\u6267\u884C\u9012\u5F52 Agent: ${FORBIDDEN_AGENT}`;
19357
+ await TaskRunService.fail(run.id, message);
19358
+ await TaskService.fail(task.id, message, {}, { setDeadLetter: true });
19359
+ this.releaseBatch(task);
19360
+ continue;
19361
+ }
19362
+ this.spawnTask(task, run.id);
19363
+ } catch (err) {
19364
+ this.releaseBatch(task);
19365
+ const message = `Worker \u542F\u52A8\u4EFB\u52A1\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`;
19366
+ try {
19367
+ await TaskService.fail(task.id, message);
19368
+ } catch (failErr) {
19369
+ this.logError("failed to compensate task startup", failErr, task.id);
19370
+ }
19371
+ this.logError("task dispatch failed", err, task.id);
19372
+ }
19373
+ }
19374
+ }
19375
+ spawnTask(task, runId) {
19376
+ const model = this.resolveModel(task.model);
19377
+ const args = ["run", "--agent", task.agent, "--format", "json"];
19378
+ if (model) args.push("-m", model);
19379
+ args.push(task.prompt);
19380
+ const child = spawn(this.opencodeBin, args, {
19381
+ cwd: task.cwd || process.cwd(),
19382
+ stdio: ["ignore", "pipe", "pipe"],
19383
+ detached: process.platform !== "win32"
19384
+ });
19385
+ const entry = {
19386
+ task,
19387
+ runId,
19388
+ child,
19389
+ output: "",
19390
+ sessionId: null,
19391
+ timeoutTimer: null,
19392
+ shutdown: false,
19393
+ settled: false
19394
+ };
19395
+ this.runningTasks.set(task.id, entry);
19396
+ const handleData = (data) => {
19397
+ const text2 = data.toString();
19398
+ entry.output = (entry.output + text2).slice(-this.maxOutputChars);
19399
+ process.stdout.write(text2);
19400
+ const match2 = entry.output.match(/"sessionID"\s*:\s*"(ses_[^"]+)"/);
19401
+ if (match2?.[1] && match2[1] !== entry.sessionId) {
19402
+ entry.sessionId = match2[1];
19403
+ TaskRunService.updateSessionId(runId, match2[1]).catch((err) => {
19404
+ this.logError("sessionId update failed", err, task.id);
18972
19405
  });
19406
+ }
19407
+ };
19408
+ child.stdout?.on("data", handleData);
19409
+ child.stderr?.on("data", handleData);
19410
+ child.once("error", (err) => {
19411
+ void this.finishEntry(entry, null, `\u65E0\u6CD5\u542F\u52A8 opencode\uFF1A${err.message}`);
19412
+ });
19413
+ child.once("close", (code, signal) => {
19414
+ const failure = code === 0 ? void 0 : `opencode \u9000\u51FA\u7801 ${code ?? "null"}${signal ? `\uFF0C\u4FE1\u53F7 ${signal}` : ""}`;
19415
+ void this.finishEntry(entry, code, failure);
19416
+ });
19417
+ const timeoutMs = task.timeoutMs ?? this.cfg.taskTimeoutMs;
19418
+ if (timeoutMs > 0) {
19419
+ entry.timeoutTimer = setTimeout(() => {
19420
+ this.signalEntry(entry, "SIGKILL");
19421
+ void this.finishEntry(entry, null, `\u4EFB\u52A1\u8D85\u65F6\uFF08${timeoutMs}ms\uFF09`);
19422
+ }, timeoutMs);
19423
+ }
19424
+ TaskRunService.updatePid(runId, process.pid, child.pid ?? 0).catch((err) => {
19425
+ this.signalEntry(entry, "SIGKILL");
19426
+ void this.finishEntry(entry, null, `\u8BB0\u5F55 Worker PID \u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`);
19427
+ });
19428
+ }
19429
+ async finishEntry(entry, code, failure) {
19430
+ if (entry.settled) return;
19431
+ entry.settled = true;
19432
+ if (entry.timeoutTimer) {
19433
+ clearTimeout(entry.timeoutTimer);
19434
+ entry.timeoutTimer = null;
19435
+ }
19436
+ try {
19437
+ if (entry.shutdown) return;
19438
+ const currentRun = await TaskRunService.getById(entry.runId);
19439
+ if (!currentRun || currentRun.status !== "running") return;
19440
+ const output = entry.output.trim();
19441
+ const log = failure ? `${failure}${output ? `
19442
+ ${output}` : ""}` : output;
19443
+ if (code === 0 && !failure) {
19444
+ const completed = await TaskService.done(entry.task.id, log);
19445
+ if (completed) {
19446
+ await TaskRunService.done(entry.runId, log);
19447
+ console.log(JSON.stringify({
19448
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
19449
+ level: "info",
19450
+ msg: "task done",
19451
+ taskId: entry.task.id
19452
+ }));
19453
+ return;
19454
+ }
19455
+ await TaskRunService.fail(entry.runId, "\u4EFB\u52A1\u72B6\u6001\u5DF2\u88AB\u5176\u4ED6\u64CD\u4F5C\u6539\u53D8");
19456
+ return;
19457
+ }
19458
+ await TaskRunService.fail(entry.runId, log);
19459
+ const failed = await TaskService.fail(entry.task.id, log);
19460
+ if (!failed) {
19461
+ this.logError("task failure state transition rejected", failure ?? "unknown failure", entry.task.id);
19462
+ }
19463
+ console.error(JSON.stringify({
19464
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
19465
+ level: "error",
19466
+ msg: "task failed",
19467
+ taskId: entry.task.id,
19468
+ error: failure
19469
+ }));
19470
+ } finally {
19471
+ this.runningTasks.delete(entry.task.id);
19472
+ this.releaseBatch(entry.task);
19473
+ }
19474
+ }
19475
+ async reconcileCancelledTasks() {
19476
+ for (const entry of [...this.runningTasks.values()]) {
19477
+ try {
19478
+ const task = await TaskService.getById(entry.task.id);
19479
+ if (task?.status === "cancelled") await this.cancelEntry(entry);
18973
19480
  } 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;
19481
+ this.logError("cancel reconciliation failed", err, entry.task.id);
18981
19482
  }
18982
19483
  }
18983
19484
  }
19485
+ async cancelEntry(entry) {
19486
+ if (entry.settled) return;
19487
+ entry.settled = true;
19488
+ if (entry.timeoutTimer) {
19489
+ clearTimeout(entry.timeoutTimer);
19490
+ entry.timeoutTimer = null;
19491
+ }
19492
+ try {
19493
+ await this.killEntry(entry);
19494
+ const output = entry.output.trim();
19495
+ const log = `\u4EFB\u52A1\u5DF2\u53D6\u6D88${output ? `
19496
+ ${output}` : ""}`;
19497
+ await TaskRunService.fail(entry.runId, log);
19498
+ console.log(JSON.stringify({
19499
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
19500
+ level: "info",
19501
+ msg: "running task cancelled",
19502
+ taskId: entry.task.id
19503
+ }));
19504
+ } finally {
19505
+ this.runningTasks.delete(entry.task.id);
19506
+ this.releaseBatch(entry.task);
19507
+ }
19508
+ }
18984
19509
  async updateHeartbeats() {
18985
- for (const [, entry] of this.runningTasks) {
19510
+ for (const entry of this.runningTasks.values()) {
18986
19511
  try {
18987
19512
  await TaskRunService.heartbeat(entry.runId);
18988
- } catch {
19513
+ } catch (err) {
19514
+ this.logError("heartbeat update failed", err, entry.task.id);
18989
19515
  }
18990
19516
  }
18991
19517
  }
18992
19518
  killEntry(entry) {
18993
- if (entry.child.exitCode !== null) {
19519
+ if (entry.child.exitCode !== null || entry.child.signalCode !== null) {
18994
19520
  return Promise.resolve();
18995
19521
  }
18996
19522
  return new Promise((resolve) => {
18997
19523
  const timeout = setTimeout(() => {
18998
- try {
18999
- if (entry.child.pid) process.kill(entry.child.pid, "SIGKILL");
19000
- } catch {
19001
- }
19524
+ this.signalEntry(entry, "SIGKILL");
19002
19525
  resolve();
19003
19526
  }, 5e3);
19004
- entry.child.on("close", () => {
19527
+ entry.child.once("close", () => {
19005
19528
  clearTimeout(timeout);
19006
19529
  resolve();
19007
19530
  });
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
- }
19531
+ this.signalEntry(entry, "SIGTERM");
19019
19532
  });
19020
19533
  }
19534
+ signalEntry(entry, signal) {
19535
+ const pid = entry.child.pid;
19536
+ if (!pid) return;
19537
+ signalSpawnedProcessTree(pid, signal);
19538
+ }
19539
+ releaseBatch(task) {
19540
+ if (task.batchId) this.activeBatchIds.delete(task.batchId);
19541
+ }
19021
19542
  resolveModel(taskModel) {
19022
19543
  if (!taskModel || taskModel === "default") return null;
19023
19544
  return taskModel;
19024
19545
  }
19546
+ logError(message, error, taskId) {
19547
+ console.error(JSON.stringify({
19548
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
19549
+ level: "error",
19550
+ msg: message,
19551
+ taskId,
19552
+ error: error instanceof Error ? error.message : String(error)
19553
+ }));
19554
+ }
19025
19555
  };
19026
19556
 
19027
19557
  // src/gateway/watchdog/heartbeat.ts
@@ -19034,15 +19564,23 @@ async function checkHeartbeats(heartbeatTimeoutMs) {
19034
19564
  for (const run of staleRuns) {
19035
19565
  try {
19036
19566
  if (run.childPid != null && run.childPid > 0) {
19037
- try {
19038
- process.kill(run.childPid, "SIGKILL");
19039
- } catch {
19567
+ const expectedExecutable = process.env.SUPERTASK_OPENCODE_BIN ?? "opencode";
19568
+ const killed = signalRecordedProcessTree(run.childPid, "SIGKILL", expectedExecutable);
19569
+ if (!killed) {
19570
+ console.warn(JSON.stringify({
19571
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
19572
+ level: "warn",
19573
+ msg: "stale child process was not killed because identity validation failed",
19574
+ taskId: run.taskId,
19575
+ runId: run.runId,
19576
+ childPid: run.childPid
19577
+ }));
19040
19578
  }
19041
19579
  }
19042
19580
  await TaskRunService.fail(run.runId, `\u5FC3\u8DF3\u8D85\u65F6 (${heartbeatTimeoutMs / 1e3}s)\uFF0CWatchdog kill`);
19043
19581
  const newRetryCount = run.taskRetryCount + 1;
19044
19582
  const maxRetries = run.taskMaxRetries;
19045
- if (newRetryCount >= maxRetries) {
19583
+ if (newRetryCount > maxRetries) {
19046
19584
  await TaskService.markDeadLetter(run.taskId, newRetryCount);
19047
19585
  console.log(JSON.stringify({
19048
19586
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -19054,7 +19592,7 @@ async function checkHeartbeats(heartbeatTimeoutMs) {
19054
19592
  maxRetries
19055
19593
  }));
19056
19594
  } else {
19057
- const backoffMs = computeBackoff(newRetryCount);
19595
+ const backoffMs = computeBackoff(newRetryCount, run.taskRetryBackoffMs);
19058
19596
  const retryAfter = Date.now() + backoffMs;
19059
19597
  await TaskService.markPendingForRetry(run.taskId, retryAfter, newRetryCount);
19060
19598
  console.log(JSON.stringify({
@@ -19110,6 +19648,7 @@ async function cleanupOldRecords(retentionDays) {
19110
19648
  }
19111
19649
 
19112
19650
  // src/gateway/watchdog/index.ts
19651
+ init_health();
19113
19652
  var Watchdog = class {
19114
19653
  constructor(cfg) {
19115
19654
  this.cfg = cfg;
@@ -19120,13 +19659,14 @@ var Watchdog = class {
19120
19659
  cleanupTimer = null;
19121
19660
  start() {
19122
19661
  this.stopped = false;
19662
+ markGatewayActivity("watchdog");
19123
19663
  this.heartbeatTimer = setInterval(
19124
19664
  () => this.runHeartbeatCheck(),
19125
- this.cfg.watchdog.cleanupIntervalMs
19665
+ this.cfg.watchdog.checkIntervalMs
19126
19666
  );
19127
19667
  this.cleanupTimer = setInterval(
19128
19668
  () => this.runCleanup(),
19129
- this.cfg.watchdog.cleanupIntervalMs * 24 * 60
19669
+ this.cfg.watchdog.cleanupIntervalMs
19130
19670
  );
19131
19671
  }
19132
19672
  stop() {
@@ -19142,6 +19682,7 @@ var Watchdog = class {
19142
19682
  }
19143
19683
  async runHeartbeatCheck() {
19144
19684
  if (this.stopped) return;
19685
+ markGatewayActivity("watchdog");
19145
19686
  try {
19146
19687
  await checkHeartbeats(this.cfg.watchdog.heartbeatTimeoutMs);
19147
19688
  } catch (err) {
@@ -19177,7 +19718,7 @@ var { taskTemplates: taskTemplates3 } = schema_exports;
19177
19718
  async function cloneTaskFromTemplate(templateId) {
19178
19719
  const rows = await db.select().from(taskTemplates3).where(eq(taskTemplates3.id, templateId)).limit(1);
19179
19720
  const tmpl = rows[0];
19180
- if (!tmpl) return null;
19721
+ if (!tmpl || !tmpl.enabled) return null;
19181
19722
  const activeCount = await db.select({ count: sql`count(*)` }).from(schema_exports.tasks).where(
19182
19723
  and(
19183
19724
  eq(schema_exports.tasks.templateId, templateId),
@@ -19186,7 +19727,8 @@ async function cloneTaskFromTemplate(templateId) {
19186
19727
  ).then((r) => r[0].count);
19187
19728
  if (activeCount >= (tmpl.maxInstances ?? 1)) return null;
19188
19729
  const nowMs = Date.now();
19189
- const nextRunAt = TaskTemplateService.calculateNextRunAt(
19730
+ const isDelayed = tmpl.scheduleType === "delayed";
19731
+ const nextRunAt = isDelayed ? null : TaskTemplateService.calculateNextRunAt(
19190
19732
  tmpl.scheduleType,
19191
19733
  tmpl,
19192
19734
  nowMs
@@ -19200,13 +19742,17 @@ async function cloneTaskFromTemplate(templateId) {
19200
19742
  category: tmpl.category ?? "general",
19201
19743
  importance: tmpl.importance ?? 3,
19202
19744
  urgency: tmpl.urgency ?? 3,
19745
+ batchId: tmpl.batchId,
19203
19746
  maxRetries: tmpl.maxRetries ?? 3,
19747
+ retryBackoffMs: tmpl.retryBackoffMs ?? 3e4,
19748
+ timeoutMs: tmpl.timeoutMs,
19204
19749
  templateId: tmpl.id,
19205
19750
  scheduledAt: nowMs
19206
19751
  });
19207
19752
  await db.update(taskTemplates3).set({
19208
19753
  lastRunAt: nowMs,
19209
19754
  nextRunAt,
19755
+ enabled: isDelayed ? false : tmpl.enabled,
19210
19756
  updatedAt: nowMs
19211
19757
  }).where(eq(taskTemplates3.id, templateId));
19212
19758
  return task;
@@ -19219,7 +19765,7 @@ async function getDueTemplates() {
19219
19765
  sql`${taskTemplates3.nextRunAt} IS NOT NULL`,
19220
19766
  sql`${taskTemplates3.nextRunAt} <= ${nowMs}`
19221
19767
  )
19222
- );
19768
+ ).orderBy(asc(taskTemplates3.nextRunAt), asc(taskTemplates3.id));
19223
19769
  }
19224
19770
  async function initializeNextRunAt(templateId) {
19225
19771
  const rows = await db.select().from(taskTemplates3).where(eq(taskTemplates3.id, templateId)).limit(1);
@@ -19237,6 +19783,7 @@ async function initializeNextRunAt(templateId) {
19237
19783
  // src/gateway/scheduler/index.ts
19238
19784
  init_db2();
19239
19785
  init_drizzle_orm();
19786
+ init_health();
19240
19787
  var Scheduler = class {
19241
19788
  constructor(cfg) {
19242
19789
  this.cfg = cfg;
@@ -19248,6 +19795,7 @@ var Scheduler = class {
19248
19795
  async start() {
19249
19796
  if (!this.cfg.scheduler.enabled) return;
19250
19797
  this.stopped = false;
19798
+ markGatewayActivity("scheduler");
19251
19799
  await this.initializeTemplates();
19252
19800
  this.timer = setInterval(() => this.tick(), this.cfg.scheduler.checkIntervalMs);
19253
19801
  }
@@ -19260,6 +19808,7 @@ var Scheduler = class {
19260
19808
  }
19261
19809
  async tick() {
19262
19810
  if (this.stopped || this.ticking) return;
19811
+ markGatewayActivity("scheduler");
19263
19812
  this.ticking = true;
19264
19813
  try {
19265
19814
  const dueTemplates = await getDueTemplates();
@@ -19309,6 +19858,7 @@ var Scheduler = class {
19309
19858
  init_db2();
19310
19859
  init_task_service();
19311
19860
  init_task_run_service();
19861
+ init_health();
19312
19862
  var STALE_THRESHOLD_MS = 3e4;
19313
19863
  function acquireLock() {
19314
19864
  const now = Date.now();
@@ -19317,7 +19867,8 @@ function acquireLock() {
19317
19867
  sqlite.exec("BEGIN IMMEDIATE");
19318
19868
  const existing = sqlite.prepare("SELECT id, pid, heartbeat_at FROM gateway_lock WHERE id = 1").get();
19319
19869
  if (existing) {
19320
- if (now - existing.heartbeat_at < STALE_THRESHOLD_MS) {
19870
+ const lockHolderAlive = existing.pid !== pid && isProcessAlive(existing.pid);
19871
+ if (now - existing.heartbeat_at < STALE_THRESHOLD_MS && lockHolderAlive) {
19321
19872
  sqlite.exec("ROLLBACK");
19322
19873
  console.error(JSON.stringify({
19323
19874
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -19330,7 +19881,7 @@ function acquireLock() {
19330
19881
  sqlite.exec("DELETE FROM gateway_lock WHERE id = 1");
19331
19882
  }
19332
19883
  sqlite.exec(
19333
- "INSERT INTO gateway_lock (id, pid, acquired_at, heartbeat_at) VALUES (1, ?, ?, ?)",
19884
+ "INSERT INTO gateway_lock (id, pid, acquired_at, heartbeat_at, ready_at) VALUES (1, ?, ?, ?, NULL)",
19334
19885
  [pid, now, now]
19335
19886
  );
19336
19887
  sqlite.exec("COMMIT");
@@ -19364,6 +19915,18 @@ function updateLockHeartbeat() {
19364
19915
  } catch {
19365
19916
  }
19366
19917
  }
19918
+ function markGatewayReady() {
19919
+ sqlite.exec(
19920
+ "UPDATE gateway_lock SET heartbeat_at = ?, ready_at = ? WHERE pid = ?",
19921
+ [Date.now(), Date.now(), process.pid]
19922
+ );
19923
+ }
19924
+ function markGatewayNotReady() {
19925
+ try {
19926
+ sqlite.exec("UPDATE gateway_lock SET ready_at = NULL WHERE pid = ?", [process.pid]);
19927
+ } catch {
19928
+ }
19929
+ }
19367
19930
  async function main() {
19368
19931
  console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: "SuperTask Gateway starting", pid: process.pid }));
19369
19932
  if (!acquireLock()) {
@@ -19375,6 +19938,21 @@ async function main() {
19375
19938
  const worker = new WorkerEngine(cfg);
19376
19939
  const watchdog = new Watchdog(cfg);
19377
19940
  const scheduler = new Scheduler(cfg);
19941
+ const recoveredOrphans = await TaskService.resetOrphanRunningToPending();
19942
+ if (recoveredOrphans > 0) {
19943
+ console.log(JSON.stringify({
19944
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
19945
+ level: "warn",
19946
+ msg: "reset orphan running tasks to pending",
19947
+ count: recoveredOrphans
19948
+ }));
19949
+ }
19950
+ initializeGatewayHealth({
19951
+ workerPollIntervalMs: cfg.worker.pollIntervalMs,
19952
+ schedulerEnabled: cfg.scheduler.enabled,
19953
+ schedulerCheckIntervalMs: cfg.scheduler.checkIntervalMs,
19954
+ watchdogCheckIntervalMs: cfg.watchdog.checkIntervalMs
19955
+ });
19378
19956
  worker.start();
19379
19957
  watchdog.start();
19380
19958
  await scheduler.start();
@@ -19392,6 +19970,7 @@ async function main() {
19392
19970
  url: `http://localhost:${cfg.dashboard.port}`
19393
19971
  }));
19394
19972
  }
19973
+ markGatewayReady();
19395
19974
  console.log(JSON.stringify({
19396
19975
  ts: (/* @__PURE__ */ new Date()).toISOString(),
19397
19976
  level: "info",
@@ -19405,10 +19984,10 @@ async function main() {
19405
19984
  shuttingDown = true;
19406
19985
  console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: `received ${signal}, shutting down...` }));
19407
19986
  clearInterval(heartbeatTimer);
19987
+ markGatewayNotReady();
19408
19988
  scheduler.stop();
19409
19989
  watchdog.stop();
19410
- const runningIds = worker.getRunningTaskIds();
19411
- await worker.stop();
19990
+ const runningIds = await worker.stop(cfg.worker.shutdownGracePeriodMs);
19412
19991
  if (runningIds.length > 0) {
19413
19992
  const resetCount = await TaskService.resetRunningToPending(runningIds);
19414
19993
  console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: "reset running tasks to pending", count: resetCount }));
@@ -19418,6 +19997,7 @@ async function main() {
19418
19997
  await TaskRunService.fail(run.id, "Gateway shutdown");
19419
19998
  }
19420
19999
  releaseLock();
20000
+ resetGatewayHealth();
19421
20001
  closeDb();
19422
20002
  console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: "Gateway stopped" }));
19423
20003
  process.exit(0);
@@ -19437,6 +20017,8 @@ if (import.meta.main) {
19437
20017
  main();
19438
20018
  }
19439
20019
  export {
19440
- main
20020
+ acquireLock,
20021
+ main,
20022
+ releaseLock
19441
20023
  };
19442
20024
  //# sourceMappingURL=index.js.map