opencode-supertask 0.1.19 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/web/index.js CHANGED
@@ -10,9 +10,6 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
10
10
  if (typeof require !== "undefined") return require.apply(this, arguments);
11
11
  throw Error('Dynamic require of "' + x + '" is not supported');
12
12
  });
13
- var __esm = (fn, res) => function __init() {
14
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
15
- };
16
13
  var __commonJS = (cb, mod) => function __require2() {
17
14
  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
18
15
  };
@@ -237,7 +234,7 @@ var require_CronField = __commonJS({
237
234
  if (!isValidRange) {
238
235
  throw new Error(`${this.constructor.name} Validation error, got value ${badValue} expected range ${this.min}-${this.max}${charsString}`);
239
236
  }
240
- const duplicate = this.#values.find((value, index) => this.#values.indexOf(value) !== index);
237
+ const duplicate = this.#values.find((value, index2) => this.#values.indexOf(value) !== index2);
241
238
  if (duplicate) {
242
239
  throw new Error(`${this.constructor.name} Validation error, duplicate values found: ${duplicate}`);
243
240
  }
@@ -8082,11 +8079,11 @@ var require_CronFieldCollection = __commonJS({
8082
8079
  throw new Error("Unexpected range end");
8083
8080
  }
8084
8081
  if (step * multiplier > range.end) {
8085
- const mapFn = (_, index) => {
8082
+ const mapFn = (_, index2) => {
8086
8083
  if (typeof range.start !== "number") {
8087
8084
  throw new Error("Unexpected range start");
8088
8085
  }
8089
- return index % step === 0 ? range.start + index : null;
8086
+ return index2 % step === 0 ? range.start + index2 : null;
8090
8087
  };
8091
8088
  if (typeof range.start !== "number") {
8092
8089
  throw new Error("Unexpected range start");
@@ -8978,9 +8975,9 @@ var require_CronExpressionParser = __commonJS({
8978
8975
  if (field === CronUnit.DayOfWeek && max % 7 === 0) {
8979
8976
  stack.push(0);
8980
8977
  }
8981
- for (let index = min; index <= max; index += repeatInterval) {
8982
- if (stack.indexOf(index) === -1) {
8983
- stack.push(index);
8978
+ for (let index2 = min; index2 <= max; index2 += repeatInterval) {
8979
+ if (stack.indexOf(index2) === -1) {
8980
+ stack.push(index2);
8984
8981
  }
8985
8982
  }
8986
8983
  return stack;
@@ -9202,48 +9199,16 @@ var require_dist = __commonJS({
9202
9199
  }
9203
9200
  });
9204
9201
 
9205
- // src/core/cron-parser.ts
9206
- var cron_parser_exports = {};
9207
- __export(cron_parser_exports, {
9208
- getNextCronRun: () => getNextCronRun,
9209
- isValidCronExpr: () => isValidCronExpr
9210
- });
9211
- function getNextCronRun(expr, afterMs) {
9212
- try {
9213
- const fromDate = afterMs != null ? new Date(afterMs) : /* @__PURE__ */ new Date();
9214
- const interval = import_cron_parser.CronExpressionParser.parse(expr, { currentDate: fromDate });
9215
- const next = interval.next();
9216
- return next.getTime();
9217
- } catch {
9218
- return null;
9219
- }
9220
- }
9221
- function isValidCronExpr(expr) {
9222
- try {
9223
- import_cron_parser.CronExpressionParser.parse(expr);
9224
- return true;
9225
- } catch {
9226
- return false;
9227
- }
9228
- }
9229
- var import_cron_parser;
9230
- var init_cron_parser = __esm({
9231
- "src/core/cron-parser.ts"() {
9232
- "use strict";
9233
- import_cron_parser = __toESM(require_dist(), 1);
9234
- }
9235
- });
9236
-
9237
9202
  // node_modules/hono/dist/compose.js
9238
9203
  var compose = (middleware, onError, onNotFound) => {
9239
9204
  return (context, next) => {
9240
- let index = -1;
9205
+ let index2 = -1;
9241
9206
  return dispatch(0);
9242
9207
  async function dispatch(i) {
9243
- if (i <= index) {
9208
+ if (i <= index2) {
9244
9209
  throw new Error("next() called multiple times");
9245
9210
  }
9246
- index = i;
9211
+ index2 = i;
9247
9212
  let res;
9248
9213
  let isError = false;
9249
9214
  let handler;
@@ -9338,8 +9303,8 @@ var handleParsingAllValues = (form, key, value) => {
9338
9303
  var handleParsingNestedValues = (form, key, value) => {
9339
9304
  let nestedForm = form;
9340
9305
  const keys = key.split(".");
9341
- keys.forEach((key2, index) => {
9342
- if (index === keys.length - 1) {
9306
+ keys.forEach((key2, index2) => {
9307
+ if (index2 === keys.length - 1) {
9343
9308
  nestedForm[key2] = value;
9344
9309
  } else {
9345
9310
  if (!nestedForm[key2] || typeof nestedForm[key2] !== "object" || Array.isArray(nestedForm[key2]) || nestedForm[key2] instanceof File) {
@@ -9365,8 +9330,8 @@ var splitRoutingPath = (routePath) => {
9365
9330
  };
9366
9331
  var extractGroupsFromPath = (path) => {
9367
9332
  const groups = [];
9368
- path = path.replace(/\{[^}]+\}/g, (match2, index) => {
9369
- const mark = `@${index}`;
9333
+ path = path.replace(/\{[^}]+\}/g, (match2, index2) => {
9334
+ const mark = `@${index2}`;
9370
9335
  groups.push([mark, match2]);
9371
9336
  return mark;
9372
9337
  });
@@ -9869,10 +9834,10 @@ var escapeToBuffer = (str, buffer) => {
9869
9834
  return;
9870
9835
  }
9871
9836
  let escape;
9872
- let index;
9837
+ let index2;
9873
9838
  let lastIndex = 0;
9874
- for (index = match2; index < str.length; index++) {
9875
- switch (str.charCodeAt(index)) {
9839
+ for (index2 = match2; index2 < str.length; index2++) {
9840
+ switch (str.charCodeAt(index2)) {
9876
9841
  case 34:
9877
9842
  escape = "&quot;";
9878
9843
  break;
@@ -9891,10 +9856,10 @@ var escapeToBuffer = (str, buffer) => {
9891
9856
  default:
9892
9857
  continue;
9893
9858
  }
9894
- buffer[0] += str.substring(lastIndex, index) + escape;
9895
- lastIndex = index + 1;
9859
+ buffer[0] += str.substring(lastIndex, index2) + escape;
9860
+ lastIndex = index2 + 1;
9896
9861
  }
9897
- buffer[0] += str.substring(lastIndex, index);
9862
+ buffer[0] += str.substring(lastIndex, index2);
9898
9863
  };
9899
9864
  var resolveCallbackSync = (str) => {
9900
9865
  const callbacks = str.callbacks;
@@ -10738,8 +10703,8 @@ function match(method, path) {
10738
10703
  if (!match3) {
10739
10704
  return [[], emptyParam];
10740
10705
  }
10741
- const index = match3.indexOf("", 1);
10742
- return [matcher[1][index], match3];
10706
+ const index2 = match3.indexOf("", 1);
10707
+ return [matcher[1][index2], match3];
10743
10708
  });
10744
10709
  this.match = match2;
10745
10710
  return match2(method, path);
@@ -10774,7 +10739,7 @@ var Node = class _Node {
10774
10739
  #index;
10775
10740
  #varIndex;
10776
10741
  #children = /* @__PURE__ */ Object.create(null);
10777
- insert(tokens, index, paramMap, context, pathErrorCheckOnly) {
10742
+ insert(tokens, index2, paramMap, context, pathErrorCheckOnly) {
10778
10743
  if (tokens.length === 0) {
10779
10744
  if (this.#index !== void 0) {
10780
10745
  throw PATH_ERROR;
@@ -10782,7 +10747,7 @@ var Node = class _Node {
10782
10747
  if (pathErrorCheckOnly) {
10783
10748
  return;
10784
10749
  }
10785
- this.#index = index;
10750
+ this.#index = index2;
10786
10751
  return;
10787
10752
  }
10788
10753
  const [token, ...restTokens] = tokens;
@@ -10832,7 +10797,7 @@ var Node = class _Node {
10832
10797
  node = this.#children[token] = new _Node();
10833
10798
  }
10834
10799
  }
10835
- node.insert(restTokens, index, paramMap, context, pathErrorCheckOnly);
10800
+ node.insert(restTokens, index2, paramMap, context, pathErrorCheckOnly);
10836
10801
  }
10837
10802
  buildRegExpStr() {
10838
10803
  const childKeys = Object.keys(this.#children).sort(compareKey);
@@ -10857,7 +10822,7 @@ var Node = class _Node {
10857
10822
  var Trie = class {
10858
10823
  #context = { varIndex: 0 };
10859
10824
  #root = new Node();
10860
- insert(path, index, pathErrorCheckOnly) {
10825
+ insert(path, index2, pathErrorCheckOnly) {
10861
10826
  const paramAssoc = [];
10862
10827
  const groups = [];
10863
10828
  for (let i = 0; ; ) {
@@ -10883,7 +10848,7 @@ var Trie = class {
10883
10848
  }
10884
10849
  }
10885
10850
  }
10886
- this.#root.insert(tokens, index, paramAssoc, this.#context, pathErrorCheckOnly);
10851
+ this.#root.insert(tokens, index2, paramAssoc, this.#context, pathErrorCheckOnly);
10887
10852
  return paramAssoc;
10888
10853
  }
10889
10854
  buildRegExp() {
@@ -12576,8 +12541,8 @@ function haveSameKeys(left, right) {
12576
12541
  if (leftKeys.length !== rightKeys.length) {
12577
12542
  return false;
12578
12543
  }
12579
- for (const [index, key] of leftKeys.entries()) {
12580
- if (key !== rightKeys[index]) {
12544
+ for (const [index2, key] of leftKeys.entries()) {
12545
+ if (key !== rightKeys[index2]) {
12581
12546
  return false;
12582
12547
  }
12583
12548
  }
@@ -14143,8 +14108,8 @@ var SQLiteDialect = class {
14143
14108
  }
14144
14109
  const joinsArray = [];
14145
14110
  if (joins) {
14146
- for (const [index, joinMeta] of joins.entries()) {
14147
- if (index === 0) {
14111
+ for (const [index2, joinMeta] of joins.entries()) {
14112
+ if (index2 === 0) {
14148
14113
  joinsArray.push(sql` `);
14149
14114
  }
14150
14115
  const table = joinMeta.table;
@@ -14161,7 +14126,7 @@ var SQLiteDialect = class {
14161
14126
  sql`${sql.raw(joinMeta.joinType)} join ${table} on ${joinMeta.on}`
14162
14127
  );
14163
14128
  }
14164
- if (index < joins.length - 1) {
14129
+ if (index2 < joins.length - 1) {
14165
14130
  joinsArray.push(sql` `);
14166
14131
  }
14167
14132
  }
@@ -14174,9 +14139,9 @@ var SQLiteDialect = class {
14174
14139
  buildOrderBy(orderBy) {
14175
14140
  const orderByList = [];
14176
14141
  if (orderBy) {
14177
- for (const [index, orderByValue] of orderBy.entries()) {
14142
+ for (const [index2, orderByValue] of orderBy.entries()) {
14178
14143
  orderByList.push(orderByValue);
14179
- if (index < orderBy.length - 1) {
14144
+ if (index2 < orderBy.length - 1) {
14180
14145
  orderByList.push(sql`, `);
14181
14146
  }
14182
14147
  }
@@ -14225,9 +14190,9 @@ var SQLiteDialect = class {
14225
14190
  const havingSql = having ? sql` having ${having}` : void 0;
14226
14191
  const groupByList = [];
14227
14192
  if (groupBy) {
14228
- for (const [index, groupByValue] of groupBy.entries()) {
14193
+ for (const [index2, groupByValue] of groupBy.entries()) {
14229
14194
  groupByList.push(groupByValue);
14230
- if (index < groupBy.length - 1) {
14195
+ if (index2 < groupBy.length - 1) {
14231
14196
  groupByList.push(sql`, `);
14232
14197
  }
14233
14198
  }
@@ -16155,6 +16120,52 @@ var BaseSQLiteDatabase = class {
16155
16120
  }
16156
16121
  };
16157
16122
 
16123
+ // node_modules/drizzle-orm/sqlite-core/indexes.js
16124
+ var IndexBuilderOn = class {
16125
+ constructor(name, unique) {
16126
+ this.name = name;
16127
+ this.unique = unique;
16128
+ }
16129
+ static [entityKind] = "SQLiteIndexBuilderOn";
16130
+ on(...columns) {
16131
+ return new IndexBuilder(this.name, columns, this.unique);
16132
+ }
16133
+ };
16134
+ var IndexBuilder = class {
16135
+ static [entityKind] = "SQLiteIndexBuilder";
16136
+ /** @internal */
16137
+ config;
16138
+ constructor(name, columns, unique) {
16139
+ this.config = {
16140
+ name,
16141
+ columns,
16142
+ unique,
16143
+ where: void 0
16144
+ };
16145
+ }
16146
+ /**
16147
+ * Condition for partial index.
16148
+ */
16149
+ where(condition) {
16150
+ this.config.where = condition;
16151
+ return this;
16152
+ }
16153
+ /** @internal */
16154
+ build(table) {
16155
+ return new Index(this.config, table);
16156
+ }
16157
+ };
16158
+ var Index = class {
16159
+ static [entityKind] = "SQLiteIndex";
16160
+ config;
16161
+ constructor(config, table) {
16162
+ this.config = { ...config, table };
16163
+ }
16164
+ };
16165
+ function index(name) {
16166
+ return new IndexBuilderOn(name, false);
16167
+ }
16168
+
16158
16169
  // node_modules/drizzle-orm/sqlite-core/session.js
16159
16170
  var ExecuteResultSync = class extends QueryPromise {
16160
16171
  constructor(resultCb) {
@@ -16501,12 +16512,17 @@ var tasks = sqliteTable("tasks", {
16501
16512
  resultLog: text("result_log"),
16502
16513
  retryCount: integer("retry_count").default(0),
16503
16514
  maxRetries: integer("max_retries").default(3),
16515
+ retryBackoffMs: integer("retry_backoff_ms").default(3e4),
16504
16516
  // Gateway 扩展字段(毫秒)
16505
16517
  retryAfter: integer("retry_after"),
16506
16518
  timeoutMs: integer("timeout_ms"),
16507
16519
  templateId: integer("template_id"),
16508
16520
  scheduledAt: integer("scheduled_at")
16509
- });
16521
+ }, (table) => [
16522
+ index("tasks_queue_idx").on(table.status, table.retryAfter, table.urgency, table.importance, table.createdAt, table.id),
16523
+ index("tasks_batch_status_idx").on(table.batchId, table.status),
16524
+ index("tasks_template_status_idx").on(table.templateId, table.status)
16525
+ ]);
16510
16526
  var TASK_CATEGORIES = [
16511
16527
  "translate",
16512
16528
  "generate",
@@ -16516,7 +16532,7 @@ var TASK_CATEGORIES = [
16516
16532
  ];
16517
16533
  var taskRuns = sqliteTable("task_runs", {
16518
16534
  id: integer("id").primaryKey({ autoIncrement: true }),
16519
- taskId: integer("task_id").notNull().references(() => tasks.id),
16535
+ taskId: integer("task_id").notNull().references(() => tasks.id, { onDelete: "cascade" }),
16520
16536
  sessionId: text("session_id"),
16521
16537
  model: text("model"),
16522
16538
  status: text("status").default("running"),
@@ -16529,7 +16545,10 @@ var taskRuns = sqliteTable("task_runs", {
16529
16545
  heartbeatAt: integer("heartbeat_at"),
16530
16546
  workerPid: integer("worker_pid"),
16531
16547
  childPid: integer("child_pid")
16532
- });
16548
+ }, (table) => [
16549
+ index("task_runs_task_started_idx").on(table.taskId, table.startedAt, table.id),
16550
+ index("task_runs_status_heartbeat_idx").on(table.status, table.heartbeatAt)
16551
+ ]);
16533
16552
  var taskTemplates = sqliteTable("task_templates", {
16534
16553
  id: integer("id").primaryKey({ autoIncrement: true }),
16535
16554
  name: text("name").notNull(),
@@ -16540,6 +16559,7 @@ var taskTemplates = sqliteTable("task_templates", {
16540
16559
  category: text("category").default("general"),
16541
16560
  importance: integer("importance").default(3),
16542
16561
  urgency: integer("urgency").default(3),
16562
+ batchId: text("batch_id"),
16543
16563
  scheduleType: text("schedule_type").notNull(),
16544
16564
  cronExpr: text("cron_expr"),
16545
16565
  intervalMs: integer("interval_ms"),
@@ -16547,12 +16567,15 @@ var taskTemplates = sqliteTable("task_templates", {
16547
16567
  maxInstances: integer("max_instances").default(1),
16548
16568
  maxRetries: integer("max_retries").default(3),
16549
16569
  retryBackoffMs: integer("retry_backoff_ms").default(3e4),
16570
+ timeoutMs: integer("timeout_ms"),
16550
16571
  lastRunAt: integer("last_run_at"),
16551
16572
  nextRunAt: integer("next_run_at"),
16552
16573
  enabled: integer("enabled", { mode: "boolean" }).default(true),
16553
16574
  createdAt: integer("created_at").default(0),
16554
16575
  updatedAt: integer("updated_at").default(0)
16555
- });
16576
+ }, (table) => [
16577
+ index("task_templates_due_idx").on(table.enabled, table.nextRunAt, table.id)
16578
+ ]);
16556
16579
 
16557
16580
  // src/core/db/index.ts
16558
16581
  import { existsSync, mkdirSync } from "fs";
@@ -16566,7 +16589,15 @@ var _db = null;
16566
16589
  var _migrationRan = false;
16567
16590
  function getMigrationsFolder() {
16568
16591
  const __dirname = dirname(fileURLToPath(import.meta.url));
16569
- return join(__dirname, "../../../drizzle");
16592
+ let dir = __dirname;
16593
+ for (let i = 0; i < 5; i++) {
16594
+ const candidate = join(dir, "drizzle", "meta", "_journal.json");
16595
+ if (existsSync(candidate)) {
16596
+ return join(dir, "drizzle");
16597
+ }
16598
+ dir = dirname(dir);
16599
+ }
16600
+ return join(__dirname, "../../drizzle");
16570
16601
  }
16571
16602
  function initDb() {
16572
16603
  const dataDir = dirname(DB_FILE_PATH);
@@ -16581,16 +16612,30 @@ function initDb() {
16581
16612
  id INTEGER PRIMARY KEY CHECK (id = 1),
16582
16613
  pid INTEGER NOT NULL,
16583
16614
  acquired_at INTEGER NOT NULL,
16584
- heartbeat_at INTEGER NOT NULL
16615
+ heartbeat_at INTEGER NOT NULL,
16616
+ ready_at INTEGER
16585
16617
  );
16586
16618
  `);
16619
+ const gatewayLockColumns = _sqlite.query("PRAGMA table_info(gateway_lock)").all();
16620
+ if (!gatewayLockColumns.some((column) => column.name === "ready_at")) {
16621
+ _sqlite.exec("ALTER TABLE gateway_lock ADD COLUMN ready_at INTEGER;");
16622
+ }
16587
16623
  _db = drizzle(_sqlite, { schema: schema_exports });
16588
16624
  if (!_migrationRan) {
16589
- _migrationRan = true;
16590
16625
  try {
16591
16626
  migrate(_db, { migrationsFolder: getMigrationsFolder() });
16627
+ _sqlite.exec("PRAGMA foreign_keys = ON;");
16628
+ const violations = _sqlite.query("PRAGMA foreign_key_check;").all();
16629
+ if (violations.length > 0) {
16630
+ 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`);
16631
+ }
16632
+ _migrationRan = true;
16592
16633
  } catch (err) {
16593
16634
  const msg = err instanceof Error ? err.message : String(err);
16635
+ _migrationRan = false;
16636
+ _sqlite.close();
16637
+ _sqlite = null;
16638
+ _db = null;
16594
16639
  console.error(`[supertask] migration failed: ${msg}`);
16595
16640
  throw new Error(`[supertask] DB migration failed: ${msg}`);
16596
16641
  }
@@ -16629,7 +16674,7 @@ function computeBackoff(retryCount, baseMs = 3e4, maxMs = MAX_BACKOFF_MS) {
16629
16674
  }
16630
16675
 
16631
16676
  // src/core/services/task.service.ts
16632
- var { tasks: tasks2 } = schema_exports;
16677
+ var { tasks: tasks2, taskRuns: taskRuns2 } = schema_exports;
16633
16678
  var TaskService = class {
16634
16679
  static buildScopeWhere(scope) {
16635
16680
  const conditions = [];
@@ -16639,9 +16684,27 @@ var TaskService = class {
16639
16684
  return conditions;
16640
16685
  }
16641
16686
  static async add(data) {
16687
+ this.validateNewTask(data);
16642
16688
  const result = await db.insert(tasks2).values(data).returning();
16643
16689
  return result[0];
16644
16690
  }
16691
+ static validateNewTask(data) {
16692
+ if (!data.name.trim()) throw new Error("name \u4E0D\u80FD\u4E3A\u7A7A");
16693
+ if (!data.agent.trim()) throw new Error("agent \u4E0D\u80FD\u4E3A\u7A7A");
16694
+ if (!data.prompt.trim()) throw new Error("prompt \u4E0D\u80FD\u4E3A\u7A7A");
16695
+ this.validateInteger("importance", data.importance, 1, 5);
16696
+ this.validateInteger("urgency", data.urgency, 1, 5);
16697
+ this.validateInteger("maxRetries", data.maxRetries, 0, 1e3);
16698
+ this.validateInteger("retryBackoffMs", data.retryBackoffMs, 0, 864e5);
16699
+ this.validateInteger("timeoutMs", data.timeoutMs, 1e3, 6048e5);
16700
+ this.validateInteger("dependsOn", data.dependsOn, 1, Number.MAX_SAFE_INTEGER);
16701
+ }
16702
+ static validateInteger(name, value, min, max) {
16703
+ if (value === void 0 || value === null) return;
16704
+ if (!Number.isInteger(value) || value < min || value > max) {
16705
+ throw new Error(`${name} \u5FC5\u987B\u662F ${min} \u5230 ${max} \u4E4B\u95F4\u7684\u6574\u6570`);
16706
+ }
16707
+ }
16645
16708
  static async next(scope = {}) {
16646
16709
  const baseConditions = [...this.buildScopeWhere(scope)];
16647
16710
  const nowMs = Date.now();
@@ -16664,7 +16727,7 @@ var TaskService = class {
16664
16727
  ),
16665
16728
  and(
16666
16729
  eq(tasks2.status, "failed"),
16667
- sql`${tasks2.retryCount} < ${tasks2.maxRetries}`,
16730
+ sql`${tasks2.retryCount} <= ${tasks2.maxRetries}`,
16668
16731
  retryAfterFilter
16669
16732
  )
16670
16733
  );
@@ -16678,7 +16741,8 @@ var TaskService = class {
16678
16741
  const allTasks = await db.select().from(tasks2).where(and(...conditions)).orderBy(
16679
16742
  desc(tasks2.urgency),
16680
16743
  desc(tasks2.importance),
16681
- asc(tasks2.createdAt)
16744
+ asc(tasks2.createdAt),
16745
+ asc(tasks2.id)
16682
16746
  );
16683
16747
  for (const task of allTasks) {
16684
16748
  if (task.dependsOn) {
@@ -16699,7 +16763,7 @@ var TaskService = class {
16699
16763
  eq(tasks2.status, "pending"),
16700
16764
  and(
16701
16765
  eq(tasks2.status, "failed"),
16702
- sql`${tasks2.retryCount} < ${tasks2.maxRetries}`
16766
+ sql`${tasks2.retryCount} <= ${tasks2.maxRetries}`
16703
16767
  )
16704
16768
  ),
16705
16769
  ...this.buildScopeWhere(scope)
@@ -16712,7 +16776,11 @@ var TaskService = class {
16712
16776
  return result[0] || null;
16713
16777
  }
16714
16778
  static async done(id, log, scope = {}) {
16715
- const conditions = [eq(tasks2.id, id), ...this.buildScopeWhere(scope)];
16779
+ const conditions = [
16780
+ eq(tasks2.id, id),
16781
+ eq(tasks2.status, "running"),
16782
+ ...this.buildScopeWhere(scope)
16783
+ ];
16716
16784
  const result = await db.update(tasks2).set({
16717
16785
  status: "done",
16718
16786
  finishedAt: /* @__PURE__ */ new Date(),
@@ -16723,17 +16791,24 @@ var TaskService = class {
16723
16791
  }
16724
16792
  static async fail(id, log, scope = {}, options) {
16725
16793
  const current = await this.getById(id, scope);
16726
- if (!current) return null;
16794
+ if (!current || current.status !== "running") return null;
16727
16795
  const newRetryCount = (current.retryCount ?? 0) + 1;
16728
16796
  const maxRetries = current.maxRetries ?? 3;
16729
- const isDeadLetter = options?.setDeadLetter ?? newRetryCount >= maxRetries;
16730
- const conditions = [eq(tasks2.id, id), ...this.buildScopeWhere(scope)];
16797
+ const isDeadLetter = options?.setDeadLetter ?? newRetryCount > maxRetries;
16798
+ const conditions = [
16799
+ eq(tasks2.id, id),
16800
+ eq(tasks2.status, "running"),
16801
+ ...this.buildScopeWhere(scope)
16802
+ ];
16731
16803
  const result = await db.update(tasks2).set({
16732
16804
  status: isDeadLetter ? "dead_letter" : "failed",
16733
16805
  finishedAt: /* @__PURE__ */ new Date(),
16734
16806
  resultLog: log,
16735
16807
  retryCount: newRetryCount,
16736
- retryAfter: isDeadLetter ? null : options?.retryAfterMs ?? Date.now() + computeBackoff(newRetryCount)
16808
+ retryAfter: isDeadLetter ? null : options?.retryAfterMs ?? Date.now() + computeBackoff(
16809
+ newRetryCount,
16810
+ current.retryBackoffMs ?? 3e4
16811
+ )
16737
16812
  }).where(and(...conditions)).returning();
16738
16813
  return result[0] || null;
16739
16814
  }
@@ -16766,8 +16841,20 @@ var TaskService = class {
16766
16841
  return result.length;
16767
16842
  }
16768
16843
  static async cancel(id, scope = {}) {
16769
- const conditions = [eq(tasks2.id, id), ...this.buildScopeWhere(scope)];
16770
- const result = await db.update(tasks2).set({ status: "cancelled" }).where(and(...conditions)).returning();
16844
+ const conditions = [
16845
+ eq(tasks2.id, id),
16846
+ or(
16847
+ eq(tasks2.status, "pending"),
16848
+ eq(tasks2.status, "running"),
16849
+ eq(tasks2.status, "failed")
16850
+ ),
16851
+ ...this.buildScopeWhere(scope)
16852
+ ];
16853
+ const result = await db.update(tasks2).set({
16854
+ status: "cancelled",
16855
+ finishedAt: /* @__PURE__ */ new Date(),
16856
+ retryAfter: null
16857
+ }).where(and(...conditions)).returning();
16771
16858
  return result[0] || null;
16772
16859
  }
16773
16860
  static async retry(id, scope = {}) {
@@ -16779,7 +16866,8 @@ var TaskService = class {
16779
16866
  status: "pending",
16780
16867
  startedAt: null,
16781
16868
  finishedAt: null,
16782
- retryAfter: null
16869
+ retryAfter: null,
16870
+ retryCount: 0
16783
16871
  }).where(and(...conditions)).returning();
16784
16872
  return result[0] || null;
16785
16873
  }
@@ -16793,7 +16881,8 @@ var TaskService = class {
16793
16881
  status: "pending",
16794
16882
  startedAt: null,
16795
16883
  finishedAt: null,
16796
- retryAfter: null
16884
+ retryAfter: null,
16885
+ retryCount: 0
16797
16886
  }).where(and(...conditions)).returning();
16798
16887
  return result.length;
16799
16888
  }
@@ -16861,6 +16950,9 @@ var TaskService = class {
16861
16950
  }
16862
16951
  static async delete(id, scope = {}) {
16863
16952
  const conditions = [eq(tasks2.id, id), ...this.buildScopeWhere(scope)];
16953
+ const existing = await db.select({ id: tasks2.id }).from(tasks2).where(and(...conditions)).limit(1);
16954
+ if (!existing[0]) return false;
16955
+ await db.delete(taskRuns2).where(eq(taskRuns2.taskId, id));
16864
16956
  const result = await db.delete(tasks2).where(and(...conditions)).returning();
16865
16957
  return result.length > 0;
16866
16958
  }
@@ -16878,59 +16970,59 @@ var TaskService = class {
16878
16970
  };
16879
16971
 
16880
16972
  // src/core/services/task-run.service.ts
16881
- var { taskRuns: taskRuns2 } = schema_exports;
16973
+ var { taskRuns: taskRuns3 } = schema_exports;
16882
16974
  var TaskRunService = class {
16883
16975
  static async create(data) {
16884
- const result = await db.insert(taskRuns2).values(data).returning();
16976
+ const result = await db.insert(taskRuns3).values(data).returning();
16885
16977
  return result[0];
16886
16978
  }
16887
16979
  static async updateSessionId(id, sessionId) {
16888
- const result = await db.update(taskRuns2).set({ sessionId }).where(eq(taskRuns2.id, id)).returning();
16980
+ const result = await db.update(taskRuns3).set({ sessionId }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
16889
16981
  return result[0] || null;
16890
16982
  }
16891
16983
  static async done(id, log) {
16892
- const result = await db.update(taskRuns2).set({
16984
+ const result = await db.update(taskRuns3).set({
16893
16985
  status: "done",
16894
16986
  finishedAt: /* @__PURE__ */ new Date(),
16895
16987
  log
16896
- }).where(eq(taskRuns2.id, id)).returning();
16988
+ }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
16897
16989
  return result[0] || null;
16898
16990
  }
16899
16991
  static async fail(id, log) {
16900
- const result = await db.update(taskRuns2).set({
16992
+ const result = await db.update(taskRuns3).set({
16901
16993
  status: "failed",
16902
16994
  finishedAt: /* @__PURE__ */ new Date(),
16903
16995
  log
16904
- }).where(eq(taskRuns2.id, id)).returning();
16996
+ }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
16905
16997
  return result[0] || null;
16906
16998
  }
16907
16999
  static async heartbeat(id) {
16908
- const result = await db.update(taskRuns2).set({ heartbeatAt: Date.now() }).where(and(eq(taskRuns2.id, id), eq(taskRuns2.status, "running"))).returning();
17000
+ const result = await db.update(taskRuns3).set({ heartbeatAt: Date.now() }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
16909
17001
  return result[0] || null;
16910
17002
  }
16911
17003
  static async updatePid(id, workerPid, childPid) {
16912
- const result = await db.update(taskRuns2).set({
17004
+ const result = await db.update(taskRuns3).set({
16913
17005
  workerPid,
16914
17006
  childPid,
16915
17007
  lockedAt: Date.now(),
16916
17008
  lockedBy: `gateway-${process.pid}`
16917
- }).where(eq(taskRuns2.id, id)).returning();
17009
+ }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
16918
17010
  return result[0] || null;
16919
17011
  }
16920
17012
  static async getById(id) {
16921
- const result = await db.select().from(taskRuns2).where(eq(taskRuns2.id, id));
17013
+ const result = await db.select().from(taskRuns3).where(eq(taskRuns3.id, id));
16922
17014
  return result[0] || null;
16923
17015
  }
16924
17016
  static async listByTaskId(taskId) {
16925
- return await db.select().from(taskRuns2).where(eq(taskRuns2.taskId, taskId)).orderBy(desc(taskRuns2.startedAt), desc(taskRuns2.id));
17017
+ return await db.select().from(taskRuns3).where(eq(taskRuns3.taskId, taskId)).orderBy(desc(taskRuns3.startedAt), desc(taskRuns3.id));
16926
17018
  }
16927
17019
  static async getLatestByTaskId(taskId) {
16928
- const result = await db.select().from(taskRuns2).where(eq(taskRuns2.taskId, taskId)).orderBy(desc(taskRuns2.startedAt), desc(taskRuns2.id)).limit(1);
17020
+ const result = await db.select().from(taskRuns3).where(eq(taskRuns3.taskId, taskId)).orderBy(desc(taskRuns3.startedAt), desc(taskRuns3.id)).limit(1);
16929
17021
  return result[0] || null;
16930
17022
  }
16931
17023
  static async getLatestByTaskIds(taskIds) {
16932
17024
  if (taskIds.length === 0) return /* @__PURE__ */ new Map();
16933
- const latestRuns = await db.select().from(taskRuns2).where(inArray(taskRuns2.taskId, taskIds)).orderBy(desc(taskRuns2.startedAt));
17025
+ const latestRuns = await db.select().from(taskRuns3).where(inArray(taskRuns3.taskId, taskIds)).orderBy(desc(taskRuns3.startedAt), desc(taskRuns3.id));
16934
17026
  const result = /* @__PURE__ */ new Map();
16935
17027
  for (const run of latestRuns) {
16936
17028
  if (!result.has(run.taskId)) {
@@ -16944,22 +17036,23 @@ var TaskRunService = class {
16944
17036
  const cutoffSec = Math.floor(cutoffMs / 1e3);
16945
17037
  const { tasks: tasksTable } = schema_exports;
16946
17038
  const result = await db.select({
16947
- runId: taskRuns2.id,
16948
- taskId: taskRuns2.taskId,
16949
- childPid: taskRuns2.childPid,
17039
+ runId: taskRuns3.id,
17040
+ taskId: taskRuns3.taskId,
17041
+ childPid: taskRuns3.childPid,
16950
17042
  taskRetryCount: tasksTable.retryCount,
16951
- taskMaxRetries: tasksTable.maxRetries
16952
- }).from(taskRuns2).innerJoin(tasksTable, eq(taskRuns2.taskId, tasksTable.id)).where(
17043
+ taskMaxRetries: tasksTable.maxRetries,
17044
+ taskRetryBackoffMs: tasksTable.retryBackoffMs
17045
+ }).from(taskRuns3).innerJoin(tasksTable, eq(taskRuns3.taskId, tasksTable.id)).where(
16953
17046
  and(
16954
- eq(taskRuns2.status, "running"),
17047
+ eq(taskRuns3.status, "running"),
16955
17048
  or(
16956
17049
  and(
16957
- sql`${taskRuns2.heartbeatAt} IS NULL`,
16958
- sql`${taskRuns2.startedAt} < ${cutoffSec}`
17050
+ sql`${taskRuns3.heartbeatAt} IS NULL`,
17051
+ sql`${taskRuns3.startedAt} < ${cutoffSec}`
16959
17052
  ),
16960
17053
  and(
16961
- sql`${taskRuns2.heartbeatAt} IS NOT NULL`,
16962
- sql`${taskRuns2.heartbeatAt} < ${cutoffMs}`
17054
+ sql`${taskRuns3.heartbeatAt} IS NOT NULL`,
17055
+ sql`${taskRuns3.heartbeatAt} < ${cutoffMs}`
16963
17056
  )
16964
17057
  )
16965
17058
  )
@@ -16969,28 +17062,50 @@ var TaskRunService = class {
16969
17062
  taskId: row.taskId,
16970
17063
  childPid: row.childPid,
16971
17064
  taskRetryCount: row.taskRetryCount ?? 0,
16972
- taskMaxRetries: row.taskMaxRetries ?? 3
17065
+ taskMaxRetries: row.taskMaxRetries ?? 3,
17066
+ taskRetryBackoffMs: row.taskRetryBackoffMs ?? 3e4
16973
17067
  }));
16974
17068
  }
16975
17069
  static async getRunningRunByTaskId(taskId) {
16976
- 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);
17070
+ 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);
16977
17071
  return result[0] || null;
16978
17072
  }
16979
17073
  static async deleteByTaskIds(taskIds) {
16980
17074
  if (taskIds.length === 0) return 0;
16981
- const result = await db.delete(taskRuns2).where(inArray(taskRuns2.taskId, taskIds)).returning();
17075
+ const result = await db.delete(taskRuns3).where(inArray(taskRuns3.taskId, taskIds)).returning();
16982
17076
  return result.length;
16983
17077
  }
16984
17078
  static async getAllRunningRuns() {
16985
- return await db.select().from(taskRuns2).where(eq(taskRuns2.status, "running"));
17079
+ return await db.select().from(taskRuns3).where(eq(taskRuns3.status, "running"));
16986
17080
  }
16987
17081
  };
16988
17082
 
17083
+ // src/core/cron-parser.ts
17084
+ var import_cron_parser = __toESM(require_dist(), 1);
17085
+ function getNextCronRun(expr, afterMs) {
17086
+ try {
17087
+ const fromDate = afterMs != null ? new Date(afterMs) : /* @__PURE__ */ new Date();
17088
+ const interval = import_cron_parser.CronExpressionParser.parse(expr, { currentDate: fromDate });
17089
+ const next = interval.next();
17090
+ return next.getTime();
17091
+ } catch {
17092
+ return null;
17093
+ }
17094
+ }
17095
+ function isValidCronExpr(expr) {
17096
+ try {
17097
+ import_cron_parser.CronExpressionParser.parse(expr);
17098
+ return true;
17099
+ } catch {
17100
+ return false;
17101
+ }
17102
+ }
17103
+
16989
17104
  // src/core/services/task-template.service.ts
16990
- init_cron_parser();
16991
17105
  var { taskTemplates: taskTemplates2 } = schema_exports;
16992
17106
  var TaskTemplateService = class {
16993
17107
  static async create(data) {
17108
+ this.validate(data);
16994
17109
  const now = Date.now();
16995
17110
  const result = await db.insert(taskTemplates2).values({ ...data, createdAt: now, updatedAt: now }).returning();
16996
17111
  const tmpl = result[0];
@@ -17006,8 +17121,38 @@ var TaskTemplateService = class {
17006
17121
  }
17007
17122
  return tmpl;
17008
17123
  }
17124
+ static validate(data) {
17125
+ if (!data.name.trim()) throw new Error("name \u4E0D\u80FD\u4E3A\u7A7A");
17126
+ if (!data.agent.trim()) throw new Error("agent \u4E0D\u80FD\u4E3A\u7A7A");
17127
+ if (!data.prompt.trim()) throw new Error("prompt \u4E0D\u80FD\u4E3A\u7A7A");
17128
+ const scheduleType = data.scheduleType;
17129
+ if (!["cron", "delayed", "recurring"].includes(scheduleType)) {
17130
+ throw new Error("scheduleType \u5FC5\u987B\u662F cron\u3001delayed \u6216 recurring");
17131
+ }
17132
+ if (scheduleType === "cron" && (!data.cronExpr || !isValidCronExpr(data.cronExpr))) {
17133
+ throw new Error("cronExpr \u7F3A\u5931\u6216\u683C\u5F0F\u65E0\u6548");
17134
+ }
17135
+ if (scheduleType === "recurring" && (!Number.isInteger(data.intervalMs) || (data.intervalMs ?? 0) <= 0)) {
17136
+ throw new Error("intervalMs \u5FC5\u987B\u662F\u6B63\u6574\u6570");
17137
+ }
17138
+ if (scheduleType === "delayed" && (!Number.isInteger(data.runAt) || (data.runAt ?? 0) <= 0)) {
17139
+ throw new Error("runAt \u5FC5\u987B\u662F\u6B63\u6574\u6570\u65F6\u95F4\u6233");
17140
+ }
17141
+ this.validateInteger("importance", data.importance, 1, 5);
17142
+ this.validateInteger("urgency", data.urgency, 1, 5);
17143
+ this.validateInteger("maxInstances", data.maxInstances, 1, 1e3);
17144
+ this.validateInteger("maxRetries", data.maxRetries, 0, 1e3);
17145
+ this.validateInteger("retryBackoffMs", data.retryBackoffMs, 0, 864e5);
17146
+ this.validateInteger("timeoutMs", data.timeoutMs, 1e3, 6048e5);
17147
+ }
17148
+ static validateInteger(name, value, min, max) {
17149
+ if (value === void 0 || value === null) return;
17150
+ if (!Number.isInteger(value) || value < min || value > max) {
17151
+ throw new Error(`${name} \u5FC5\u987B\u662F ${min} \u5230 ${max} \u4E4B\u95F4\u7684\u6574\u6570`);
17152
+ }
17153
+ }
17009
17154
  static async list(limit = 50) {
17010
- return await db.select().from(taskTemplates2).orderBy(desc(taskTemplates2.createdAt)).limit(limit);
17155
+ return await db.select().from(taskTemplates2).orderBy(desc(taskTemplates2.createdAt), desc(taskTemplates2.id)).limit(limit);
17011
17156
  }
17012
17157
  static async getById(id) {
17013
17158
  const result = await db.select().from(taskTemplates2).where(eq(taskTemplates2.id, id));
@@ -17046,72 +17191,236 @@ var TaskTemplateService = class {
17046
17191
  };
17047
17192
 
17048
17193
  // src/gateway/config.ts
17049
- import { readFileSync, existsSync as existsSync2 } from "fs";
17194
+ import { existsSync as existsSync2, readFileSync } from "fs";
17050
17195
  import { homedir as homedir2 } from "os";
17051
17196
  import { join as join2 } from "path";
17052
17197
  var DEFAULT_CONFIG = {
17198
+ configVersion: 2,
17053
17199
  worker: {
17054
17200
  maxConcurrency: 2,
17055
17201
  pollIntervalMs: 1e3,
17056
17202
  heartbeatIntervalMs: 3e4,
17057
- taskTimeoutMs: 18e5
17203
+ taskTimeoutMs: 18e5,
17204
+ shutdownGracePeriodMs: 3e4
17058
17205
  },
17059
17206
  scheduler: {
17060
17207
  enabled: true,
17061
- checkIntervalMs: 1e3,
17062
- catchUp: "next"
17208
+ checkIntervalMs: 1e3
17063
17209
  },
17064
17210
  watchdog: {
17065
17211
  heartbeatTimeoutMs: 6e5,
17066
- cleanupIntervalMs: 6e4,
17212
+ checkIntervalMs: 6e4,
17213
+ cleanupIntervalMs: 864e5,
17067
17214
  retentionDays: 30
17068
17215
  },
17069
17216
  dashboard: {
17070
17217
  enabled: true,
17071
17218
  port: 4680
17072
- },
17073
- logging: {
17074
- level: "info",
17075
- format: "json"
17076
17219
  }
17077
17220
  };
17078
- var CONFIG_PATH = join2(homedir2(), ".config/opencode/supertask.json");
17079
- function deepMerge(base, override) {
17080
- const result = { ...base };
17081
- for (const key of Object.keys(override)) {
17082
- const val = override[key];
17083
- if (val !== null && typeof val === "object" && !Array.isArray(val) && key in result) {
17084
- const baseVal = result[key];
17085
- if (baseVal !== null && typeof baseVal === "object" && !Array.isArray(baseVal)) {
17086
- result[key] = deepMerge(baseVal, val);
17087
- continue;
17088
- }
17089
- }
17090
- if (val !== void 0) {
17091
- result[key] = val;
17092
- }
17221
+ var DEFAULT_CONFIG_PATH = join2(homedir2(), ".config/opencode/supertask.json");
17222
+ function getConfigPath() {
17223
+ return process.env.SUPERTASK_CONFIG_PATH ?? DEFAULT_CONFIG_PATH;
17224
+ }
17225
+ function objectAt(value, path) {
17226
+ if (value === void 0) return {};
17227
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
17228
+ throw new Error(`${path} \u5FC5\u987B\u662F\u5BF9\u8C61`);
17093
17229
  }
17094
- return result;
17230
+ return value;
17231
+ }
17232
+ function integerAt(source, key, fallback, min, max, path) {
17233
+ const value = source[key];
17234
+ if (value === void 0) return fallback;
17235
+ if (typeof value !== "number" || !Number.isInteger(value) || value < min || value > max) {
17236
+ throw new Error(`${path}.${key} \u5FC5\u987B\u662F ${min} \u5230 ${max} \u4E4B\u95F4\u7684\u6574\u6570`);
17237
+ }
17238
+ return value;
17239
+ }
17240
+ function booleanAt(source, key, fallback, path) {
17241
+ const value = source[key];
17242
+ if (value === void 0) return fallback;
17243
+ if (typeof value !== "boolean") throw new Error(`${path}.${key} \u5FC5\u987B\u662F\u5E03\u5C14\u503C`);
17244
+ return value;
17095
17245
  }
17096
- function loadConfig() {
17097
- if (!existsSync2(CONFIG_PATH)) {
17098
- return DEFAULT_CONFIG;
17246
+ function validateConfig(input) {
17247
+ const root = objectAt(input, "config");
17248
+ const version2 = root.configVersion ?? 1;
17249
+ if (version2 !== 1 && version2 !== 2) {
17250
+ throw new Error("config.configVersion \u53EA\u652F\u6301 1 \u6216 2");
17251
+ }
17252
+ const worker = objectAt(root.worker, "worker");
17253
+ const scheduler = objectAt(root.scheduler, "scheduler");
17254
+ const watchdog = objectAt(root.watchdog, "watchdog");
17255
+ const dashboard = objectAt(root.dashboard, "dashboard");
17256
+ const heartbeatIntervalMs = integerAt(worker, "heartbeatIntervalMs", DEFAULT_CONFIG.worker.heartbeatIntervalMs, 1e3, 36e5, "worker");
17257
+ const heartbeatTimeoutMs = integerAt(watchdog, "heartbeatTimeoutMs", DEFAULT_CONFIG.watchdog.heartbeatTimeoutMs, 1e3, 864e5, "watchdog");
17258
+ let checkIntervalMs;
17259
+ let cleanupIntervalMs;
17260
+ if (version2 === 1 && watchdog.checkIntervalMs === void 0 && watchdog.cleanupIntervalMs !== void 0) {
17261
+ checkIntervalMs = integerAt(watchdog, "cleanupIntervalMs", DEFAULT_CONFIG.watchdog.checkIntervalMs, 1e3, 36e5, "watchdog");
17262
+ cleanupIntervalMs = DEFAULT_CONFIG.watchdog.cleanupIntervalMs;
17263
+ } else {
17264
+ checkIntervalMs = integerAt(watchdog, "checkIntervalMs", DEFAULT_CONFIG.watchdog.checkIntervalMs, 1e3, 36e5, "watchdog");
17265
+ cleanupIntervalMs = integerAt(watchdog, "cleanupIntervalMs", DEFAULT_CONFIG.watchdog.cleanupIntervalMs, 6e4, 6048e5, "watchdog");
17266
+ }
17267
+ if (heartbeatIntervalMs >= heartbeatTimeoutMs) {
17268
+ throw new Error("worker.heartbeatIntervalMs \u5FC5\u987B\u5C0F\u4E8E watchdog.heartbeatTimeoutMs");
17099
17269
  }
17270
+ if (checkIntervalMs > heartbeatTimeoutMs) {
17271
+ throw new Error("watchdog.checkIntervalMs \u4E0D\u80FD\u5927\u4E8E watchdog.heartbeatTimeoutMs");
17272
+ }
17273
+ return {
17274
+ configVersion: 2,
17275
+ worker: {
17276
+ maxConcurrency: integerAt(worker, "maxConcurrency", DEFAULT_CONFIG.worker.maxConcurrency, 1, 64, "worker"),
17277
+ pollIntervalMs: integerAt(worker, "pollIntervalMs", DEFAULT_CONFIG.worker.pollIntervalMs, 50, 6e4, "worker"),
17278
+ heartbeatIntervalMs,
17279
+ taskTimeoutMs: integerAt(worker, "taskTimeoutMs", DEFAULT_CONFIG.worker.taskTimeoutMs, 1e3, 6048e5, "worker"),
17280
+ shutdownGracePeriodMs: integerAt(worker, "shutdownGracePeriodMs", DEFAULT_CONFIG.worker.shutdownGracePeriodMs, 0, 36e5, "worker")
17281
+ },
17282
+ scheduler: {
17283
+ enabled: booleanAt(scheduler, "enabled", DEFAULT_CONFIG.scheduler.enabled, "scheduler"),
17284
+ checkIntervalMs: integerAt(scheduler, "checkIntervalMs", DEFAULT_CONFIG.scheduler.checkIntervalMs, 100, 6e4, "scheduler")
17285
+ },
17286
+ watchdog: {
17287
+ heartbeatTimeoutMs,
17288
+ checkIntervalMs,
17289
+ cleanupIntervalMs,
17290
+ retentionDays: integerAt(watchdog, "retentionDays", DEFAULT_CONFIG.watchdog.retentionDays, 1, 3650, "watchdog")
17291
+ },
17292
+ dashboard: {
17293
+ enabled: booleanAt(dashboard, "enabled", DEFAULT_CONFIG.dashboard.enabled, "dashboard"),
17294
+ port: integerAt(dashboard, "port", DEFAULT_CONFIG.dashboard.port, 1, 65535, "dashboard")
17295
+ }
17296
+ };
17297
+ }
17298
+ function loadConfig(path = getConfigPath()) {
17299
+ if (!existsSync2(path)) return validateConfig({ configVersion: 2 });
17100
17300
  try {
17101
- const raw2 = readFileSync(CONFIG_PATH, "utf-8");
17102
- const userConfig = JSON.parse(raw2);
17103
- return deepMerge(DEFAULT_CONFIG, userConfig);
17104
- } catch (err) {
17105
- const msg = err instanceof Error ? err.message : String(err);
17106
- console.error(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "warn", msg: "config load failed, using defaults", path: CONFIG_PATH, error: msg }));
17107
- return DEFAULT_CONFIG;
17301
+ return validateConfig(JSON.parse(readFileSync(path, "utf-8")));
17302
+ } catch (error) {
17303
+ const message = error instanceof Error ? error.message : String(error);
17304
+ throw new Error(`\u65E0\u6CD5\u8BFB\u53D6\u914D\u7F6E ${path}: ${message}`);
17108
17305
  }
17109
17306
  }
17110
17307
 
17111
17308
  // src/web/index.tsx
17112
- import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
17309
+ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync, mkdirSync as mkdirSync2, renameSync } from "fs";
17113
17310
  import { dirname as dirname2 } from "path";
17311
+
17312
+ // src/gateway/health.ts
17313
+ var LOCK_STALE_MS = 3e4;
17314
+ var state = null;
17315
+ function componentStatus(component, enabled, maxAgeMs, now) {
17316
+ const lastActivityAt = state?.lastActivityAt[component] ?? null;
17317
+ const ageMs = lastActivityAt == null ? null : Math.max(0, now - lastActivityAt);
17318
+ return {
17319
+ enabled,
17320
+ healthy: !enabled || ageMs != null && ageMs <= maxAgeMs,
17321
+ lastActivityAt,
17322
+ ageMs,
17323
+ maxAgeMs
17324
+ };
17325
+ }
17326
+ function getGatewayHealth(now = Date.now()) {
17327
+ const worker = componentStatus(
17328
+ "worker",
17329
+ true,
17330
+ Math.max((state?.config.workerPollIntervalMs ?? 1e3) * 5, 5e3),
17331
+ now
17332
+ );
17333
+ const scheduler = componentStatus(
17334
+ "scheduler",
17335
+ state?.config.schedulerEnabled ?? false,
17336
+ Math.max((state?.config.schedulerCheckIntervalMs ?? 1e3) * 5, 5e3),
17337
+ now
17338
+ );
17339
+ const watchdog = componentStatus(
17340
+ "watchdog",
17341
+ true,
17342
+ Math.max((state?.config.watchdogCheckIntervalMs ?? 6e4) * 3, 5e3),
17343
+ now
17344
+ );
17345
+ let lock = { pid: null, heartbeatAt: null, readyAt: null, ageMs: null, healthy: false };
17346
+ try {
17347
+ const row = sqlite.prepare(
17348
+ "SELECT pid, heartbeat_at, ready_at FROM gateway_lock WHERE id = 1"
17349
+ ).get();
17350
+ if (row) {
17351
+ const ageMs = Math.max(0, now - row.heartbeat_at);
17352
+ lock = {
17353
+ pid: row.pid,
17354
+ heartbeatAt: row.heartbeat_at,
17355
+ readyAt: row.ready_at,
17356
+ ageMs,
17357
+ healthy: row.pid === process.pid && row.ready_at != null && ageMs < LOCK_STALE_MS
17358
+ };
17359
+ }
17360
+ } catch {
17361
+ }
17362
+ const healthy = state != null && lock.healthy && worker.healthy && scheduler.healthy && watchdog.healthy;
17363
+ return {
17364
+ status: healthy ? "ok" : "degraded",
17365
+ pid: process.pid,
17366
+ uptimeMs: state ? Math.max(0, now - state.startedAt) : 0,
17367
+ lock,
17368
+ components: { worker, scheduler, watchdog }
17369
+ };
17370
+ }
17371
+
17372
+ // src/web/index.tsx
17114
17373
  var app = new Hono2();
17374
+ var TASK_STATUSES = /* @__PURE__ */ new Set([
17375
+ "pending",
17376
+ "running",
17377
+ "done",
17378
+ "failed",
17379
+ "dead_letter",
17380
+ "cancelled"
17381
+ ]);
17382
+ function parsePositiveInteger(value) {
17383
+ if (!/^\d+$/.test(value)) return null;
17384
+ const parsed = Number(value);
17385
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
17386
+ }
17387
+ function parseTaskStatus(value) {
17388
+ return TASK_STATUSES.has(value) ? value : null;
17389
+ }
17390
+ function safeStatus(value) {
17391
+ return value && TASK_STATUSES.has(value) ? value : "unknown";
17392
+ }
17393
+ app.use("*", async (c, next) => {
17394
+ await next();
17395
+ c.header("X-Content-Type-Options", "nosniff");
17396
+ c.header("X-Frame-Options", "DENY");
17397
+ c.header("Referrer-Policy", "no-referrer");
17398
+ });
17399
+ app.use("/api/*", async (c, next) => {
17400
+ if (["GET", "HEAD", "OPTIONS"].includes(c.req.method)) return next();
17401
+ const fetchSite = c.req.header("Sec-Fetch-Site");
17402
+ if (fetchSite && !["same-origin", "none"].includes(fetchSite)) {
17403
+ return c.json({ error: "cross-site request rejected" }, 403);
17404
+ }
17405
+ const origin = c.req.header("Origin");
17406
+ if (origin) {
17407
+ try {
17408
+ const originUrl = new URL(origin);
17409
+ const requestUrl = new URL(c.req.url);
17410
+ const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(originUrl.hostname);
17411
+ if (!loopback || originUrl.origin !== requestUrl.origin) {
17412
+ return c.json({ error: "cross-site request rejected" }, 403);
17413
+ }
17414
+ } catch {
17415
+ return c.json({ error: "invalid origin" }, 403);
17416
+ }
17417
+ }
17418
+ return next();
17419
+ });
17420
+ app.get("/health", (c) => {
17421
+ const health = getGatewayHealth();
17422
+ return c.json(health, health.status === "ok" ? 200 : 503);
17423
+ });
17115
17424
  function formatDuration(startAt, endAt) {
17116
17425
  if (!startAt) return "-";
17117
17426
  const start = new Date(startAt).getTime();
@@ -17149,17 +17458,21 @@ function esc(s) {
17149
17458
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
17150
17459
  }
17151
17460
  function readCurrentConfig() {
17152
- if (!existsSync3(CONFIG_PATH)) return {};
17461
+ const configPath = getConfigPath();
17462
+ if (!existsSync3(configPath)) return {};
17153
17463
  try {
17154
- return JSON.parse(readFileSync2(CONFIG_PATH, "utf-8"));
17464
+ return JSON.parse(readFileSync2(configPath, "utf-8"));
17155
17465
  } catch {
17156
17466
  return {};
17157
17467
  }
17158
17468
  }
17159
17469
  function writeConfig(cfg) {
17160
- const dir = dirname2(CONFIG_PATH);
17470
+ const configPath = getConfigPath();
17471
+ const dir = dirname2(configPath);
17161
17472
  if (!existsSync3(dir)) mkdirSync2(dir, { recursive: true });
17162
- writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2) + "\n");
17473
+ const tempPath = `${configPath}.${process.pid}.tmp`;
17474
+ writeFileSync(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
17475
+ renameSync(tempPath, configPath);
17163
17476
  }
17164
17477
  var SHARED_STYLES = html`
17165
17478
  <style>
@@ -17273,11 +17586,11 @@ async function saveConfig(){
17273
17586
  scheduler:{
17274
17587
  enabled:form.se.checked,
17275
17588
  checkIntervalMs:Number(form.si.value),
17276
- catchUp:form.cu.value,
17277
17589
  },
17278
17590
  watchdog:{
17279
17591
  heartbeatTimeoutMs:Number(form.wt.value)*1000,
17280
- cleanupIntervalMs:Number(form.wc.value)*1000,
17592
+ checkIntervalMs:Number(form.wci.value)*1000,
17593
+ cleanupIntervalMs:Number(form.wcl.value)*3600000,
17281
17594
  retentionDays:Number(form.rd.value),
17282
17595
  }
17283
17596
  };
@@ -17309,12 +17622,16 @@ async function saveConfig(){
17309
17622
  </body></html>`;
17310
17623
  }
17311
17624
  app.get("/", async (c) => {
17312
- const page = Number(c.req.query("page") || "1");
17625
+ const pageParam = c.req.query("page") || "1";
17626
+ const page = parsePositiveInteger(pageParam);
17627
+ if (page === null) return c.text("invalid page", 400);
17313
17628
  const statusFilter = c.req.query("status") || "";
17629
+ const parsedStatus = statusFilter ? parseTaskStatus(statusFilter) : null;
17630
+ if (statusFilter && !parsedStatus) return c.text("invalid status", 400);
17314
17631
  const limit = 50;
17315
17632
  const offset = (page - 1) * limit;
17316
17633
  const [tasks3, statsData] = await Promise.all([
17317
- TaskService.list({ limit, offset, ...statusFilter ? { status: statusFilter } : {} }),
17634
+ TaskService.list({ limit, offset, ...parsedStatus ? { status: parsedStatus } : {} }),
17318
17635
  TaskService.stats({})
17319
17636
  ]);
17320
17637
  const taskIds = tasks3.map((t) => t.id);
@@ -17337,12 +17654,13 @@ app.get("/", async (c) => {
17337
17654
  </div>`;
17338
17655
  let rows = "";
17339
17656
  for (const task of tasks3) {
17340
- const st = (task.status ?? "").toUpperCase();
17657
+ const status = safeStatus(task.status);
17658
+ const st = status.toUpperCase();
17341
17659
  rows += `<tr>
17342
17660
  <td class="mu">#${task.id}</td>
17343
17661
  <td><div style="font-weight:500">${esc(task.name)}</div><div class="mu sm el">${esc(task.prompt.substring(0, 120))}</div></td>
17344
17662
  <td><span class="tag">${esc(task.agent)}</span></td>
17345
- <td><span class="badge b-${task.status}">${st}</span></td>
17663
+ <td><span class="badge b-${status}">${st}</span></td>
17346
17664
  <td class="sm ${task.status === "running" ? "" : "mu"}">${formatDuration(task.startedAt, task.finishedAt)}</td>
17347
17665
  <td class="mu sm">${(task.retryCount ?? 0) > 0 ? task.retryCount : "-"}</td>
17348
17666
  <td>
@@ -17374,21 +17692,13 @@ app.get("/", async (c) => {
17374
17692
  });
17375
17693
  app.get("/templates", async (c) => {
17376
17694
  const templates = await TaskTemplateService.list(100);
17377
- for (const tmpl of templates) {
17378
- if (tmpl.enabled && tmpl.scheduleType === "cron" && tmpl.cronExpr) {
17379
- try {
17380
- const { getNextCronRun: getNextCronRun2 } = await Promise.resolve().then(() => (init_cron_parser(), cron_parser_exports));
17381
- tmpl.nextRunAt = getNextCronRun2(tmpl.cronExpr, Date.now());
17382
- } catch {
17383
- }
17384
- }
17385
- }
17386
17695
  const enabled = templates.filter((t) => t.enabled).length;
17387
17696
  const disabled = templates.length - enabled;
17388
17697
  let rows = "";
17389
17698
  for (const t of templates) {
17390
- const typeLabel = t.scheduleType === "cron" ? "Cron" : t.scheduleType === "recurring" ? "\u5FAA\u73AF" : "\u5B9A\u65F6";
17391
- const typeClass = "tag t-" + t.scheduleType;
17699
+ const scheduleType = ["cron", "recurring", "delayed"].includes(t.scheduleType) ? t.scheduleType : "unknown";
17700
+ const typeLabel = scheduleType === "cron" ? "Cron" : scheduleType === "recurring" ? "\u5FAA\u73AF" : scheduleType === "delayed" ? "\u5B9A\u65F6" : "\u672A\u77E5";
17701
+ const typeClass = "tag t-" + scheduleType;
17392
17702
  let rule = "-";
17393
17703
  if (t.scheduleType === "cron") rule = t.cronExpr || "-";
17394
17704
  else if (t.scheduleType === "recurring") rule = t.intervalMs ? `${Math.floor(t.intervalMs / 6e4)}\u5206\u949F` : "-";
@@ -17400,7 +17710,7 @@ app.get("/templates", async (c) => {
17400
17710
  <td><div style="font-weight:500">${esc(t.name)}</div><div class="mu sm el">${esc(t.prompt.substring(0, 100))}</div>
17401
17711
  <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>
17402
17712
  <td><span class="${typeClass}">${typeLabel}</span></td>
17403
- <td class="m sm">${rule}</td>
17713
+ <td class="m sm">${esc(rule)}</td>
17404
17714
  <td>${statusBadge}</td>
17405
17715
  <td class="sm">${t.lastRunAt ? timeAgo(t.lastRunAt) : "-"}</td>
17406
17716
  <td class="sm">${t.nextRunAt ? timeUntil(t.nextRunAt) : "-"}</td>
@@ -17428,7 +17738,8 @@ app.get("/templates", async (c) => {
17428
17738
  return c.html(renderLayout("\u5B9A\u65F6\u4EFB\u52A1", "templates", body));
17429
17739
  });
17430
17740
  app.get("/runs", async (c) => {
17431
- const page = Number(c.req.query("page") || "1");
17741
+ const page = parsePositiveInteger(c.req.query("page") || "1");
17742
+ if (page === null) return c.text("invalid page", 400);
17432
17743
  const limit = 50;
17433
17744
  const offset = (page - 1) * limit;
17434
17745
  const { taskRuns: tr, tasks: tk } = schema_exports;
@@ -17446,13 +17757,14 @@ app.get("/runs", async (c) => {
17446
17757
  childPid: tr.childPid,
17447
17758
  taskName: tk.name,
17448
17759
  taskAgent: tk.agent
17449
- }).from(tr).innerJoin(tk, eq(tr.taskId, tk.id)).orderBy(desc(tr.startedAt)).limit(limit).offset(offset);
17760
+ }).from(tr).innerJoin(tk, eq(tr.taskId, tk.id)).orderBy(desc(tr.startedAt), desc(tr.id)).limit(limit).offset(offset);
17450
17761
  const totalResult = await db.select({ count: sql`count(*)` }).from(tr);
17451
17762
  const total = Number(totalResult[0]?.count ?? 0);
17452
17763
  const totalPages = Math.ceil(total / limit);
17453
17764
  let rows = "";
17454
17765
  const logsHtml = [];
17455
17766
  for (const run of runs) {
17767
+ const status = safeStatus(run.status);
17456
17768
  const shortSession = run.sessionId ? run.sessionId.slice(4, 7) + "***" + run.sessionId.slice(-3) : "-";
17457
17769
  const logBtn = run.log ? `<button class="btn btn-sm" onclick="toggleLog(${run.id})">\u65E5\u5FD7</button>` : "";
17458
17770
  rows += `<tr>
@@ -17460,15 +17772,15 @@ app.get("/runs", async (c) => {
17460
17772
  <td><div style="font-weight:500">${esc(run.taskName)} <span class="mu">(#${run.taskId})</span></div>
17461
17773
  ${run.model ? `<div class="sm"><span class="tag">${esc(run.model)}</span></div>` : ""}</td>
17462
17774
  <td><span class="tag">${esc(run.taskAgent)}</span></td>
17463
- <td><span class="badge b-${run.status}">${(run.status ?? "").toUpperCase()}</span></td>
17775
+ <td><span class="badge b-${status}">${status.toUpperCase()}</span></td>
17464
17776
  <td class="sm">${formatDuration(run.startedAt, run.finishedAt)}</td>
17465
17777
  <td class="sm mu">${run.heartbeatAt ? timeAgo(run.heartbeatAt) : "-"}</td>
17466
17778
  <td><button class="btn btn-sm" onclick="showRunDetail(${run.id})">\u8BE6\u60C5</button>${logBtn}</td>
17467
17779
  </tr>`;
17468
17780
  if (run.log) {
17469
17781
  logsHtml.push(`<div id="log-${run.id}" style="display:none" class="mt8">
17470
- <div class="panel"><div class="ph"><h3>Run #${run.id} \u65E5\u5FD7 \u2014 ${run.taskName}</h3></div>
17471
- <div class="log-box">${run.log.replace(/</g, "&lt;")}</div></div></div>`);
17782
+ <div class="panel"><div class="ph"><h3>Run #${run.id} \u65E5\u5FD7 \u2014 ${esc(run.taskName)}</h3></div>
17783
+ <div class="log-box">${esc(run.log)}</div></div></div>`);
17472
17784
  }
17473
17785
  }
17474
17786
  let paging = `<div class="pn">`;
@@ -17494,17 +17806,18 @@ app.get("/runs", async (c) => {
17494
17806
  });
17495
17807
  app.get("/system", async (c) => {
17496
17808
  const config = loadConfig();
17809
+ const configPath = getConfigPath();
17497
17810
  const stats = await TaskService.stats({});
17498
17811
  const runningRuns = await TaskRunService.getAllRunningRuns();
17499
17812
  const templates = await TaskTemplateService.list(100);
17500
- const configExists = existsSync3(CONFIG_PATH);
17813
+ const configExists = existsSync3(configPath);
17501
17814
  let runRows = "";
17502
17815
  if (runningRuns.length > 0) {
17503
17816
  for (const run of runningRuns) {
17504
17817
  const shortS = run.sessionId ? run.sessionId.slice(4, 7) + "***" + run.sessionId.slice(-3) : "-";
17505
17818
  runRows += `<tr>
17506
17819
  <td class="mu">#${run.id}</td><td>#${run.taskId}</td>
17507
- <td class="m sm">${shortS}</td>
17820
+ <td class="m sm">${esc(shortS)}</td>
17508
17821
  <td class="sm">${esc(run.model) || "-"}</td>
17509
17822
  <td class="sm">${formatDate(run.startedAt)}</td>
17510
17823
  <td class="sm">${run.heartbeatAt ? timeAgo(run.heartbeatAt) : "-"}</td>
@@ -17529,19 +17842,14 @@ app.get("/system", async (c) => {
17529
17842
  <h3 style="margin:0 0 12px;font-size:14px">Scheduler \u914D\u7F6E</h3>
17530
17843
  <div class="form-row"><label>\u542F\u7528\u8C03\u5EA6</label><input type="checkbox" name="se" ${config.scheduler.enabled ? "checked" : ""}></div>
17531
17844
  <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>
17532
- <div class="form-row"><label>\u8FFD\u8D76\u6A21\u5F0F</label><select name="cu" style="width:100px">
17533
- <option value="next" ${config.scheduler.catchUp === "next" ? "selected" : ""}>next</option>
17534
- <option value="all" ${config.scheduler.catchUp === "all" ? "selected" : ""}>all</option>
17535
- <option value="latest" ${config.scheduler.catchUp === "latest" ? "selected" : ""}>latest</option>
17536
- </select></div>
17537
17845
  <div class="ir"><span class="ik">\u6D3B\u8DC3\u6A21\u677F</span><span class="iv">${templates.filter((t) => t.enabled).length} / ${templates.length}</span></div>
17538
17846
  </div>
17539
17847
  <div class="card">
17540
17848
  <h3 style="margin:0 0 12px;font-size:14px">Watchdog \u914D\u7F6E</h3>
17541
17849
  <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>
17542
- <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>
17850
+ <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>
17851
+ <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>
17543
17852
  <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>
17544
- <div class="ir"><span class="ik">\u65E5\u5FD7\u683C\u5F0F</span><span class="iv">${config.logging.format}</span></div>
17545
17853
  </div>
17546
17854
  </div>
17547
17855
  <div style="text-align:center;margin-bottom:24px">
@@ -17570,7 +17878,7 @@ app.get("/system", async (c) => {
17570
17878
 
17571
17879
  <div class="card mt16">
17572
17880
  <h3 style="margin:0 0 12px;font-size:14px">\u914D\u7F6E\u6587\u4EF6</h3>
17573
- <div class="ir"><span class="ik">\u8DEF\u5F84</span><span class="iv m sm">${CONFIG_PATH}</span></div>
17881
+ <div class="ir"><span class="ik">\u8DEF\u5F84</span><span class="iv m sm">${esc(configPath)}</span></div>
17574
17882
  <div class="ir"><span class="ik">\u6587\u4EF6\u5B58\u5728</span><span class="iv">${configFileStatus}</span></div>
17575
17883
  </div>
17576
17884
 
@@ -17582,46 +17890,61 @@ app.get("/system", async (c) => {
17582
17890
  return c.html(renderLayout("\u7CFB\u7EDF\u72B6\u6001", "system", body));
17583
17891
  });
17584
17892
  app.get("/api/tasks/:id", async (c) => {
17585
- const id = Number(c.req.param("id"));
17893
+ const id = parsePositiveInteger(c.req.param("id"));
17894
+ if (id === null) return c.json({ error: "invalid id" }, 400);
17586
17895
  const task = await TaskService.getById(id);
17587
17896
  if (!task) return c.json({ error: "not found" }, 404);
17588
17897
  const runs = await TaskRunService.listByTaskId(id);
17589
17898
  return c.json({ ...task, _runs: runs });
17590
17899
  });
17591
17900
  app.get("/api/runs/:id", async (c) => {
17592
- const id = Number(c.req.param("id"));
17901
+ const id = parsePositiveInteger(c.req.param("id"));
17902
+ if (id === null) return c.json({ error: "invalid id" }, 400);
17593
17903
  const run = await TaskRunService.getById(id);
17594
17904
  if (!run) return c.json({ error: "not found" }, 404);
17595
17905
  return c.json(run);
17596
17906
  });
17597
17907
  app.get("/api/templates/:id", async (c) => {
17598
- const id = Number(c.req.param("id"));
17908
+ const id = parsePositiveInteger(c.req.param("id"));
17909
+ if (id === null) return c.json({ error: "invalid id" }, 400);
17599
17910
  const tmpl = await TaskTemplateService.getById(id);
17600
17911
  if (!tmpl) return c.json({ error: "not found" }, 404);
17601
17912
  return c.json(tmpl);
17602
17913
  });
17603
17914
  app.post("/api/tasks/:id/retry", async (c) => {
17604
- await TaskService.retry(Number(c.req.param("id")));
17605
- return c.json({ success: true });
17915
+ const id = parsePositiveInteger(c.req.param("id"));
17916
+ if (id === null) return c.json({ error: "invalid id" }, 400);
17917
+ const task = await TaskService.retry(id);
17918
+ if (task) return c.json({ success: true });
17919
+ return await TaskService.getById(id) ? c.json({ error: "task status does not allow retry" }, 409) : c.json({ error: "not found" }, 404);
17606
17920
  });
17607
17921
  app.delete("/api/tasks/:id", async (c) => {
17608
- await TaskService.delete(Number(c.req.param("id")));
17609
- return c.json({ success: true });
17922
+ const id = parsePositiveInteger(c.req.param("id"));
17923
+ if (id === null) return c.json({ error: "invalid id" }, 400);
17924
+ const deleted = await TaskService.delete(id);
17925
+ return deleted ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
17610
17926
  });
17611
17927
  app.post("/api/templates/:id/enable", async (c) => {
17612
- const result = await TaskTemplateService.enable(Number(c.req.param("id")));
17613
- return c.json({ success: !!result });
17928
+ const id = parsePositiveInteger(c.req.param("id"));
17929
+ if (id === null) return c.json({ error: "invalid id" }, 400);
17930
+ const result = await TaskTemplateService.enable(id);
17931
+ return result ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
17614
17932
  });
17615
17933
  app.post("/api/templates/:id/disable", async (c) => {
17616
- const result = await TaskTemplateService.disable(Number(c.req.param("id")));
17617
- return c.json({ success: !!result });
17934
+ const id = parsePositiveInteger(c.req.param("id"));
17935
+ if (id === null) return c.json({ error: "invalid id" }, 400);
17936
+ const result = await TaskTemplateService.disable(id);
17937
+ return result ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
17618
17938
  });
17619
17939
  app.delete("/api/templates/:id", async (c) => {
17620
- const ok = await TaskTemplateService.delete(Number(c.req.param("id")));
17621
- return c.json({ success: ok });
17940
+ const id = parsePositiveInteger(c.req.param("id"));
17941
+ if (id === null) return c.json({ error: "invalid id" }, 400);
17942
+ const ok = await TaskTemplateService.delete(id);
17943
+ return ok ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
17622
17944
  });
17623
17945
  app.post("/api/templates/:id/trigger", async (c) => {
17624
- const id = Number(c.req.param("id"));
17946
+ const id = parsePositiveInteger(c.req.param("id"));
17947
+ if (id === null) return c.json({ error: "invalid id" }, 400);
17625
17948
  const tmpl = await TaskTemplateService.getById(id);
17626
17949
  if (!tmpl) return c.json({ error: "not found" }, 404);
17627
17950
  const task = await TaskService.add({
@@ -17633,7 +17956,10 @@ app.post("/api/templates/:id/trigger", async (c) => {
17633
17956
  category: tmpl.category,
17634
17957
  importance: tmpl.importance,
17635
17958
  urgency: tmpl.urgency,
17959
+ batchId: tmpl.batchId,
17636
17960
  maxRetries: tmpl.maxRetries,
17961
+ retryBackoffMs: tmpl.retryBackoffMs,
17962
+ timeoutMs: tmpl.timeoutMs,
17637
17963
  templateId: tmpl.id
17638
17964
  });
17639
17965
  return c.json({ success: true, taskId: task.id });
@@ -17648,22 +17974,29 @@ app.put("/api/config", async (c) => {
17648
17974
  const bW = body.worker ?? {};
17649
17975
  const bS = body.scheduler ?? {};
17650
17976
  const bD = body.watchdog ?? {};
17651
- const merged = { ...current, ...body, worker: { ...curW, ...bW }, scheduler: { ...curS, ...bS }, watchdog: { ...curD, ...bD } };
17652
- writeConfig(merged);
17977
+ const merged = {
17978
+ ...current,
17979
+ ...body,
17980
+ configVersion: 2,
17981
+ worker: { ...curW, ...bW },
17982
+ scheduler: { ...curS, ...bS },
17983
+ watchdog: { ...curD, ...bD }
17984
+ };
17985
+ writeConfig(validateConfig(merged));
17653
17986
  return c.json({ success: true });
17654
17987
  } catch (err) {
17655
- return c.json({ success: false, error: err instanceof Error ? err.message : String(err) });
17988
+ return c.json({ success: false, error: err instanceof Error ? err.message : String(err) }, 400);
17656
17989
  }
17657
17990
  });
17658
17991
  app.post("/api/database/clear", async (c) => {
17659
17992
  try {
17660
- const { tasks: tasks3, taskRuns: taskRuns3, taskTemplates: taskTemplates3 } = schema_exports;
17661
- await db.delete(taskRuns3);
17993
+ const { tasks: tasks3, taskRuns: taskRuns4, taskTemplates: taskTemplates3 } = schema_exports;
17994
+ await db.delete(taskRuns4);
17662
17995
  await db.delete(taskTemplates3);
17663
17996
  await db.delete(tasks3);
17664
17997
  return c.json({ success: true });
17665
17998
  } catch (err) {
17666
- return c.json({ success: false, error: err instanceof Error ? err.message : String(err) });
17999
+ return c.json({ success: false, error: err instanceof Error ? err.message : String(err) }, 500);
17667
18000
  }
17668
18001
  });
17669
18002
  var dashboardApp = app;