opencode-supertask 0.1.19 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
 
@@ -6084,7 +6099,15 @@ import { join, dirname } from "path";
6084
6099
  import { fileURLToPath } from "url";
6085
6100
  function getMigrationsFolder() {
6086
6101
  const __dirname = dirname(fileURLToPath(import.meta.url));
6087
- return join(__dirname, "../../../drizzle");
6102
+ let dir = __dirname;
6103
+ for (let i = 0; i < 5; i++) {
6104
+ const candidate = join(dir, "drizzle", "meta", "_journal.json");
6105
+ if (existsSync(candidate)) {
6106
+ return join(dir, "drizzle");
6107
+ }
6108
+ dir = dirname(dir);
6109
+ }
6110
+ return join(__dirname, "../../drizzle");
6088
6111
  }
6089
6112
  function initDb() {
6090
6113
  const dataDir = dirname(DB_FILE_PATH);
@@ -6099,16 +6122,30 @@ function initDb() {
6099
6122
  id INTEGER PRIMARY KEY CHECK (id = 1),
6100
6123
  pid INTEGER NOT NULL,
6101
6124
  acquired_at INTEGER NOT NULL,
6102
- heartbeat_at INTEGER NOT NULL
6125
+ heartbeat_at INTEGER NOT NULL,
6126
+ ready_at INTEGER
6103
6127
  );
6104
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
+ }
6105
6133
  _db = drizzle(_sqlite, { schema: schema_exports });
6106
6134
  if (!_migrationRan) {
6107
- _migrationRan = true;
6108
6135
  try {
6109
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;
6110
6143
  } catch (err) {
6111
6144
  const msg = err instanceof Error ? err.message : String(err);
6145
+ _migrationRan = false;
6146
+ _sqlite.close();
6147
+ _sqlite = null;
6148
+ _db = null;
6112
6149
  console.error(`[supertask] migration failed: ${msg}`);
6113
6150
  throw new Error(`[supertask] DB migration failed: ${msg}`);
6114
6151
  }
@@ -6128,6 +6165,7 @@ function closeDb() {
6128
6165
  _sqlite.close();
6129
6166
  _sqlite = null;
6130
6167
  _db = null;
6168
+ _migrationRan = false;
6131
6169
  }
6132
6170
  }
6133
6171
  var DEFAULT_DB_PATH, DB_FILE_PATH, _sqlite, _db, _migrationRan, db, sqlite;
@@ -6162,71 +6200,123 @@ var init_db2 = __esm({
6162
6200
  });
6163
6201
 
6164
6202
  // src/gateway/config.ts
6165
- import { readFileSync, existsSync as existsSync2 } from "fs";
6203
+ import { existsSync as existsSync2, readFileSync } from "fs";
6166
6204
  import { homedir as homedir2 } from "os";
6167
6205
  import { join as join2 } from "path";
6168
- function deepMerge(base, override) {
6169
- const result = { ...base };
6170
- for (const key of Object.keys(override)) {
6171
- const val = override[key];
6172
- if (val !== null && typeof val === "object" && !Array.isArray(val) && key in result) {
6173
- const baseVal = result[key];
6174
- if (baseVal !== null && typeof baseVal === "object" && !Array.isArray(baseVal)) {
6175
- result[key] = deepMerge(baseVal, val);
6176
- continue;
6177
- }
6178
- }
6179
- if (val !== void 0) {
6180
- result[key] = val;
6181
- }
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`);
6182
6213
  }
6183
- return result;
6214
+ return value;
6215
+ }
6216
+ function integerAt(source, key, fallback, min, max, path) {
6217
+ const value = source[key];
6218
+ if (value === void 0) return fallback;
6219
+ if (typeof value !== "number" || !Number.isInteger(value) || value < min || value > max) {
6220
+ throw new Error(`${path}.${key} \u5FC5\u987B\u662F ${min} \u5230 ${max} \u4E4B\u95F4\u7684\u6574\u6570`);
6221
+ }
6222
+ return value;
6223
+ }
6224
+ function booleanAt(source, key, fallback, path) {
6225
+ const value = source[key];
6226
+ if (value === void 0) return fallback;
6227
+ if (typeof value !== "boolean") throw new Error(`${path}.${key} \u5FC5\u987B\u662F\u5E03\u5C14\u503C`);
6228
+ return value;
6184
6229
  }
6185
- function loadConfig() {
6186
- if (!existsSync2(CONFIG_PATH)) {
6187
- return DEFAULT_CONFIG;
6230
+ function validateConfig(input) {
6231
+ const root = objectAt(input, "config");
6232
+ const version2 = root.configVersion ?? 1;
6233
+ if (version2 !== 1 && version2 !== 2) {
6234
+ throw new Error("config.configVersion \u53EA\u652F\u6301 1 \u6216 2");
6188
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 });
6189
6284
  try {
6190
- const raw2 = readFileSync(CONFIG_PATH, "utf-8");
6191
- const userConfig = JSON.parse(raw2);
6192
- return deepMerge(DEFAULT_CONFIG, userConfig);
6193
- } catch (err) {
6194
- const msg = err instanceof Error ? err.message : String(err);
6195
- console.error(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "warn", msg: "config load failed, using defaults", path: CONFIG_PATH, error: msg }));
6196
- 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}`);
6197
6289
  }
6198
6290
  }
6199
- var DEFAULT_CONFIG, CONFIG_PATH;
6291
+ var DEFAULT_CONFIG, DEFAULT_CONFIG_PATH;
6200
6292
  var init_config = __esm({
6201
6293
  "src/gateway/config.ts"() {
6202
6294
  "use strict";
6203
6295
  DEFAULT_CONFIG = {
6296
+ configVersion: 2,
6204
6297
  worker: {
6205
6298
  maxConcurrency: 2,
6206
6299
  pollIntervalMs: 1e3,
6207
6300
  heartbeatIntervalMs: 3e4,
6208
- taskTimeoutMs: 18e5
6301
+ taskTimeoutMs: 18e5,
6302
+ shutdownGracePeriodMs: 3e4
6209
6303
  },
6210
6304
  scheduler: {
6211
6305
  enabled: true,
6212
- checkIntervalMs: 1e3,
6213
- catchUp: "next"
6306
+ checkIntervalMs: 1e3
6214
6307
  },
6215
6308
  watchdog: {
6216
6309
  heartbeatTimeoutMs: 6e5,
6217
- cleanupIntervalMs: 6e4,
6310
+ checkIntervalMs: 6e4,
6311
+ cleanupIntervalMs: 864e5,
6218
6312
  retentionDays: 30
6219
6313
  },
6220
6314
  dashboard: {
6221
6315
  enabled: true,
6222
6316
  port: 4680
6223
- },
6224
- logging: {
6225
- level: "info",
6226
- format: "json"
6227
6317
  }
6228
6318
  };
6229
- CONFIG_PATH = join2(homedir2(), ".config/opencode/supertask.json");
6319
+ DEFAULT_CONFIG_PATH = join2(homedir2(), ".config/opencode/supertask.json");
6230
6320
  }
6231
6321
  });
6232
6322
 
@@ -6280,14 +6370,14 @@ var init_backoff = __esm({
6280
6370
  });
6281
6371
 
6282
6372
  // src/core/services/task.service.ts
6283
- var tasks2, TaskService;
6373
+ var tasks2, taskRuns2, TaskService;
6284
6374
  var init_task_service = __esm({
6285
6375
  "src/core/services/task.service.ts"() {
6286
6376
  "use strict";
6287
6377
  init_db2();
6288
6378
  init_drizzle_orm();
6289
6379
  init_backoff();
6290
- ({ tasks: tasks2 } = schema_exports);
6380
+ ({ tasks: tasks2, taskRuns: taskRuns2 } = schema_exports);
6291
6381
  TaskService = class {
6292
6382
  static buildScopeWhere(scope) {
6293
6383
  const conditions = [];
@@ -6297,9 +6387,27 @@ var init_task_service = __esm({
6297
6387
  return conditions;
6298
6388
  }
6299
6389
  static async add(data) {
6390
+ this.validateNewTask(data);
6300
6391
  const result = await db.insert(tasks2).values(data).returning();
6301
6392
  return result[0];
6302
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
+ }
6303
6411
  static async next(scope = {}) {
6304
6412
  const baseConditions = [...this.buildScopeWhere(scope)];
6305
6413
  const nowMs = Date.now();
@@ -6322,7 +6430,7 @@ var init_task_service = __esm({
6322
6430
  ),
6323
6431
  and(
6324
6432
  eq(tasks2.status, "failed"),
6325
- sql`${tasks2.retryCount} < ${tasks2.maxRetries}`,
6433
+ sql`${tasks2.retryCount} <= ${tasks2.maxRetries}`,
6326
6434
  retryAfterFilter
6327
6435
  )
6328
6436
  );
@@ -6336,7 +6444,8 @@ var init_task_service = __esm({
6336
6444
  const allTasks = await db.select().from(tasks2).where(and(...conditions)).orderBy(
6337
6445
  desc(tasks2.urgency),
6338
6446
  desc(tasks2.importance),
6339
- asc(tasks2.createdAt)
6447
+ asc(tasks2.createdAt),
6448
+ asc(tasks2.id)
6340
6449
  );
6341
6450
  for (const task of allTasks) {
6342
6451
  if (task.dependsOn) {
@@ -6357,7 +6466,7 @@ var init_task_service = __esm({
6357
6466
  eq(tasks2.status, "pending"),
6358
6467
  and(
6359
6468
  eq(tasks2.status, "failed"),
6360
- sql`${tasks2.retryCount} < ${tasks2.maxRetries}`
6469
+ sql`${tasks2.retryCount} <= ${tasks2.maxRetries}`
6361
6470
  )
6362
6471
  ),
6363
6472
  ...this.buildScopeWhere(scope)
@@ -6370,7 +6479,11 @@ var init_task_service = __esm({
6370
6479
  return result[0] || null;
6371
6480
  }
6372
6481
  static async done(id, log, scope = {}) {
6373
- 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
+ ];
6374
6487
  const result = await db.update(tasks2).set({
6375
6488
  status: "done",
6376
6489
  finishedAt: /* @__PURE__ */ new Date(),
@@ -6381,17 +6494,24 @@ var init_task_service = __esm({
6381
6494
  }
6382
6495
  static async fail(id, log, scope = {}, options) {
6383
6496
  const current = await this.getById(id, scope);
6384
- if (!current) return null;
6497
+ if (!current || current.status !== "running") return null;
6385
6498
  const newRetryCount = (current.retryCount ?? 0) + 1;
6386
6499
  const maxRetries = current.maxRetries ?? 3;
6387
- const isDeadLetter = options?.setDeadLetter ?? newRetryCount >= maxRetries;
6388
- 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
+ ];
6389
6506
  const result = await db.update(tasks2).set({
6390
6507
  status: isDeadLetter ? "dead_letter" : "failed",
6391
6508
  finishedAt: /* @__PURE__ */ new Date(),
6392
6509
  resultLog: log,
6393
6510
  retryCount: newRetryCount,
6394
- 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
+ )
6395
6515
  }).where(and(...conditions)).returning();
6396
6516
  return result[0] || null;
6397
6517
  }
@@ -6424,8 +6544,20 @@ var init_task_service = __esm({
6424
6544
  return result.length;
6425
6545
  }
6426
6546
  static async cancel(id, scope = {}) {
6427
- const conditions = [eq(tasks2.id, id), ...this.buildScopeWhere(scope)];
6428
- const result = await db.update(tasks2).set({ status: "cancelled" }).where(and(...conditions)).returning();
6547
+ const conditions = [
6548
+ eq(tasks2.id, id),
6549
+ or(
6550
+ eq(tasks2.status, "pending"),
6551
+ eq(tasks2.status, "running"),
6552
+ eq(tasks2.status, "failed")
6553
+ ),
6554
+ ...this.buildScopeWhere(scope)
6555
+ ];
6556
+ const result = await db.update(tasks2).set({
6557
+ status: "cancelled",
6558
+ finishedAt: /* @__PURE__ */ new Date(),
6559
+ retryAfter: null
6560
+ }).where(and(...conditions)).returning();
6429
6561
  return result[0] || null;
6430
6562
  }
6431
6563
  static async retry(id, scope = {}) {
@@ -6437,7 +6569,8 @@ var init_task_service = __esm({
6437
6569
  status: "pending",
6438
6570
  startedAt: null,
6439
6571
  finishedAt: null,
6440
- retryAfter: null
6572
+ retryAfter: null,
6573
+ retryCount: 0
6441
6574
  }).where(and(...conditions)).returning();
6442
6575
  return result[0] || null;
6443
6576
  }
@@ -6451,7 +6584,8 @@ var init_task_service = __esm({
6451
6584
  status: "pending",
6452
6585
  startedAt: null,
6453
6586
  finishedAt: null,
6454
- retryAfter: null
6587
+ retryAfter: null,
6588
+ retryCount: 0
6455
6589
  }).where(and(...conditions)).returning();
6456
6590
  return result.length;
6457
6591
  }
@@ -6519,6 +6653,9 @@ var init_task_service = __esm({
6519
6653
  }
6520
6654
  static async delete(id, scope = {}) {
6521
6655
  const conditions = [eq(tasks2.id, id), ...this.buildScopeWhere(scope)];
6656
+ const existing = await db.select({ id: tasks2.id }).from(tasks2).where(and(...conditions)).limit(1);
6657
+ if (!existing[0]) return false;
6658
+ await db.delete(taskRuns2).where(eq(taskRuns2.taskId, id));
6522
6659
  const result = await db.delete(tasks2).where(and(...conditions)).returning();
6523
6660
  return result.length > 0;
6524
6661
  }
@@ -6538,65 +6675,65 @@ var init_task_service = __esm({
6538
6675
  });
6539
6676
 
6540
6677
  // src/core/services/task-run.service.ts
6541
- var taskRuns2, TaskRunService;
6678
+ var taskRuns3, TaskRunService;
6542
6679
  var init_task_run_service = __esm({
6543
6680
  "src/core/services/task-run.service.ts"() {
6544
6681
  "use strict";
6545
6682
  init_db2();
6546
6683
  init_drizzle_orm();
6547
- ({ taskRuns: taskRuns2 } = schema_exports);
6684
+ ({ taskRuns: taskRuns3 } = schema_exports);
6548
6685
  TaskRunService = class {
6549
6686
  static async create(data) {
6550
- const result = await db.insert(taskRuns2).values(data).returning();
6687
+ const result = await db.insert(taskRuns3).values(data).returning();
6551
6688
  return result[0];
6552
6689
  }
6553
6690
  static async updateSessionId(id, sessionId) {
6554
- const result = await db.update(taskRuns2).set({ sessionId }).where(eq(taskRuns2.id, id)).returning();
6691
+ const result = await db.update(taskRuns3).set({ sessionId }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
6555
6692
  return result[0] || null;
6556
6693
  }
6557
6694
  static async done(id, log) {
6558
- const result = await db.update(taskRuns2).set({
6695
+ const result = await db.update(taskRuns3).set({
6559
6696
  status: "done",
6560
6697
  finishedAt: /* @__PURE__ */ new Date(),
6561
6698
  log
6562
- }).where(eq(taskRuns2.id, id)).returning();
6699
+ }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
6563
6700
  return result[0] || null;
6564
6701
  }
6565
6702
  static async fail(id, log) {
6566
- const result = await db.update(taskRuns2).set({
6703
+ const result = await db.update(taskRuns3).set({
6567
6704
  status: "failed",
6568
6705
  finishedAt: /* @__PURE__ */ new Date(),
6569
6706
  log
6570
- }).where(eq(taskRuns2.id, id)).returning();
6707
+ }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
6571
6708
  return result[0] || null;
6572
6709
  }
6573
6710
  static async heartbeat(id) {
6574
- const result = await db.update(taskRuns2).set({ heartbeatAt: Date.now() }).where(and(eq(taskRuns2.id, id), eq(taskRuns2.status, "running"))).returning();
6711
+ const result = await db.update(taskRuns3).set({ heartbeatAt: Date.now() }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
6575
6712
  return result[0] || null;
6576
6713
  }
6577
6714
  static async updatePid(id, workerPid, childPid) {
6578
- const result = await db.update(taskRuns2).set({
6715
+ const result = await db.update(taskRuns3).set({
6579
6716
  workerPid,
6580
6717
  childPid,
6581
6718
  lockedAt: Date.now(),
6582
6719
  lockedBy: `gateway-${process.pid}`
6583
- }).where(eq(taskRuns2.id, id)).returning();
6720
+ }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
6584
6721
  return result[0] || null;
6585
6722
  }
6586
6723
  static async getById(id) {
6587
- const result = await db.select().from(taskRuns2).where(eq(taskRuns2.id, id));
6724
+ const result = await db.select().from(taskRuns3).where(eq(taskRuns3.id, id));
6588
6725
  return result[0] || null;
6589
6726
  }
6590
6727
  static async listByTaskId(taskId) {
6591
- return await db.select().from(taskRuns2).where(eq(taskRuns2.taskId, taskId)).orderBy(desc(taskRuns2.startedAt), desc(taskRuns2.id));
6728
+ return await db.select().from(taskRuns3).where(eq(taskRuns3.taskId, taskId)).orderBy(desc(taskRuns3.startedAt), desc(taskRuns3.id));
6592
6729
  }
6593
6730
  static async getLatestByTaskId(taskId) {
6594
- const result = await db.select().from(taskRuns2).where(eq(taskRuns2.taskId, taskId)).orderBy(desc(taskRuns2.startedAt), desc(taskRuns2.id)).limit(1);
6731
+ const result = await db.select().from(taskRuns3).where(eq(taskRuns3.taskId, taskId)).orderBy(desc(taskRuns3.startedAt), desc(taskRuns3.id)).limit(1);
6595
6732
  return result[0] || null;
6596
6733
  }
6597
6734
  static async getLatestByTaskIds(taskIds) {
6598
6735
  if (taskIds.length === 0) return /* @__PURE__ */ new Map();
6599
- const latestRuns = await db.select().from(taskRuns2).where(inArray(taskRuns2.taskId, taskIds)).orderBy(desc(taskRuns2.startedAt));
6736
+ const latestRuns = await db.select().from(taskRuns3).where(inArray(taskRuns3.taskId, taskIds)).orderBy(desc(taskRuns3.startedAt), desc(taskRuns3.id));
6600
6737
  const result = /* @__PURE__ */ new Map();
6601
6738
  for (const run of latestRuns) {
6602
6739
  if (!result.has(run.taskId)) {
@@ -6610,22 +6747,23 @@ var init_task_run_service = __esm({
6610
6747
  const cutoffSec = Math.floor(cutoffMs / 1e3);
6611
6748
  const { tasks: tasksTable } = schema_exports;
6612
6749
  const result = await db.select({
6613
- runId: taskRuns2.id,
6614
- taskId: taskRuns2.taskId,
6615
- childPid: taskRuns2.childPid,
6750
+ runId: taskRuns3.id,
6751
+ taskId: taskRuns3.taskId,
6752
+ childPid: taskRuns3.childPid,
6616
6753
  taskRetryCount: tasksTable.retryCount,
6617
- taskMaxRetries: tasksTable.maxRetries
6618
- }).from(taskRuns2).innerJoin(tasksTable, eq(taskRuns2.taskId, tasksTable.id)).where(
6754
+ taskMaxRetries: tasksTable.maxRetries,
6755
+ taskRetryBackoffMs: tasksTable.retryBackoffMs
6756
+ }).from(taskRuns3).innerJoin(tasksTable, eq(taskRuns3.taskId, tasksTable.id)).where(
6619
6757
  and(
6620
- eq(taskRuns2.status, "running"),
6758
+ eq(taskRuns3.status, "running"),
6621
6759
  or(
6622
6760
  and(
6623
- sql`${taskRuns2.heartbeatAt} IS NULL`,
6624
- sql`${taskRuns2.startedAt} < ${cutoffSec}`
6761
+ sql`${taskRuns3.heartbeatAt} IS NULL`,
6762
+ sql`${taskRuns3.startedAt} < ${cutoffSec}`
6625
6763
  ),
6626
6764
  and(
6627
- sql`${taskRuns2.heartbeatAt} IS NOT NULL`,
6628
- sql`${taskRuns2.heartbeatAt} < ${cutoffMs}`
6765
+ sql`${taskRuns3.heartbeatAt} IS NOT NULL`,
6766
+ sql`${taskRuns3.heartbeatAt} < ${cutoffMs}`
6629
6767
  )
6630
6768
  )
6631
6769
  )
@@ -6635,25 +6773,111 @@ var init_task_run_service = __esm({
6635
6773
  taskId: row.taskId,
6636
6774
  childPid: row.childPid,
6637
6775
  taskRetryCount: row.taskRetryCount ?? 0,
6638
- taskMaxRetries: row.taskMaxRetries ?? 3
6776
+ taskMaxRetries: row.taskMaxRetries ?? 3,
6777
+ taskRetryBackoffMs: row.taskRetryBackoffMs ?? 3e4
6639
6778
  }));
6640
6779
  }
6641
6780
  static async getRunningRunByTaskId(taskId) {
6642
- const result = await db.select().from(taskRuns2).where(and(eq(taskRuns2.taskId, taskId), eq(taskRuns2.status, "running"))).orderBy(desc(taskRuns2.startedAt), desc(taskRuns2.id)).limit(1);
6781
+ const result = await db.select().from(taskRuns3).where(and(eq(taskRuns3.taskId, taskId), eq(taskRuns3.status, "running"))).orderBy(desc(taskRuns3.startedAt), desc(taskRuns3.id)).limit(1);
6643
6782
  return result[0] || null;
6644
6783
  }
6645
6784
  static async deleteByTaskIds(taskIds) {
6646
6785
  if (taskIds.length === 0) return 0;
6647
- const result = await db.delete(taskRuns2).where(inArray(taskRuns2.taskId, taskIds)).returning();
6786
+ const result = await db.delete(taskRuns3).where(inArray(taskRuns3.taskId, taskIds)).returning();
6648
6787
  return result.length;
6649
6788
  }
6650
6789
  static async getAllRunningRuns() {
6651
- return await db.select().from(taskRuns2).where(eq(taskRuns2.status, "running"));
6790
+ return await db.select().from(taskRuns3).where(eq(taskRuns3.status, "running"));
6652
6791
  }
6653
6792
  };
6654
6793
  }
6655
6794
  });
6656
6795
 
6796
+ // src/gateway/health.ts
6797
+ function initializeGatewayHealth(config) {
6798
+ const now = Date.now();
6799
+ state = {
6800
+ startedAt: now,
6801
+ config,
6802
+ lastActivityAt: {
6803
+ worker: now,
6804
+ scheduler: now,
6805
+ watchdog: now
6806
+ }
6807
+ };
6808
+ }
6809
+ function markGatewayActivity(component) {
6810
+ if (state) state.lastActivityAt[component] = Date.now();
6811
+ }
6812
+ function resetGatewayHealth() {
6813
+ state = null;
6814
+ }
6815
+ function componentStatus(component, enabled, maxAgeMs, now) {
6816
+ const lastActivityAt = state?.lastActivityAt[component] ?? null;
6817
+ const ageMs = lastActivityAt == null ? null : Math.max(0, now - lastActivityAt);
6818
+ return {
6819
+ enabled,
6820
+ healthy: !enabled || ageMs != null && ageMs <= maxAgeMs,
6821
+ lastActivityAt,
6822
+ ageMs,
6823
+ maxAgeMs
6824
+ };
6825
+ }
6826
+ function getGatewayHealth(now = Date.now()) {
6827
+ const worker = componentStatus(
6828
+ "worker",
6829
+ true,
6830
+ Math.max((state?.config.workerPollIntervalMs ?? 1e3) * 5, 5e3),
6831
+ now
6832
+ );
6833
+ const scheduler = componentStatus(
6834
+ "scheduler",
6835
+ state?.config.schedulerEnabled ?? false,
6836
+ Math.max((state?.config.schedulerCheckIntervalMs ?? 1e3) * 5, 5e3),
6837
+ now
6838
+ );
6839
+ const watchdog = componentStatus(
6840
+ "watchdog",
6841
+ true,
6842
+ Math.max((state?.config.watchdogCheckIntervalMs ?? 6e4) * 3, 5e3),
6843
+ now
6844
+ );
6845
+ let lock = { pid: null, heartbeatAt: null, readyAt: null, ageMs: null, healthy: false };
6846
+ try {
6847
+ const row = sqlite.prepare(
6848
+ "SELECT pid, heartbeat_at, ready_at FROM gateway_lock WHERE id = 1"
6849
+ ).get();
6850
+ if (row) {
6851
+ const ageMs = Math.max(0, now - row.heartbeat_at);
6852
+ lock = {
6853
+ pid: row.pid,
6854
+ heartbeatAt: row.heartbeat_at,
6855
+ readyAt: row.ready_at,
6856
+ ageMs,
6857
+ healthy: row.pid === process.pid && row.ready_at != null && ageMs < LOCK_STALE_MS
6858
+ };
6859
+ }
6860
+ } catch {
6861
+ }
6862
+ const healthy = state != null && lock.healthy && worker.healthy && scheduler.healthy && watchdog.healthy;
6863
+ return {
6864
+ status: healthy ? "ok" : "degraded",
6865
+ pid: process.pid,
6866
+ uptimeMs: state ? Math.max(0, now - state.startedAt) : 0,
6867
+ lock,
6868
+ components: { worker, scheduler, watchdog }
6869
+ };
6870
+ }
6871
+ var LOCK_STALE_MS, state;
6872
+ var init_health = __esm({
6873
+ "src/gateway/health.ts"() {
6874
+ "use strict";
6875
+ init_db2();
6876
+ LOCK_STALE_MS = 3e4;
6877
+ state = null;
6878
+ }
6879
+ });
6880
+
6657
6881
  // node_modules/cron-parser/dist/fields/types.js
6658
6882
  var require_types = __commonJS({
6659
6883
  "node_modules/cron-parser/dist/fields/types.js"(exports) {
@@ -6854,7 +7078,7 @@ var require_CronField = __commonJS({
6854
7078
  if (!isValidRange) {
6855
7079
  throw new Error(`${this.constructor.name} Validation error, got value ${badValue} expected range ${this.min}-${this.max}${charsString}`);
6856
7080
  }
6857
- const duplicate = this.#values.find((value, index) => this.#values.indexOf(value) !== index);
7081
+ const duplicate = this.#values.find((value, index2) => this.#values.indexOf(value) !== index2);
6858
7082
  if (duplicate) {
6859
7083
  throw new Error(`${this.constructor.name} Validation error, duplicate values found: ${duplicate}`);
6860
7084
  }
@@ -14699,11 +14923,11 @@ var require_CronFieldCollection = __commonJS({
14699
14923
  throw new Error("Unexpected range end");
14700
14924
  }
14701
14925
  if (step * multiplier > range.end) {
14702
- const mapFn = (_, index) => {
14926
+ const mapFn = (_, index2) => {
14703
14927
  if (typeof range.start !== "number") {
14704
14928
  throw new Error("Unexpected range start");
14705
14929
  }
14706
- return index % step === 0 ? range.start + index : null;
14930
+ return index2 % step === 0 ? range.start + index2 : null;
14707
14931
  };
14708
14932
  if (typeof range.start !== "number") {
14709
14933
  throw new Error("Unexpected range start");
@@ -15595,9 +15819,9 @@ var require_CronExpressionParser = __commonJS({
15595
15819
  if (field === CronUnit.DayOfWeek && max % 7 === 0) {
15596
15820
  stack.push(0);
15597
15821
  }
15598
- for (let index = min; index <= max; index += repeatInterval) {
15599
- if (stack.indexOf(index) === -1) {
15600
- stack.push(index);
15822
+ for (let index2 = min; index2 <= max; index2 += repeatInterval) {
15823
+ if (stack.indexOf(index2) === -1) {
15824
+ stack.push(index2);
15601
15825
  }
15602
15826
  }
15603
15827
  return stack;
@@ -15820,11 +16044,6 @@ var require_dist = __commonJS({
15820
16044
  });
15821
16045
 
15822
16046
  // src/core/cron-parser.ts
15823
- var cron_parser_exports = {};
15824
- __export(cron_parser_exports, {
15825
- getNextCronRun: () => getNextCronRun,
15826
- isValidCronExpr: () => isValidCronExpr
15827
- });
15828
16047
  function getNextCronRun(expr, afterMs) {
15829
16048
  try {
15830
16049
  const fromDate = afterMs != null ? new Date(afterMs) : /* @__PURE__ */ new Date();
@@ -15862,6 +16081,7 @@ var init_task_template_service = __esm({
15862
16081
  ({ taskTemplates: taskTemplates2 } = schema_exports);
15863
16082
  TaskTemplateService = class {
15864
16083
  static async create(data) {
16084
+ this.validate(data);
15865
16085
  const now = Date.now();
15866
16086
  const result = await db.insert(taskTemplates2).values({ ...data, createdAt: now, updatedAt: now }).returning();
15867
16087
  const tmpl = result[0];
@@ -15877,8 +16097,38 @@ var init_task_template_service = __esm({
15877
16097
  }
15878
16098
  return tmpl;
15879
16099
  }
16100
+ static validate(data) {
16101
+ if (!data.name.trim()) throw new Error("name \u4E0D\u80FD\u4E3A\u7A7A");
16102
+ if (!data.agent.trim()) throw new Error("agent \u4E0D\u80FD\u4E3A\u7A7A");
16103
+ if (!data.prompt.trim()) throw new Error("prompt \u4E0D\u80FD\u4E3A\u7A7A");
16104
+ const scheduleType = data.scheduleType;
16105
+ if (!["cron", "delayed", "recurring"].includes(scheduleType)) {
16106
+ throw new Error("scheduleType \u5FC5\u987B\u662F cron\u3001delayed \u6216 recurring");
16107
+ }
16108
+ if (scheduleType === "cron" && (!data.cronExpr || !isValidCronExpr(data.cronExpr))) {
16109
+ throw new Error("cronExpr \u7F3A\u5931\u6216\u683C\u5F0F\u65E0\u6548");
16110
+ }
16111
+ if (scheduleType === "recurring" && (!Number.isInteger(data.intervalMs) || (data.intervalMs ?? 0) <= 0)) {
16112
+ throw new Error("intervalMs \u5FC5\u987B\u662F\u6B63\u6574\u6570");
16113
+ }
16114
+ if (scheduleType === "delayed" && (!Number.isInteger(data.runAt) || (data.runAt ?? 0) <= 0)) {
16115
+ throw new Error("runAt \u5FC5\u987B\u662F\u6B63\u6574\u6570\u65F6\u95F4\u6233");
16116
+ }
16117
+ this.validateInteger("importance", data.importance, 1, 5);
16118
+ this.validateInteger("urgency", data.urgency, 1, 5);
16119
+ this.validateInteger("maxInstances", data.maxInstances, 1, 1e3);
16120
+ this.validateInteger("maxRetries", data.maxRetries, 0, 1e3);
16121
+ this.validateInteger("retryBackoffMs", data.retryBackoffMs, 0, 864e5);
16122
+ this.validateInteger("timeoutMs", data.timeoutMs, 1e3, 6048e5);
16123
+ }
16124
+ static validateInteger(name, value, min, max) {
16125
+ if (value === void 0 || value === null) return;
16126
+ if (!Number.isInteger(value) || value < min || value > max) {
16127
+ throw new Error(`${name} \u5FC5\u987B\u662F ${min} \u5230 ${max} \u4E4B\u95F4\u7684\u6574\u6570`);
16128
+ }
16129
+ }
15880
16130
  static async list(limit = 50) {
15881
- return await db.select().from(taskTemplates2).orderBy(desc(taskTemplates2.createdAt)).limit(limit);
16131
+ return await db.select().from(taskTemplates2).orderBy(desc(taskTemplates2.createdAt), desc(taskTemplates2.id)).limit(limit);
15882
16132
  }
15883
16133
  static async getById(id) {
15884
16134
  const result = await db.select().from(taskTemplates2).where(eq(taskTemplates2.id, id));
@@ -15925,13 +16175,13 @@ var init_compose = __esm({
15925
16175
  "use strict";
15926
16176
  compose = (middleware, onError, onNotFound) => {
15927
16177
  return (context, next) => {
15928
- let index = -1;
16178
+ let index2 = -1;
15929
16179
  return dispatch(0);
15930
16180
  async function dispatch(i) {
15931
- if (i <= index) {
16181
+ if (i <= index2) {
15932
16182
  throw new Error("next() called multiple times");
15933
16183
  }
15934
- index = i;
16184
+ index2 = i;
15935
16185
  let res;
15936
16186
  let isError = false;
15937
16187
  let handler;
@@ -16046,8 +16296,8 @@ var init_body = __esm({
16046
16296
  handleParsingNestedValues = (form, key, value) => {
16047
16297
  let nestedForm = form;
16048
16298
  const keys = key.split(".");
16049
- keys.forEach((key2, index) => {
16050
- if (index === keys.length - 1) {
16299
+ keys.forEach((key2, index2) => {
16300
+ if (index2 === keys.length - 1) {
16051
16301
  nestedForm[key2] = value;
16052
16302
  } else {
16053
16303
  if (!nestedForm[key2] || typeof nestedForm[key2] !== "object" || Array.isArray(nestedForm[key2]) || nestedForm[key2] instanceof File) {
@@ -16079,8 +16329,8 @@ var init_url = __esm({
16079
16329
  };
16080
16330
  extractGroupsFromPath = (path) => {
16081
16331
  const groups = [];
16082
- path = path.replace(/\{[^}]+\}/g, (match2, index) => {
16083
- const mark = `@${index}`;
16332
+ path = path.replace(/\{[^}]+\}/g, (match2, index2) => {
16333
+ const mark = `@${index2}`;
16084
16334
  groups.push([mark, match2]);
16085
16335
  return mark;
16086
16336
  });
@@ -16599,10 +16849,10 @@ var init_html = __esm({
16599
16849
  return;
16600
16850
  }
16601
16851
  let escape;
16602
- let index;
16852
+ let index2;
16603
16853
  let lastIndex = 0;
16604
- for (index = match2; index < str.length; index++) {
16605
- switch (str.charCodeAt(index)) {
16854
+ for (index2 = match2; index2 < str.length; index2++) {
16855
+ switch (str.charCodeAt(index2)) {
16606
16856
  case 34:
16607
16857
  escape = "&quot;";
16608
16858
  break;
@@ -16621,10 +16871,10 @@ var init_html = __esm({
16621
16871
  default:
16622
16872
  continue;
16623
16873
  }
16624
- buffer[0] += str.substring(lastIndex, index) + escape;
16625
- lastIndex = index + 1;
16874
+ buffer[0] += str.substring(lastIndex, index2) + escape;
16875
+ lastIndex = index2 + 1;
16626
16876
  }
16627
- buffer[0] += str.substring(lastIndex, index);
16877
+ buffer[0] += str.substring(lastIndex, index2);
16628
16878
  };
16629
16879
  resolveCallbackSync = (str) => {
16630
16880
  const callbacks = str.callbacks;
@@ -17500,8 +17750,8 @@ function match(method, path) {
17500
17750
  if (!match3) {
17501
17751
  return [[], emptyParam];
17502
17752
  }
17503
- const index = match3.indexOf("", 1);
17504
- return [matcher[1][index], match3];
17753
+ const index2 = match3.indexOf("", 1);
17754
+ return [matcher[1][index2], match3];
17505
17755
  });
17506
17756
  this.match = match2;
17507
17757
  return match2(method, path);
@@ -17548,7 +17798,7 @@ var init_node = __esm({
17548
17798
  #index;
17549
17799
  #varIndex;
17550
17800
  #children = /* @__PURE__ */ Object.create(null);
17551
- insert(tokens, index, paramMap, context, pathErrorCheckOnly) {
17801
+ insert(tokens, index2, paramMap, context, pathErrorCheckOnly) {
17552
17802
  if (tokens.length === 0) {
17553
17803
  if (this.#index !== void 0) {
17554
17804
  throw PATH_ERROR;
@@ -17556,7 +17806,7 @@ var init_node = __esm({
17556
17806
  if (pathErrorCheckOnly) {
17557
17807
  return;
17558
17808
  }
17559
- this.#index = index;
17809
+ this.#index = index2;
17560
17810
  return;
17561
17811
  }
17562
17812
  const [token, ...restTokens] = tokens;
@@ -17606,7 +17856,7 @@ var init_node = __esm({
17606
17856
  node = this.#children[token] = new _Node();
17607
17857
  }
17608
17858
  }
17609
- node.insert(restTokens, index, paramMap, context, pathErrorCheckOnly);
17859
+ node.insert(restTokens, index2, paramMap, context, pathErrorCheckOnly);
17610
17860
  }
17611
17861
  buildRegExpStr() {
17612
17862
  const childKeys = Object.keys(this.#children).sort(compareKey);
@@ -17638,7 +17888,7 @@ var init_trie = __esm({
17638
17888
  Trie = class {
17639
17889
  #context = { varIndex: 0 };
17640
17890
  #root = new Node();
17641
- insert(path, index, pathErrorCheckOnly) {
17891
+ insert(path, index2, pathErrorCheckOnly) {
17642
17892
  const paramAssoc = [];
17643
17893
  const groups = [];
17644
17894
  for (let i = 0; ; ) {
@@ -17664,7 +17914,7 @@ var init_trie = __esm({
17664
17914
  }
17665
17915
  }
17666
17916
  }
17667
- this.#root.insert(tokens, index, paramAssoc, this.#context, pathErrorCheckOnly);
17917
+ this.#root.insert(tokens, index2, paramAssoc, this.#context, pathErrorCheckOnly);
17668
17918
  return paramAssoc;
17669
17919
  }
17670
17920
  buildRegExp() {
@@ -18258,8 +18508,19 @@ __export(web_exports, {
18258
18508
  dashboardApp: () => dashboardApp,
18259
18509
  default: () => web_default
18260
18510
  });
18261
- import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
18511
+ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync, mkdirSync as mkdirSync2, renameSync } from "fs";
18262
18512
  import { dirname as dirname2 } from "path";
18513
+ function parsePositiveInteger(value) {
18514
+ if (!/^\d+$/.test(value)) return null;
18515
+ const parsed = Number(value);
18516
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
18517
+ }
18518
+ function parseTaskStatus(value) {
18519
+ return TASK_STATUSES.has(value) ? value : null;
18520
+ }
18521
+ function safeStatus(value) {
18522
+ return value && TASK_STATUSES.has(value) ? value : "unknown";
18523
+ }
18263
18524
  function formatDuration(startAt, endAt) {
18264
18525
  if (!startAt) return "-";
18265
18526
  const start = new Date(startAt).getTime();
@@ -18297,17 +18558,21 @@ function esc(s) {
18297
18558
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
18298
18559
  }
18299
18560
  function readCurrentConfig() {
18300
- if (!existsSync3(CONFIG_PATH)) return {};
18561
+ const configPath = getConfigPath();
18562
+ if (!existsSync3(configPath)) return {};
18301
18563
  try {
18302
- return JSON.parse(readFileSync2(CONFIG_PATH, "utf-8"));
18564
+ return JSON.parse(readFileSync2(configPath, "utf-8"));
18303
18565
  } catch {
18304
18566
  return {};
18305
18567
  }
18306
18568
  }
18307
18569
  function writeConfig(cfg) {
18308
- const dir = dirname2(CONFIG_PATH);
18570
+ const configPath = getConfigPath();
18571
+ const dir = dirname2(configPath);
18309
18572
  if (!existsSync3(dir)) mkdirSync2(dir, { recursive: true });
18310
- writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2) + "\n");
18573
+ const tempPath = `${configPath}.${process.pid}.tmp`;
18574
+ writeFileSync(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
18575
+ renameSync(tempPath, configPath);
18311
18576
  }
18312
18577
  function renderLayout(title, activeTab, body) {
18313
18578
  return `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>${title} - SuperTask</title>${SHARED_STYLES}
@@ -18346,11 +18611,11 @@ async function saveConfig(){
18346
18611
  scheduler:{
18347
18612
  enabled:form.se.checked,
18348
18613
  checkIntervalMs:Number(form.si.value),
18349
- catchUp:form.cu.value,
18350
18614
  },
18351
18615
  watchdog:{
18352
18616
  heartbeatTimeoutMs:Number(form.wt.value)*1000,
18353
- cleanupIntervalMs:Number(form.wc.value)*1000,
18617
+ checkIntervalMs:Number(form.wci.value)*1000,
18618
+ cleanupIntervalMs:Number(form.wcl.value)*3600000,
18354
18619
  retentionDays:Number(form.rd.value),
18355
18620
  }
18356
18621
  };
@@ -18381,7 +18646,7 @@ async function saveConfig(){
18381
18646
  <dialog id="dd"><div class="dh"><h3 style="margin:0">\u8BE6\u60C5</h3><button class="cb" onclick="document.getElementById('dd').close()">&times;</button></div><div class="db"><pre id="dc"></pre></div></dialog>
18382
18647
  </body></html>`;
18383
18648
  }
18384
- var app, SHARED_STYLES, dashboardApp, web_default;
18649
+ var app, TASK_STATUSES, SHARED_STYLES, dashboardApp, web_default;
18385
18650
  var init_web = __esm({
18386
18651
  "src/web/index.tsx"() {
18387
18652
  "use strict";
@@ -18393,7 +18658,47 @@ var init_web = __esm({
18393
18658
  init_drizzle_orm();
18394
18659
  init_db2();
18395
18660
  init_config();
18661
+ init_health();
18396
18662
  app = new Hono2();
18663
+ TASK_STATUSES = /* @__PURE__ */ new Set([
18664
+ "pending",
18665
+ "running",
18666
+ "done",
18667
+ "failed",
18668
+ "dead_letter",
18669
+ "cancelled"
18670
+ ]);
18671
+ app.use("*", async (c, next) => {
18672
+ await next();
18673
+ c.header("X-Content-Type-Options", "nosniff");
18674
+ c.header("X-Frame-Options", "DENY");
18675
+ c.header("Referrer-Policy", "no-referrer");
18676
+ });
18677
+ app.use("/api/*", async (c, next) => {
18678
+ if (["GET", "HEAD", "OPTIONS"].includes(c.req.method)) return next();
18679
+ const fetchSite = c.req.header("Sec-Fetch-Site");
18680
+ if (fetchSite && !["same-origin", "none"].includes(fetchSite)) {
18681
+ return c.json({ error: "cross-site request rejected" }, 403);
18682
+ }
18683
+ const origin = c.req.header("Origin");
18684
+ if (origin) {
18685
+ try {
18686
+ const originUrl = new URL(origin);
18687
+ const requestUrl = new URL(c.req.url);
18688
+ const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(originUrl.hostname);
18689
+ if (!loopback || originUrl.origin !== requestUrl.origin) {
18690
+ return c.json({ error: "cross-site request rejected" }, 403);
18691
+ }
18692
+ } catch {
18693
+ return c.json({ error: "invalid origin" }, 403);
18694
+ }
18695
+ }
18696
+ return next();
18697
+ });
18698
+ app.get("/health", (c) => {
18699
+ const health = getGatewayHealth();
18700
+ return c.json(health, health.status === "ok" ? 200 : 503);
18701
+ });
18397
18702
  SHARED_STYLES = html`
18398
18703
  <style>
18399
18704
  :root { --bg:#0d1117; --card:#161b22; --border:#30363d; --t1:#c9d1d9; --t2:#8b949e; --green:#238636; --red:#da3633; --yellow:#d29922; --blue:#1f6feb; --purple:#8957e5; }
@@ -18470,12 +18775,16 @@ var init_web = __esm({
18470
18775
  </style>
18471
18776
  `;
18472
18777
  app.get("/", async (c) => {
18473
- const page = Number(c.req.query("page") || "1");
18778
+ const pageParam = c.req.query("page") || "1";
18779
+ const page = parsePositiveInteger(pageParam);
18780
+ if (page === null) return c.text("invalid page", 400);
18474
18781
  const statusFilter = c.req.query("status") || "";
18782
+ const parsedStatus = statusFilter ? parseTaskStatus(statusFilter) : null;
18783
+ if (statusFilter && !parsedStatus) return c.text("invalid status", 400);
18475
18784
  const limit = 50;
18476
18785
  const offset = (page - 1) * limit;
18477
18786
  const [tasks3, statsData] = await Promise.all([
18478
- TaskService.list({ limit, offset, ...statusFilter ? { status: statusFilter } : {} }),
18787
+ TaskService.list({ limit, offset, ...parsedStatus ? { status: parsedStatus } : {} }),
18479
18788
  TaskService.stats({})
18480
18789
  ]);
18481
18790
  const taskIds = tasks3.map((t) => t.id);
@@ -18498,12 +18807,13 @@ var init_web = __esm({
18498
18807
  </div>`;
18499
18808
  let rows = "";
18500
18809
  for (const task of tasks3) {
18501
- const st = (task.status ?? "").toUpperCase();
18810
+ const status = safeStatus(task.status);
18811
+ const st = status.toUpperCase();
18502
18812
  rows += `<tr>
18503
18813
  <td class="mu">#${task.id}</td>
18504
18814
  <td><div style="font-weight:500">${esc(task.name)}</div><div class="mu sm el">${esc(task.prompt.substring(0, 120))}</div></td>
18505
18815
  <td><span class="tag">${esc(task.agent)}</span></td>
18506
- <td><span class="badge b-${task.status}">${st}</span></td>
18816
+ <td><span class="badge b-${status}">${st}</span></td>
18507
18817
  <td class="sm ${task.status === "running" ? "" : "mu"}">${formatDuration(task.startedAt, task.finishedAt)}</td>
18508
18818
  <td class="mu sm">${(task.retryCount ?? 0) > 0 ? task.retryCount : "-"}</td>
18509
18819
  <td>
@@ -18535,21 +18845,13 @@ var init_web = __esm({
18535
18845
  });
18536
18846
  app.get("/templates", async (c) => {
18537
18847
  const templates = await TaskTemplateService.list(100);
18538
- for (const tmpl of templates) {
18539
- if (tmpl.enabled && tmpl.scheduleType === "cron" && tmpl.cronExpr) {
18540
- try {
18541
- const { getNextCronRun: getNextCronRun2 } = await Promise.resolve().then(() => (init_cron_parser(), cron_parser_exports));
18542
- tmpl.nextRunAt = getNextCronRun2(tmpl.cronExpr, Date.now());
18543
- } catch {
18544
- }
18545
- }
18546
- }
18547
18848
  const enabled = templates.filter((t) => t.enabled).length;
18548
18849
  const disabled = templates.length - enabled;
18549
18850
  let rows = "";
18550
18851
  for (const t of templates) {
18551
- const typeLabel = t.scheduleType === "cron" ? "Cron" : t.scheduleType === "recurring" ? "\u5FAA\u73AF" : "\u5B9A\u65F6";
18552
- const typeClass = "tag t-" + t.scheduleType;
18852
+ const scheduleType = ["cron", "recurring", "delayed"].includes(t.scheduleType) ? t.scheduleType : "unknown";
18853
+ const typeLabel = scheduleType === "cron" ? "Cron" : scheduleType === "recurring" ? "\u5FAA\u73AF" : scheduleType === "delayed" ? "\u5B9A\u65F6" : "\u672A\u77E5";
18854
+ const typeClass = "tag t-" + scheduleType;
18553
18855
  let rule = "-";
18554
18856
  if (t.scheduleType === "cron") rule = t.cronExpr || "-";
18555
18857
  else if (t.scheduleType === "recurring") rule = t.intervalMs ? `${Math.floor(t.intervalMs / 6e4)}\u5206\u949F` : "-";
@@ -18561,7 +18863,7 @@ var init_web = __esm({
18561
18863
  <td><div style="font-weight:500">${esc(t.name)}</div><div class="mu sm el">${esc(t.prompt.substring(0, 100))}</div>
18562
18864
  <div class="sm" style="margin-top:2px"><span class="tag">${esc(t.agent)}</span>${t.model && t.model !== "default" ? ` <span class="tag">${esc(t.model)}</span>` : ""}</div></td>
18563
18865
  <td><span class="${typeClass}">${typeLabel}</span></td>
18564
- <td class="m sm">${rule}</td>
18866
+ <td class="m sm">${esc(rule)}</td>
18565
18867
  <td>${statusBadge}</td>
18566
18868
  <td class="sm">${t.lastRunAt ? timeAgo(t.lastRunAt) : "-"}</td>
18567
18869
  <td class="sm">${t.nextRunAt ? timeUntil(t.nextRunAt) : "-"}</td>
@@ -18589,7 +18891,8 @@ var init_web = __esm({
18589
18891
  return c.html(renderLayout("\u5B9A\u65F6\u4EFB\u52A1", "templates", body));
18590
18892
  });
18591
18893
  app.get("/runs", async (c) => {
18592
- const page = Number(c.req.query("page") || "1");
18894
+ const page = parsePositiveInteger(c.req.query("page") || "1");
18895
+ if (page === null) return c.text("invalid page", 400);
18593
18896
  const limit = 50;
18594
18897
  const offset = (page - 1) * limit;
18595
18898
  const { taskRuns: tr, tasks: tk } = schema_exports;
@@ -18607,13 +18910,14 @@ var init_web = __esm({
18607
18910
  childPid: tr.childPid,
18608
18911
  taskName: tk.name,
18609
18912
  taskAgent: tk.agent
18610
- }).from(tr).innerJoin(tk, eq(tr.taskId, tk.id)).orderBy(desc(tr.startedAt)).limit(limit).offset(offset);
18913
+ }).from(tr).innerJoin(tk, eq(tr.taskId, tk.id)).orderBy(desc(tr.startedAt), desc(tr.id)).limit(limit).offset(offset);
18611
18914
  const totalResult = await db.select({ count: sql`count(*)` }).from(tr);
18612
18915
  const total = Number(totalResult[0]?.count ?? 0);
18613
18916
  const totalPages = Math.ceil(total / limit);
18614
18917
  let rows = "";
18615
18918
  const logsHtml = [];
18616
18919
  for (const run of runs) {
18920
+ const status = safeStatus(run.status);
18617
18921
  const shortSession = run.sessionId ? run.sessionId.slice(4, 7) + "***" + run.sessionId.slice(-3) : "-";
18618
18922
  const logBtn = run.log ? `<button class="btn btn-sm" onclick="toggleLog(${run.id})">\u65E5\u5FD7</button>` : "";
18619
18923
  rows += `<tr>
@@ -18621,15 +18925,15 @@ var init_web = __esm({
18621
18925
  <td><div style="font-weight:500">${esc(run.taskName)} <span class="mu">(#${run.taskId})</span></div>
18622
18926
  ${run.model ? `<div class="sm"><span class="tag">${esc(run.model)}</span></div>` : ""}</td>
18623
18927
  <td><span class="tag">${esc(run.taskAgent)}</span></td>
18624
- <td><span class="badge b-${run.status}">${(run.status ?? "").toUpperCase()}</span></td>
18928
+ <td><span class="badge b-${status}">${status.toUpperCase()}</span></td>
18625
18929
  <td class="sm">${formatDuration(run.startedAt, run.finishedAt)}</td>
18626
18930
  <td class="sm mu">${run.heartbeatAt ? timeAgo(run.heartbeatAt) : "-"}</td>
18627
18931
  <td><button class="btn btn-sm" onclick="showRunDetail(${run.id})">\u8BE6\u60C5</button>${logBtn}</td>
18628
18932
  </tr>`;
18629
18933
  if (run.log) {
18630
18934
  logsHtml.push(`<div id="log-${run.id}" style="display:none" class="mt8">
18631
- <div class="panel"><div class="ph"><h3>Run #${run.id} \u65E5\u5FD7 \u2014 ${run.taskName}</h3></div>
18632
- <div class="log-box">${run.log.replace(/</g, "&lt;")}</div></div></div>`);
18935
+ <div class="panel"><div class="ph"><h3>Run #${run.id} \u65E5\u5FD7 \u2014 ${esc(run.taskName)}</h3></div>
18936
+ <div class="log-box">${esc(run.log)}</div></div></div>`);
18633
18937
  }
18634
18938
  }
18635
18939
  let paging = `<div class="pn">`;
@@ -18655,17 +18959,18 @@ var init_web = __esm({
18655
18959
  });
18656
18960
  app.get("/system", async (c) => {
18657
18961
  const config = loadConfig();
18962
+ const configPath = getConfigPath();
18658
18963
  const stats = await TaskService.stats({});
18659
18964
  const runningRuns = await TaskRunService.getAllRunningRuns();
18660
18965
  const templates = await TaskTemplateService.list(100);
18661
- const configExists = existsSync3(CONFIG_PATH);
18966
+ const configExists = existsSync3(configPath);
18662
18967
  let runRows = "";
18663
18968
  if (runningRuns.length > 0) {
18664
18969
  for (const run of runningRuns) {
18665
18970
  const shortS = run.sessionId ? run.sessionId.slice(4, 7) + "***" + run.sessionId.slice(-3) : "-";
18666
18971
  runRows += `<tr>
18667
18972
  <td class="mu">#${run.id}</td><td>#${run.taskId}</td>
18668
- <td class="m sm">${shortS}</td>
18973
+ <td class="m sm">${esc(shortS)}</td>
18669
18974
  <td class="sm">${esc(run.model) || "-"}</td>
18670
18975
  <td class="sm">${formatDate(run.startedAt)}</td>
18671
18976
  <td class="sm">${run.heartbeatAt ? timeAgo(run.heartbeatAt) : "-"}</td>
@@ -18690,19 +18995,14 @@ var init_web = __esm({
18690
18995
  <h3 style="margin:0 0 12px;font-size:14px">Scheduler \u914D\u7F6E</h3>
18691
18996
  <div class="form-row"><label>\u542F\u7528\u8C03\u5EA6</label><input type="checkbox" name="se" ${config.scheduler.enabled ? "checked" : ""}></div>
18692
18997
  <div class="form-row"><label>\u68C0\u67E5\u95F4\u9694(ms)</label><input type="number" name="si" value="${config.scheduler.checkIntervalMs}" min="100" style="width:100px"></div>
18693
- <div class="form-row"><label>\u8FFD\u8D76\u6A21\u5F0F</label><select name="cu" style="width:100px">
18694
- <option value="next" ${config.scheduler.catchUp === "next" ? "selected" : ""}>next</option>
18695
- <option value="all" ${config.scheduler.catchUp === "all" ? "selected" : ""}>all</option>
18696
- <option value="latest" ${config.scheduler.catchUp === "latest" ? "selected" : ""}>latest</option>
18697
- </select></div>
18698
18998
  <div class="ir"><span class="ik">\u6D3B\u8DC3\u6A21\u677F</span><span class="iv">${templates.filter((t) => t.enabled).length} / ${templates.length}</span></div>
18699
18999
  </div>
18700
19000
  <div class="card">
18701
19001
  <h3 style="margin:0 0 12px;font-size:14px">Watchdog \u914D\u7F6E</h3>
18702
19002
  <div class="form-row"><label>\u5FC3\u8DF3\u8D85\u65F6(\u79D2)</label><input type="number" name="wt" value="${config.watchdog.heartbeatTimeoutMs / 1e3}" min="10" style="width:100px"></div>
18703
- <div class="form-row"><label>\u6E05\u7406\u95F4\u9694(\u79D2)</label><input type="number" name="wc" value="${config.watchdog.cleanupIntervalMs / 1e3}" min="10" style="width:100px"></div>
19003
+ <div class="form-row"><label>\u68C0\u67E5\u95F4\u9694(\u79D2)</label><input type="number" name="wci" value="${config.watchdog.checkIntervalMs / 1e3}" min="1" style="width:100px"></div>
19004
+ <div class="form-row"><label>\u6E05\u7406\u95F4\u9694(\u5C0F\u65F6)</label><input type="number" name="wcl" value="${config.watchdog.cleanupIntervalMs / 36e5}" min="1" style="width:100px"></div>
18704
19005
  <div class="form-row"><label>\u6570\u636E\u4FDD\u7559(\u5929)</label><input type="number" name="rd" value="${config.watchdog.retentionDays}" min="1" style="width:100px"></div>
18705
- <div class="ir"><span class="ik">\u65E5\u5FD7\u683C\u5F0F</span><span class="iv">${config.logging.format}</span></div>
18706
19006
  </div>
18707
19007
  </div>
18708
19008
  <div style="text-align:center;margin-bottom:24px">
@@ -18731,7 +19031,7 @@ var init_web = __esm({
18731
19031
 
18732
19032
  <div class="card mt16">
18733
19033
  <h3 style="margin:0 0 12px;font-size:14px">\u914D\u7F6E\u6587\u4EF6</h3>
18734
- <div class="ir"><span class="ik">\u8DEF\u5F84</span><span class="iv m sm">${CONFIG_PATH}</span></div>
19034
+ <div class="ir"><span class="ik">\u8DEF\u5F84</span><span class="iv m sm">${esc(configPath)}</span></div>
18735
19035
  <div class="ir"><span class="ik">\u6587\u4EF6\u5B58\u5728</span><span class="iv">${configFileStatus}</span></div>
18736
19036
  </div>
18737
19037
 
@@ -18743,46 +19043,61 @@ var init_web = __esm({
18743
19043
  return c.html(renderLayout("\u7CFB\u7EDF\u72B6\u6001", "system", body));
18744
19044
  });
18745
19045
  app.get("/api/tasks/:id", async (c) => {
18746
- const id = Number(c.req.param("id"));
19046
+ const id = parsePositiveInteger(c.req.param("id"));
19047
+ if (id === null) return c.json({ error: "invalid id" }, 400);
18747
19048
  const task = await TaskService.getById(id);
18748
19049
  if (!task) return c.json({ error: "not found" }, 404);
18749
19050
  const runs = await TaskRunService.listByTaskId(id);
18750
19051
  return c.json({ ...task, _runs: runs });
18751
19052
  });
18752
19053
  app.get("/api/runs/:id", async (c) => {
18753
- const id = Number(c.req.param("id"));
19054
+ const id = parsePositiveInteger(c.req.param("id"));
19055
+ if (id === null) return c.json({ error: "invalid id" }, 400);
18754
19056
  const run = await TaskRunService.getById(id);
18755
19057
  if (!run) return c.json({ error: "not found" }, 404);
18756
19058
  return c.json(run);
18757
19059
  });
18758
19060
  app.get("/api/templates/:id", async (c) => {
18759
- const id = Number(c.req.param("id"));
19061
+ const id = parsePositiveInteger(c.req.param("id"));
19062
+ if (id === null) return c.json({ error: "invalid id" }, 400);
18760
19063
  const tmpl = await TaskTemplateService.getById(id);
18761
19064
  if (!tmpl) return c.json({ error: "not found" }, 404);
18762
19065
  return c.json(tmpl);
18763
19066
  });
18764
19067
  app.post("/api/tasks/:id/retry", async (c) => {
18765
- await TaskService.retry(Number(c.req.param("id")));
18766
- return c.json({ success: true });
19068
+ const id = parsePositiveInteger(c.req.param("id"));
19069
+ if (id === null) return c.json({ error: "invalid id" }, 400);
19070
+ const task = await TaskService.retry(id);
19071
+ if (task) return c.json({ success: true });
19072
+ return await TaskService.getById(id) ? c.json({ error: "task status does not allow retry" }, 409) : c.json({ error: "not found" }, 404);
18767
19073
  });
18768
19074
  app.delete("/api/tasks/:id", async (c) => {
18769
- await TaskService.delete(Number(c.req.param("id")));
18770
- return c.json({ success: true });
19075
+ const id = parsePositiveInteger(c.req.param("id"));
19076
+ if (id === null) return c.json({ error: "invalid id" }, 400);
19077
+ const deleted = await TaskService.delete(id);
19078
+ return deleted ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
18771
19079
  });
18772
19080
  app.post("/api/templates/:id/enable", async (c) => {
18773
- const result = await TaskTemplateService.enable(Number(c.req.param("id")));
18774
- return c.json({ success: !!result });
19081
+ const id = parsePositiveInteger(c.req.param("id"));
19082
+ if (id === null) return c.json({ error: "invalid id" }, 400);
19083
+ const result = await TaskTemplateService.enable(id);
19084
+ return result ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
18775
19085
  });
18776
19086
  app.post("/api/templates/:id/disable", async (c) => {
18777
- const result = await TaskTemplateService.disable(Number(c.req.param("id")));
18778
- return c.json({ success: !!result });
19087
+ const id = parsePositiveInteger(c.req.param("id"));
19088
+ if (id === null) return c.json({ error: "invalid id" }, 400);
19089
+ const result = await TaskTemplateService.disable(id);
19090
+ return result ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
18779
19091
  });
18780
19092
  app.delete("/api/templates/:id", async (c) => {
18781
- const ok = await TaskTemplateService.delete(Number(c.req.param("id")));
18782
- return c.json({ success: ok });
19093
+ const id = parsePositiveInteger(c.req.param("id"));
19094
+ if (id === null) return c.json({ error: "invalid id" }, 400);
19095
+ const ok = await TaskTemplateService.delete(id);
19096
+ return ok ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
18783
19097
  });
18784
19098
  app.post("/api/templates/:id/trigger", async (c) => {
18785
- const id = Number(c.req.param("id"));
19099
+ const id = parsePositiveInteger(c.req.param("id"));
19100
+ if (id === null) return c.json({ error: "invalid id" }, 400);
18786
19101
  const tmpl = await TaskTemplateService.getById(id);
18787
19102
  if (!tmpl) return c.json({ error: "not found" }, 404);
18788
19103
  const task = await TaskService.add({
@@ -18794,7 +19109,10 @@ var init_web = __esm({
18794
19109
  category: tmpl.category,
18795
19110
  importance: tmpl.importance,
18796
19111
  urgency: tmpl.urgency,
19112
+ batchId: tmpl.batchId,
18797
19113
  maxRetries: tmpl.maxRetries,
19114
+ retryBackoffMs: tmpl.retryBackoffMs,
19115
+ timeoutMs: tmpl.timeoutMs,
18798
19116
  templateId: tmpl.id
18799
19117
  });
18800
19118
  return c.json({ success: true, taskId: task.id });
@@ -18809,22 +19127,29 @@ var init_web = __esm({
18809
19127
  const bW = body.worker ?? {};
18810
19128
  const bS = body.scheduler ?? {};
18811
19129
  const bD = body.watchdog ?? {};
18812
- const merged = { ...current, ...body, worker: { ...curW, ...bW }, scheduler: { ...curS, ...bS }, watchdog: { ...curD, ...bD } };
18813
- writeConfig(merged);
19130
+ const merged = {
19131
+ ...current,
19132
+ ...body,
19133
+ configVersion: 2,
19134
+ worker: { ...curW, ...bW },
19135
+ scheduler: { ...curS, ...bS },
19136
+ watchdog: { ...curD, ...bD }
19137
+ };
19138
+ writeConfig(validateConfig(merged));
18814
19139
  return c.json({ success: true });
18815
19140
  } catch (err) {
18816
- return c.json({ success: false, error: err instanceof Error ? err.message : String(err) });
19141
+ return c.json({ success: false, error: err instanceof Error ? err.message : String(err) }, 400);
18817
19142
  }
18818
19143
  });
18819
19144
  app.post("/api/database/clear", async (c) => {
18820
19145
  try {
18821
- const { tasks: tasks3, taskRuns: taskRuns3, taskTemplates: taskTemplates4 } = schema_exports;
18822
- await db.delete(taskRuns3);
19146
+ const { tasks: tasks3, taskRuns: taskRuns4, taskTemplates: taskTemplates4 } = schema_exports;
19147
+ await db.delete(taskRuns4);
18823
19148
  await db.delete(taskTemplates4);
18824
19149
  await db.delete(tasks3);
18825
19150
  return c.json({ success: true });
18826
19151
  } catch (err) {
18827
- return c.json({ success: false, error: err instanceof Error ? err.message : String(err) });
19152
+ return c.json({ success: false, error: err instanceof Error ? err.message : String(err) }, 500);
18828
19153
  }
18829
19154
  });
18830
19155
  dashboardApp = app;
@@ -18842,39 +19167,118 @@ init_config();
18842
19167
  // src/worker/index.ts
18843
19168
  init_task_service();
18844
19169
  init_task_run_service();
19170
+ init_health();
18845
19171
  import { spawn } from "child_process";
19172
+
19173
+ // src/core/process-control.ts
19174
+ import { spawnSync } from "child_process";
19175
+ import { basename } from "path";
19176
+ function isSafePid(pid) {
19177
+ return Number.isInteger(pid) && pid > 1 && pid !== process.pid;
19178
+ }
19179
+ function inspectUnixProcess(pid) {
19180
+ const result = spawnSync("ps", ["-o", "pgid=", "-o", "command=", "-p", String(pid)], {
19181
+ encoding: "utf8",
19182
+ stdio: ["ignore", "pipe", "ignore"]
19183
+ });
19184
+ if (result.status !== 0) return null;
19185
+ const match2 = result.stdout.trim().match(/^(\d+)\s+(.+)$/s);
19186
+ if (!match2) return null;
19187
+ return { processGroupId: Number(match2[1]), command: match2[2] };
19188
+ }
19189
+ function commandMatches(command, expectedExecutable) {
19190
+ const expectedName = basename(expectedExecutable).trim().toLowerCase();
19191
+ if (!expectedName) return false;
19192
+ return command.toLowerCase().includes(expectedName);
19193
+ }
19194
+ function signalSpawnedProcessTree(pid, signal) {
19195
+ if (!isSafePid(pid)) return false;
19196
+ if (process.platform !== "win32") {
19197
+ try {
19198
+ process.kill(-pid, signal);
19199
+ return true;
19200
+ } catch {
19201
+ }
19202
+ }
19203
+ try {
19204
+ process.kill(pid, signal);
19205
+ return true;
19206
+ } catch {
19207
+ return false;
19208
+ }
19209
+ }
19210
+ function signalRecordedProcessTree(pid, signal, expectedExecutable) {
19211
+ if (!isSafePid(pid)) return false;
19212
+ if (process.platform === "win32") {
19213
+ const list = spawnSync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
19214
+ encoding: "utf8",
19215
+ stdio: ["ignore", "pipe", "ignore"]
19216
+ });
19217
+ if (list.status !== 0 || !commandMatches(list.stdout, expectedExecutable)) return false;
19218
+ const args = ["/PID", String(pid), "/T"];
19219
+ if (signal === "SIGKILL") args.push("/F");
19220
+ return spawnSync("taskkill", args, { stdio: "ignore" }).status === 0;
19221
+ }
19222
+ const info = inspectUnixProcess(pid);
19223
+ if (!info || !commandMatches(info.command, expectedExecutable)) return false;
19224
+ try {
19225
+ if (info.processGroupId === pid) process.kill(-pid, signal);
19226
+ else process.kill(pid, signal);
19227
+ return true;
19228
+ } catch {
19229
+ return false;
19230
+ }
19231
+ }
19232
+
19233
+ // src/worker/index.ts
19234
+ var DEFAULT_MAX_OUTPUT_CHARS = 64 * 1024;
19235
+ var FORBIDDEN_AGENT = "supertask-runner";
18846
19236
  var WorkerEngine = class {
18847
19237
  activeBatchIds = /* @__PURE__ */ new Set();
18848
19238
  runningTasks = /* @__PURE__ */ new Map();
18849
19239
  stopped = false;
18850
19240
  pollTimer = null;
18851
19241
  heartbeatTimer = null;
19242
+ pollCyclePromise = null;
18852
19243
  cfg;
18853
- constructor(cfg) {
19244
+ opencodeBin;
19245
+ maxOutputChars;
19246
+ constructor(cfg, options = {}) {
18854
19247
  this.cfg = cfg.worker;
19248
+ this.opencodeBin = options.opencodeBin ?? process.env.SUPERTASK_OPENCODE_BIN ?? "opencode";
19249
+ this.maxOutputChars = options.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
18855
19250
  }
18856
19251
  start() {
18857
19252
  this.stopped = false;
19253
+ markGatewayActivity("worker");
18858
19254
  this.poll();
18859
19255
  this.heartbeatTimer = setInterval(() => this.updateHeartbeats(), this.cfg.heartbeatIntervalMs);
18860
19256
  }
18861
- stop() {
19257
+ async stop(gracePeriodMs = 0) {
18862
19258
  this.stopped = true;
18863
19259
  if (this.pollTimer) {
18864
19260
  clearTimeout(this.pollTimer);
18865
19261
  this.pollTimer = null;
18866
19262
  }
19263
+ if (this.pollCyclePromise) await this.pollCyclePromise;
19264
+ if (gracePeriodMs > 0 && this.runningTasks.size > 0) {
19265
+ const deadline = Date.now() + gracePeriodMs;
19266
+ while (this.runningTasks.size > 0 && Date.now() < deadline) {
19267
+ await Bun.sleep(Math.min(50, deadline - Date.now()));
19268
+ }
19269
+ }
18867
19270
  if (this.heartbeatTimer) {
18868
19271
  clearInterval(this.heartbeatTimer);
18869
19272
  this.heartbeatTimer = null;
18870
19273
  }
19274
+ const interruptedTaskIds = [...this.runningTasks.keys()];
18871
19275
  const killPromises = [];
18872
- for (const [, entry] of this.runningTasks) {
19276
+ for (const entry of this.runningTasks.values()) {
18873
19277
  entry.shutdown = true;
18874
19278
  killPromises.push(this.killEntry(entry));
18875
19279
  }
18876
- return Promise.allSettled(killPromises).then(() => {
18877
- });
19280
+ await Promise.allSettled(killPromises);
19281
+ return interruptedTaskIds;
18878
19282
  }
18879
19283
  getRunningTaskIds() {
18880
19284
  return [...this.runningTasks.keys()];
@@ -18884,136 +19288,244 @@ var WorkerEngine = class {
18884
19288
  }
18885
19289
  poll() {
18886
19290
  if (this.stopped) return;
18887
- this.tryDispatch().then(() => {
19291
+ markGatewayActivity("worker");
19292
+ this.pollCyclePromise = this.tryDispatch().finally(() => {
19293
+ this.pollCyclePromise = null;
18888
19294
  if (this.stopped) return;
18889
19295
  this.pollTimer = setTimeout(() => this.poll(), this.cfg.pollIntervalMs);
18890
19296
  });
18891
19297
  }
18892
19298
  async tryDispatch() {
19299
+ await this.reconcileCancelledTasks();
18893
19300
  while (!this.stopped && this.runningTasks.size < this.cfg.maxConcurrency) {
19301
+ let task;
19302
+ try {
19303
+ task = await TaskService.next({ excludedBatchIds: [...this.activeBatchIds] });
19304
+ } catch (err) {
19305
+ this.logError("task claim failed", err);
19306
+ break;
19307
+ }
19308
+ if (!task) break;
19309
+ if (this.stopped) break;
19310
+ if (!await TaskService.start(task.id)) continue;
19311
+ if (task.batchId) this.activeBatchIds.add(task.batchId);
19312
+ if (this.stopped) {
19313
+ await TaskService.resetRunningToPending([task.id]);
19314
+ this.releaseBatch(task);
19315
+ break;
19316
+ }
18894
19317
  try {
18895
- const excludedBatchIds = [...this.activeBatchIds];
18896
- const task = await TaskService.next({ excludedBatchIds });
18897
- if (!task) break;
18898
- if (!await TaskService.start(task.id)) continue;
18899
- if (task.batchId) {
18900
- this.activeBatchIds.add(task.batchId);
18901
- }
18902
19318
  const run = await TaskRunService.create({
18903
19319
  taskId: task.id,
18904
19320
  model: this.resolveModel(task.model),
18905
19321
  status: "running"
18906
19322
  });
18907
- const modelToUse = this.resolveModel(task.model);
18908
- const args = ["run", "--agent", "supertask-runner", "--format", "json"];
18909
- if (modelToUse) {
18910
- args.push("-m", modelToUse);
18911
- }
18912
- args.push(`\u6267\u884C\u4EFB\u52A1 ID: ${task.id}${modelToUse ? ` OVERRIDE_MODEL=${modelToUse}` : ""}`);
18913
- const cwd = task.cwd || process.cwd();
18914
- const child = spawn("opencode", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
18915
- await TaskRunService.updatePid(run.id, process.pid, child.pid ?? 0);
18916
- let output = "";
18917
- const handleData = (data) => {
18918
- const text2 = data.toString();
18919
- output += text2;
18920
- process.stdout.write(text2);
18921
- const match2 = text2.match(/"sessionID"\s*:\s*"(ses_[^"]+)"/);
18922
- if (match2) {
18923
- TaskRunService.updateSessionId(run.id, match2[1]).then(() => {
18924
- console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: "sessionId captured", taskId: task.id, sessionId: match2[1] }));
18925
- });
18926
- }
18927
- };
18928
- child.stdout?.on("data", handleData);
18929
- child.stderr?.on("data", handleData);
18930
- const entry = { task, runId: run.id, child, startedAt: Date.now(), shutdown: false };
18931
- this.runningTasks.set(task.id, entry);
18932
- child.on("close", async (code) => {
18933
- this.runningTasks.delete(task.id);
18934
- if (task.batchId) this.activeBatchIds.delete(task.batchId);
18935
- if (entry.shutdown) return;
18936
- const currentRun = await TaskRunService.getById(run.id);
18937
- if (!currentRun || currentRun.status !== "running") return;
18938
- if (code === 0) {
18939
- await TaskRunService.done(run.id);
18940
- await TaskService.done(task.id);
18941
- console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: "task done", taskId: task.id }));
18942
- } else {
18943
- const lastOutput = output.slice(-2e3);
18944
- await TaskRunService.fail(run.id, lastOutput);
18945
- const currentStatus = await TaskService.getById(task.id);
18946
- if (currentStatus?.status === "running") {
18947
- await TaskService.fail(task.id, "Worker\u6267\u884C\u5F02\u5E38\uFF1AOpencode \u8FDB\u7A0B\u975E\u6B63\u5E38\u9000\u51FA");
18948
- }
18949
- console.error(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "error", msg: "task failed", taskId: task.id, code }));
18950
- }
18951
- });
18952
- child.on("error", async (err) => {
18953
- this.runningTasks.delete(task.id);
18954
- if (task.batchId) this.activeBatchIds.delete(task.batchId);
18955
- if (entry.shutdown) return;
18956
- const currentRun = await TaskRunService.getById(run.id);
18957
- if (!currentRun || currentRun.status !== "running") return;
18958
- await TaskRunService.fail(run.id, err.message);
18959
- const currentStatus = await TaskService.getById(task.id);
18960
- if (currentStatus?.status === "running") {
18961
- await TaskService.fail(task.id, `spawn \u5F02\u5E38: ${err.message}`);
18962
- }
18963
- console.error(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "error", msg: "task spawn error", taskId: task.id, error: err.message }));
19323
+ if (this.stopped) {
19324
+ await TaskRunService.fail(run.id, "Gateway shutdown before spawn");
19325
+ await TaskService.resetRunningToPending([task.id]);
19326
+ this.releaseBatch(task);
19327
+ break;
19328
+ }
19329
+ if (task.agent === FORBIDDEN_AGENT) {
19330
+ const message = `\u7981\u6B62\u6267\u884C\u9012\u5F52 Agent: ${FORBIDDEN_AGENT}`;
19331
+ await TaskRunService.fail(run.id, message);
19332
+ await TaskService.fail(task.id, message, {}, { setDeadLetter: true });
19333
+ this.releaseBatch(task);
19334
+ continue;
19335
+ }
19336
+ this.spawnTask(task, run.id);
19337
+ } catch (err) {
19338
+ this.releaseBatch(task);
19339
+ const message = `Worker \u542F\u52A8\u4EFB\u52A1\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`;
19340
+ try {
19341
+ await TaskService.fail(task.id, message);
19342
+ } catch (failErr) {
19343
+ this.logError("failed to compensate task startup", failErr, task.id);
19344
+ }
19345
+ this.logError("task dispatch failed", err, task.id);
19346
+ }
19347
+ }
19348
+ }
19349
+ spawnTask(task, runId) {
19350
+ const model = this.resolveModel(task.model);
19351
+ const args = ["run", "--agent", task.agent, "--format", "json"];
19352
+ if (model) args.push("-m", model);
19353
+ args.push(task.prompt);
19354
+ const child = spawn(this.opencodeBin, args, {
19355
+ cwd: task.cwd || process.cwd(),
19356
+ stdio: ["ignore", "pipe", "pipe"],
19357
+ detached: process.platform !== "win32"
19358
+ });
19359
+ const entry = {
19360
+ task,
19361
+ runId,
19362
+ child,
19363
+ output: "",
19364
+ sessionId: null,
19365
+ timeoutTimer: null,
19366
+ shutdown: false,
19367
+ settled: false
19368
+ };
19369
+ this.runningTasks.set(task.id, entry);
19370
+ const handleData = (data) => {
19371
+ const text2 = data.toString();
19372
+ entry.output = (entry.output + text2).slice(-this.maxOutputChars);
19373
+ process.stdout.write(text2);
19374
+ const match2 = entry.output.match(/"sessionID"\s*:\s*"(ses_[^"]+)"/);
19375
+ if (match2?.[1] && match2[1] !== entry.sessionId) {
19376
+ entry.sessionId = match2[1];
19377
+ TaskRunService.updateSessionId(runId, match2[1]).catch((err) => {
19378
+ this.logError("sessionId update failed", err, task.id);
18964
19379
  });
19380
+ }
19381
+ };
19382
+ child.stdout?.on("data", handleData);
19383
+ child.stderr?.on("data", handleData);
19384
+ child.once("error", (err) => {
19385
+ void this.finishEntry(entry, null, `\u65E0\u6CD5\u542F\u52A8 opencode\uFF1A${err.message}`);
19386
+ });
19387
+ child.once("close", (code, signal) => {
19388
+ const failure = code === 0 ? void 0 : `opencode \u9000\u51FA\u7801 ${code ?? "null"}${signal ? `\uFF0C\u4FE1\u53F7 ${signal}` : ""}`;
19389
+ void this.finishEntry(entry, code, failure);
19390
+ });
19391
+ const timeoutMs = task.timeoutMs ?? this.cfg.taskTimeoutMs;
19392
+ if (timeoutMs > 0) {
19393
+ entry.timeoutTimer = setTimeout(() => {
19394
+ this.signalEntry(entry, "SIGKILL");
19395
+ void this.finishEntry(entry, null, `\u4EFB\u52A1\u8D85\u65F6\uFF08${timeoutMs}ms\uFF09`);
19396
+ }, timeoutMs);
19397
+ }
19398
+ TaskRunService.updatePid(runId, process.pid, child.pid ?? 0).catch((err) => {
19399
+ this.signalEntry(entry, "SIGKILL");
19400
+ void this.finishEntry(entry, null, `\u8BB0\u5F55 Worker PID \u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`);
19401
+ });
19402
+ }
19403
+ async finishEntry(entry, code, failure) {
19404
+ if (entry.settled) return;
19405
+ entry.settled = true;
19406
+ if (entry.timeoutTimer) {
19407
+ clearTimeout(entry.timeoutTimer);
19408
+ entry.timeoutTimer = null;
19409
+ }
19410
+ try {
19411
+ if (entry.shutdown) return;
19412
+ const currentRun = await TaskRunService.getById(entry.runId);
19413
+ if (!currentRun || currentRun.status !== "running") return;
19414
+ const output = entry.output.trim();
19415
+ const log = failure ? `${failure}${output ? `
19416
+ ${output}` : ""}` : output;
19417
+ if (code === 0 && !failure) {
19418
+ const completed = await TaskService.done(entry.task.id, log);
19419
+ if (completed) {
19420
+ await TaskRunService.done(entry.runId, log);
19421
+ console.log(JSON.stringify({
19422
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
19423
+ level: "info",
19424
+ msg: "task done",
19425
+ taskId: entry.task.id
19426
+ }));
19427
+ return;
19428
+ }
19429
+ await TaskRunService.fail(entry.runId, "\u4EFB\u52A1\u72B6\u6001\u5DF2\u88AB\u5176\u4ED6\u64CD\u4F5C\u6539\u53D8");
19430
+ return;
19431
+ }
19432
+ await TaskRunService.fail(entry.runId, log);
19433
+ const failed = await TaskService.fail(entry.task.id, log);
19434
+ if (!failed) {
19435
+ this.logError("task failure state transition rejected", failure ?? "unknown failure", entry.task.id);
19436
+ }
19437
+ console.error(JSON.stringify({
19438
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
19439
+ level: "error",
19440
+ msg: "task failed",
19441
+ taskId: entry.task.id,
19442
+ error: failure
19443
+ }));
19444
+ } finally {
19445
+ this.runningTasks.delete(entry.task.id);
19446
+ this.releaseBatch(entry.task);
19447
+ }
19448
+ }
19449
+ async reconcileCancelledTasks() {
19450
+ for (const entry of [...this.runningTasks.values()]) {
19451
+ try {
19452
+ const task = await TaskService.getById(entry.task.id);
19453
+ if (task?.status === "cancelled") await this.cancelEntry(entry);
18965
19454
  } catch (err) {
18966
- console.error(JSON.stringify({
18967
- ts: (/* @__PURE__ */ new Date()).toISOString(),
18968
- level: "error",
18969
- msg: "tryDispatch iteration failed",
18970
- error: err instanceof Error ? err.message : String(err)
18971
- }));
18972
- break;
19455
+ this.logError("cancel reconciliation failed", err, entry.task.id);
18973
19456
  }
18974
19457
  }
18975
19458
  }
19459
+ async cancelEntry(entry) {
19460
+ if (entry.settled) return;
19461
+ entry.settled = true;
19462
+ if (entry.timeoutTimer) {
19463
+ clearTimeout(entry.timeoutTimer);
19464
+ entry.timeoutTimer = null;
19465
+ }
19466
+ try {
19467
+ await this.killEntry(entry);
19468
+ const output = entry.output.trim();
19469
+ const log = `\u4EFB\u52A1\u5DF2\u53D6\u6D88${output ? `
19470
+ ${output}` : ""}`;
19471
+ await TaskRunService.fail(entry.runId, log);
19472
+ console.log(JSON.stringify({
19473
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
19474
+ level: "info",
19475
+ msg: "running task cancelled",
19476
+ taskId: entry.task.id
19477
+ }));
19478
+ } finally {
19479
+ this.runningTasks.delete(entry.task.id);
19480
+ this.releaseBatch(entry.task);
19481
+ }
19482
+ }
18976
19483
  async updateHeartbeats() {
18977
- for (const [, entry] of this.runningTasks) {
19484
+ for (const entry of this.runningTasks.values()) {
18978
19485
  try {
18979
19486
  await TaskRunService.heartbeat(entry.runId);
18980
- } catch {
19487
+ } catch (err) {
19488
+ this.logError("heartbeat update failed", err, entry.task.id);
18981
19489
  }
18982
19490
  }
18983
19491
  }
18984
19492
  killEntry(entry) {
18985
- if (entry.child.exitCode !== null) {
19493
+ if (entry.child.exitCode !== null || entry.child.signalCode !== null) {
18986
19494
  return Promise.resolve();
18987
19495
  }
18988
19496
  return new Promise((resolve) => {
18989
19497
  const timeout = setTimeout(() => {
18990
- try {
18991
- if (entry.child.pid) process.kill(entry.child.pid, "SIGKILL");
18992
- } catch {
18993
- }
19498
+ this.signalEntry(entry, "SIGKILL");
18994
19499
  resolve();
18995
19500
  }, 5e3);
18996
- entry.child.on("close", () => {
19501
+ entry.child.once("close", () => {
18997
19502
  clearTimeout(timeout);
18998
19503
  resolve();
18999
19504
  });
19000
- try {
19001
- if (entry.child.pid) {
19002
- entry.child.kill("SIGTERM");
19003
- } else {
19004
- clearTimeout(timeout);
19005
- resolve();
19006
- }
19007
- } catch {
19008
- clearTimeout(timeout);
19009
- resolve();
19010
- }
19505
+ this.signalEntry(entry, "SIGTERM");
19011
19506
  });
19012
19507
  }
19508
+ signalEntry(entry, signal) {
19509
+ const pid = entry.child.pid;
19510
+ if (!pid) return;
19511
+ signalSpawnedProcessTree(pid, signal);
19512
+ }
19513
+ releaseBatch(task) {
19514
+ if (task.batchId) this.activeBatchIds.delete(task.batchId);
19515
+ }
19013
19516
  resolveModel(taskModel) {
19014
19517
  if (!taskModel || taskModel === "default") return null;
19015
19518
  return taskModel;
19016
19519
  }
19520
+ logError(message, error, taskId) {
19521
+ console.error(JSON.stringify({
19522
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
19523
+ level: "error",
19524
+ msg: message,
19525
+ taskId,
19526
+ error: error instanceof Error ? error.message : String(error)
19527
+ }));
19528
+ }
19017
19529
  };
19018
19530
 
19019
19531
  // src/gateway/watchdog/heartbeat.ts
@@ -19026,15 +19538,23 @@ async function checkHeartbeats(heartbeatTimeoutMs) {
19026
19538
  for (const run of staleRuns) {
19027
19539
  try {
19028
19540
  if (run.childPid != null && run.childPid > 0) {
19029
- try {
19030
- process.kill(run.childPid, "SIGKILL");
19031
- } catch {
19541
+ const expectedExecutable = process.env.SUPERTASK_OPENCODE_BIN ?? "opencode";
19542
+ const killed = signalRecordedProcessTree(run.childPid, "SIGKILL", expectedExecutable);
19543
+ if (!killed) {
19544
+ console.warn(JSON.stringify({
19545
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
19546
+ level: "warn",
19547
+ msg: "stale child process was not killed because identity validation failed",
19548
+ taskId: run.taskId,
19549
+ runId: run.runId,
19550
+ childPid: run.childPid
19551
+ }));
19032
19552
  }
19033
19553
  }
19034
19554
  await TaskRunService.fail(run.runId, `\u5FC3\u8DF3\u8D85\u65F6 (${heartbeatTimeoutMs / 1e3}s)\uFF0CWatchdog kill`);
19035
19555
  const newRetryCount = run.taskRetryCount + 1;
19036
19556
  const maxRetries = run.taskMaxRetries;
19037
- if (newRetryCount >= maxRetries) {
19557
+ if (newRetryCount > maxRetries) {
19038
19558
  await TaskService.markDeadLetter(run.taskId, newRetryCount);
19039
19559
  console.log(JSON.stringify({
19040
19560
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -19046,7 +19566,7 @@ async function checkHeartbeats(heartbeatTimeoutMs) {
19046
19566
  maxRetries
19047
19567
  }));
19048
19568
  } else {
19049
- const backoffMs = computeBackoff(newRetryCount);
19569
+ const backoffMs = computeBackoff(newRetryCount, run.taskRetryBackoffMs);
19050
19570
  const retryAfter = Date.now() + backoffMs;
19051
19571
  await TaskService.markPendingForRetry(run.taskId, retryAfter, newRetryCount);
19052
19572
  console.log(JSON.stringify({
@@ -19102,6 +19622,7 @@ async function cleanupOldRecords(retentionDays) {
19102
19622
  }
19103
19623
 
19104
19624
  // src/gateway/watchdog/index.ts
19625
+ init_health();
19105
19626
  var Watchdog = class {
19106
19627
  constructor(cfg) {
19107
19628
  this.cfg = cfg;
@@ -19112,13 +19633,14 @@ var Watchdog = class {
19112
19633
  cleanupTimer = null;
19113
19634
  start() {
19114
19635
  this.stopped = false;
19636
+ markGatewayActivity("watchdog");
19115
19637
  this.heartbeatTimer = setInterval(
19116
19638
  () => this.runHeartbeatCheck(),
19117
- this.cfg.watchdog.cleanupIntervalMs
19639
+ this.cfg.watchdog.checkIntervalMs
19118
19640
  );
19119
19641
  this.cleanupTimer = setInterval(
19120
19642
  () => this.runCleanup(),
19121
- this.cfg.watchdog.cleanupIntervalMs * 24 * 60
19643
+ this.cfg.watchdog.cleanupIntervalMs
19122
19644
  );
19123
19645
  }
19124
19646
  stop() {
@@ -19134,6 +19656,7 @@ var Watchdog = class {
19134
19656
  }
19135
19657
  async runHeartbeatCheck() {
19136
19658
  if (this.stopped) return;
19659
+ markGatewayActivity("watchdog");
19137
19660
  try {
19138
19661
  await checkHeartbeats(this.cfg.watchdog.heartbeatTimeoutMs);
19139
19662
  } catch (err) {
@@ -19169,7 +19692,7 @@ var { taskTemplates: taskTemplates3 } = schema_exports;
19169
19692
  async function cloneTaskFromTemplate(templateId) {
19170
19693
  const rows = await db.select().from(taskTemplates3).where(eq(taskTemplates3.id, templateId)).limit(1);
19171
19694
  const tmpl = rows[0];
19172
- if (!tmpl) return null;
19695
+ if (!tmpl || !tmpl.enabled) return null;
19173
19696
  const activeCount = await db.select({ count: sql`count(*)` }).from(schema_exports.tasks).where(
19174
19697
  and(
19175
19698
  eq(schema_exports.tasks.templateId, templateId),
@@ -19178,7 +19701,8 @@ async function cloneTaskFromTemplate(templateId) {
19178
19701
  ).then((r) => r[0].count);
19179
19702
  if (activeCount >= (tmpl.maxInstances ?? 1)) return null;
19180
19703
  const nowMs = Date.now();
19181
- const nextRunAt = TaskTemplateService.calculateNextRunAt(
19704
+ const isDelayed = tmpl.scheduleType === "delayed";
19705
+ const nextRunAt = isDelayed ? null : TaskTemplateService.calculateNextRunAt(
19182
19706
  tmpl.scheduleType,
19183
19707
  tmpl,
19184
19708
  nowMs
@@ -19192,13 +19716,17 @@ async function cloneTaskFromTemplate(templateId) {
19192
19716
  category: tmpl.category ?? "general",
19193
19717
  importance: tmpl.importance ?? 3,
19194
19718
  urgency: tmpl.urgency ?? 3,
19719
+ batchId: tmpl.batchId,
19195
19720
  maxRetries: tmpl.maxRetries ?? 3,
19721
+ retryBackoffMs: tmpl.retryBackoffMs ?? 3e4,
19722
+ timeoutMs: tmpl.timeoutMs,
19196
19723
  templateId: tmpl.id,
19197
19724
  scheduledAt: nowMs
19198
19725
  });
19199
19726
  await db.update(taskTemplates3).set({
19200
19727
  lastRunAt: nowMs,
19201
19728
  nextRunAt,
19729
+ enabled: isDelayed ? false : tmpl.enabled,
19202
19730
  updatedAt: nowMs
19203
19731
  }).where(eq(taskTemplates3.id, templateId));
19204
19732
  return task;
@@ -19211,7 +19739,7 @@ async function getDueTemplates() {
19211
19739
  sql`${taskTemplates3.nextRunAt} IS NOT NULL`,
19212
19740
  sql`${taskTemplates3.nextRunAt} <= ${nowMs}`
19213
19741
  )
19214
- );
19742
+ ).orderBy(asc(taskTemplates3.nextRunAt), asc(taskTemplates3.id));
19215
19743
  }
19216
19744
  async function initializeNextRunAt(templateId) {
19217
19745
  const rows = await db.select().from(taskTemplates3).where(eq(taskTemplates3.id, templateId)).limit(1);
@@ -19229,6 +19757,7 @@ async function initializeNextRunAt(templateId) {
19229
19757
  // src/gateway/scheduler/index.ts
19230
19758
  init_db2();
19231
19759
  init_drizzle_orm();
19760
+ init_health();
19232
19761
  var Scheduler = class {
19233
19762
  constructor(cfg) {
19234
19763
  this.cfg = cfg;
@@ -19240,6 +19769,7 @@ var Scheduler = class {
19240
19769
  async start() {
19241
19770
  if (!this.cfg.scheduler.enabled) return;
19242
19771
  this.stopped = false;
19772
+ markGatewayActivity("scheduler");
19243
19773
  await this.initializeTemplates();
19244
19774
  this.timer = setInterval(() => this.tick(), this.cfg.scheduler.checkIntervalMs);
19245
19775
  }
@@ -19252,6 +19782,7 @@ var Scheduler = class {
19252
19782
  }
19253
19783
  async tick() {
19254
19784
  if (this.stopped || this.ticking) return;
19785
+ markGatewayActivity("scheduler");
19255
19786
  this.ticking = true;
19256
19787
  try {
19257
19788
  const dueTemplates = await getDueTemplates();
@@ -19301,6 +19832,7 @@ var Scheduler = class {
19301
19832
  init_db2();
19302
19833
  init_task_service();
19303
19834
  init_task_run_service();
19835
+ init_health();
19304
19836
  var STALE_THRESHOLD_MS = 3e4;
19305
19837
  function acquireLock() {
19306
19838
  const now = Date.now();
@@ -19322,7 +19854,7 @@ function acquireLock() {
19322
19854
  sqlite.exec("DELETE FROM gateway_lock WHERE id = 1");
19323
19855
  }
19324
19856
  sqlite.exec(
19325
- "INSERT INTO gateway_lock (id, pid, acquired_at, heartbeat_at) VALUES (1, ?, ?, ?)",
19857
+ "INSERT INTO gateway_lock (id, pid, acquired_at, heartbeat_at, ready_at) VALUES (1, ?, ?, ?, NULL)",
19326
19858
  [pid, now, now]
19327
19859
  );
19328
19860
  sqlite.exec("COMMIT");
@@ -19356,6 +19888,18 @@ function updateLockHeartbeat() {
19356
19888
  } catch {
19357
19889
  }
19358
19890
  }
19891
+ function markGatewayReady() {
19892
+ sqlite.exec(
19893
+ "UPDATE gateway_lock SET heartbeat_at = ?, ready_at = ? WHERE pid = ?",
19894
+ [Date.now(), Date.now(), process.pid]
19895
+ );
19896
+ }
19897
+ function markGatewayNotReady() {
19898
+ try {
19899
+ sqlite.exec("UPDATE gateway_lock SET ready_at = NULL WHERE pid = ?", [process.pid]);
19900
+ } catch {
19901
+ }
19902
+ }
19359
19903
  async function main() {
19360
19904
  console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: "SuperTask Gateway starting", pid: process.pid }));
19361
19905
  if (!acquireLock()) {
@@ -19367,20 +19911,21 @@ async function main() {
19367
19911
  const worker = new WorkerEngine(cfg);
19368
19912
  const watchdog = new Watchdog(cfg);
19369
19913
  const scheduler = new Scheduler(cfg);
19914
+ initializeGatewayHealth({
19915
+ workerPollIntervalMs: cfg.worker.pollIntervalMs,
19916
+ schedulerEnabled: cfg.scheduler.enabled,
19917
+ schedulerCheckIntervalMs: cfg.scheduler.checkIntervalMs,
19918
+ watchdogCheckIntervalMs: cfg.watchdog.checkIntervalMs
19919
+ });
19370
19920
  worker.start();
19371
19921
  watchdog.start();
19372
19922
  await scheduler.start();
19373
19923
  if (cfg.dashboard.enabled) {
19374
19924
  const { dashboardApp: dashboardApp2 } = await Promise.resolve().then(() => (init_web(), web_exports));
19375
19925
  Bun.serve({
19926
+ hostname: "127.0.0.1",
19376
19927
  port: cfg.dashboard.port,
19377
- fetch(req) {
19378
- const url = new URL(req.url);
19379
- if (url.hostname !== "localhost" && url.hostname !== "127.0.0.1" && url.hostname !== "::1") {
19380
- return new Response("Forbidden", { status: 403 });
19381
- }
19382
- return dashboardApp2.fetch(req);
19383
- }
19928
+ fetch: dashboardApp2.fetch
19384
19929
  });
19385
19930
  console.log(JSON.stringify({
19386
19931
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -19389,6 +19934,7 @@ async function main() {
19389
19934
  url: `http://localhost:${cfg.dashboard.port}`
19390
19935
  }));
19391
19936
  }
19937
+ markGatewayReady();
19392
19938
  console.log(JSON.stringify({
19393
19939
  ts: (/* @__PURE__ */ new Date()).toISOString(),
19394
19940
  level: "info",
@@ -19402,10 +19948,10 @@ async function main() {
19402
19948
  shuttingDown = true;
19403
19949
  console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: `received ${signal}, shutting down...` }));
19404
19950
  clearInterval(heartbeatTimer);
19951
+ markGatewayNotReady();
19405
19952
  scheduler.stop();
19406
19953
  watchdog.stop();
19407
- const runningIds = worker.getRunningTaskIds();
19408
- await worker.stop();
19954
+ const runningIds = await worker.stop(cfg.worker.shutdownGracePeriodMs);
19409
19955
  if (runningIds.length > 0) {
19410
19956
  const resetCount = await TaskService.resetRunningToPending(runningIds);
19411
19957
  console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: "reset running tasks to pending", count: resetCount }));
@@ -19415,6 +19961,7 @@ async function main() {
19415
19961
  await TaskRunService.fail(run.id, "Gateway shutdown");
19416
19962
  }
19417
19963
  releaseLock();
19964
+ resetGatewayHealth();
19418
19965
  closeDb();
19419
19966
  console.log(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "info", msg: "Gateway stopped" }));
19420
19967
  process.exit(0);