opencode-supertask 0.1.20 → 0.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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";
@@ -16589,16 +16612,30 @@ function initDb() {
16589
16612
  id INTEGER PRIMARY KEY CHECK (id = 1),
16590
16613
  pid INTEGER NOT NULL,
16591
16614
  acquired_at INTEGER NOT NULL,
16592
- heartbeat_at INTEGER NOT NULL
16615
+ heartbeat_at INTEGER NOT NULL,
16616
+ ready_at INTEGER
16593
16617
  );
16594
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
+ }
16595
16623
  _db = drizzle(_sqlite, { schema: schema_exports });
16596
16624
  if (!_migrationRan) {
16597
- _migrationRan = true;
16598
16625
  try {
16599
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;
16600
16633
  } catch (err) {
16601
16634
  const msg = err instanceof Error ? err.message : String(err);
16635
+ _migrationRan = false;
16636
+ _sqlite.close();
16637
+ _sqlite = null;
16638
+ _db = null;
16602
16639
  console.error(`[supertask] migration failed: ${msg}`);
16603
16640
  throw new Error(`[supertask] DB migration failed: ${msg}`);
16604
16641
  }
@@ -16637,7 +16674,7 @@ function computeBackoff(retryCount, baseMs = 3e4, maxMs = MAX_BACKOFF_MS) {
16637
16674
  }
16638
16675
 
16639
16676
  // src/core/services/task.service.ts
16640
- var { tasks: tasks2 } = schema_exports;
16677
+ var { tasks: tasks2, taskRuns: taskRuns2 } = schema_exports;
16641
16678
  var TaskService = class {
16642
16679
  static buildScopeWhere(scope) {
16643
16680
  const conditions = [];
@@ -16647,9 +16684,27 @@ var TaskService = class {
16647
16684
  return conditions;
16648
16685
  }
16649
16686
  static async add(data) {
16687
+ this.validateNewTask(data);
16650
16688
  const result = await db.insert(tasks2).values(data).returning();
16651
16689
  return result[0];
16652
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
+ }
16653
16708
  static async next(scope = {}) {
16654
16709
  const baseConditions = [...this.buildScopeWhere(scope)];
16655
16710
  const nowMs = Date.now();
@@ -16672,7 +16727,7 @@ var TaskService = class {
16672
16727
  ),
16673
16728
  and(
16674
16729
  eq(tasks2.status, "failed"),
16675
- sql`${tasks2.retryCount} < ${tasks2.maxRetries}`,
16730
+ sql`${tasks2.retryCount} <= ${tasks2.maxRetries}`,
16676
16731
  retryAfterFilter
16677
16732
  )
16678
16733
  );
@@ -16686,7 +16741,8 @@ var TaskService = class {
16686
16741
  const allTasks = await db.select().from(tasks2).where(and(...conditions)).orderBy(
16687
16742
  desc(tasks2.urgency),
16688
16743
  desc(tasks2.importance),
16689
- asc(tasks2.createdAt)
16744
+ asc(tasks2.createdAt),
16745
+ asc(tasks2.id)
16690
16746
  );
16691
16747
  for (const task of allTasks) {
16692
16748
  if (task.dependsOn) {
@@ -16707,7 +16763,7 @@ var TaskService = class {
16707
16763
  eq(tasks2.status, "pending"),
16708
16764
  and(
16709
16765
  eq(tasks2.status, "failed"),
16710
- sql`${tasks2.retryCount} < ${tasks2.maxRetries}`
16766
+ sql`${tasks2.retryCount} <= ${tasks2.maxRetries}`
16711
16767
  )
16712
16768
  ),
16713
16769
  ...this.buildScopeWhere(scope)
@@ -16720,7 +16776,11 @@ var TaskService = class {
16720
16776
  return result[0] || null;
16721
16777
  }
16722
16778
  static async done(id, log, scope = {}) {
16723
- 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
+ ];
16724
16784
  const result = await db.update(tasks2).set({
16725
16785
  status: "done",
16726
16786
  finishedAt: /* @__PURE__ */ new Date(),
@@ -16731,17 +16791,24 @@ var TaskService = class {
16731
16791
  }
16732
16792
  static async fail(id, log, scope = {}, options) {
16733
16793
  const current = await this.getById(id, scope);
16734
- if (!current) return null;
16794
+ if (!current || current.status !== "running") return null;
16735
16795
  const newRetryCount = (current.retryCount ?? 0) + 1;
16736
16796
  const maxRetries = current.maxRetries ?? 3;
16737
- const isDeadLetter = options?.setDeadLetter ?? newRetryCount >= maxRetries;
16738
- 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
+ ];
16739
16803
  const result = await db.update(tasks2).set({
16740
16804
  status: isDeadLetter ? "dead_letter" : "failed",
16741
16805
  finishedAt: /* @__PURE__ */ new Date(),
16742
16806
  resultLog: log,
16743
16807
  retryCount: newRetryCount,
16744
- 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
+ )
16745
16812
  }).where(and(...conditions)).returning();
16746
16813
  return result[0] || null;
16747
16814
  }
@@ -16773,9 +16840,38 @@ var TaskService = class {
16773
16840
  ).returning();
16774
16841
  return result.length;
16775
16842
  }
16843
+ static async resetOrphanRunningToPending() {
16844
+ const result = await db.update(tasks2).set({
16845
+ status: "pending",
16846
+ startedAt: null,
16847
+ finishedAt: null
16848
+ }).where(
16849
+ and(
16850
+ eq(tasks2.status, "running"),
16851
+ sql`NOT EXISTS (
16852
+ SELECT 1 FROM ${taskRuns2}
16853
+ WHERE ${taskRuns2.taskId} = ${tasks2.id}
16854
+ AND ${taskRuns2.status} = 'running'
16855
+ )`
16856
+ )
16857
+ ).returning();
16858
+ return result.length;
16859
+ }
16776
16860
  static async cancel(id, scope = {}) {
16777
- const conditions = [eq(tasks2.id, id), ...this.buildScopeWhere(scope)];
16778
- const result = await db.update(tasks2).set({ status: "cancelled" }).where(and(...conditions)).returning();
16861
+ const conditions = [
16862
+ eq(tasks2.id, id),
16863
+ or(
16864
+ eq(tasks2.status, "pending"),
16865
+ eq(tasks2.status, "running"),
16866
+ eq(tasks2.status, "failed")
16867
+ ),
16868
+ ...this.buildScopeWhere(scope)
16869
+ ];
16870
+ const result = await db.update(tasks2).set({
16871
+ status: "cancelled",
16872
+ finishedAt: /* @__PURE__ */ new Date(),
16873
+ retryAfter: null
16874
+ }).where(and(...conditions)).returning();
16779
16875
  return result[0] || null;
16780
16876
  }
16781
16877
  static async retry(id, scope = {}) {
@@ -16787,7 +16883,8 @@ var TaskService = class {
16787
16883
  status: "pending",
16788
16884
  startedAt: null,
16789
16885
  finishedAt: null,
16790
- retryAfter: null
16886
+ retryAfter: null,
16887
+ retryCount: 0
16791
16888
  }).where(and(...conditions)).returning();
16792
16889
  return result[0] || null;
16793
16890
  }
@@ -16801,7 +16898,8 @@ var TaskService = class {
16801
16898
  status: "pending",
16802
16899
  startedAt: null,
16803
16900
  finishedAt: null,
16804
- retryAfter: null
16901
+ retryAfter: null,
16902
+ retryCount: 0
16805
16903
  }).where(and(...conditions)).returning();
16806
16904
  return result.length;
16807
16905
  }
@@ -16869,6 +16967,9 @@ var TaskService = class {
16869
16967
  }
16870
16968
  static async delete(id, scope = {}) {
16871
16969
  const conditions = [eq(tasks2.id, id), ...this.buildScopeWhere(scope)];
16970
+ const existing = await db.select({ id: tasks2.id }).from(tasks2).where(and(...conditions)).limit(1);
16971
+ if (!existing[0]) return false;
16972
+ await db.delete(taskRuns2).where(eq(taskRuns2.taskId, id));
16872
16973
  const result = await db.delete(tasks2).where(and(...conditions)).returning();
16873
16974
  return result.length > 0;
16874
16975
  }
@@ -16886,59 +16987,59 @@ var TaskService = class {
16886
16987
  };
16887
16988
 
16888
16989
  // src/core/services/task-run.service.ts
16889
- var { taskRuns: taskRuns2 } = schema_exports;
16990
+ var { taskRuns: taskRuns3 } = schema_exports;
16890
16991
  var TaskRunService = class {
16891
16992
  static async create(data) {
16892
- const result = await db.insert(taskRuns2).values(data).returning();
16993
+ const result = await db.insert(taskRuns3).values(data).returning();
16893
16994
  return result[0];
16894
16995
  }
16895
16996
  static async updateSessionId(id, sessionId) {
16896
- const result = await db.update(taskRuns2).set({ sessionId }).where(eq(taskRuns2.id, id)).returning();
16997
+ const result = await db.update(taskRuns3).set({ sessionId }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
16897
16998
  return result[0] || null;
16898
16999
  }
16899
17000
  static async done(id, log) {
16900
- const result = await db.update(taskRuns2).set({
17001
+ const result = await db.update(taskRuns3).set({
16901
17002
  status: "done",
16902
17003
  finishedAt: /* @__PURE__ */ new Date(),
16903
17004
  log
16904
- }).where(eq(taskRuns2.id, id)).returning();
17005
+ }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
16905
17006
  return result[0] || null;
16906
17007
  }
16907
17008
  static async fail(id, log) {
16908
- const result = await db.update(taskRuns2).set({
17009
+ const result = await db.update(taskRuns3).set({
16909
17010
  status: "failed",
16910
17011
  finishedAt: /* @__PURE__ */ new Date(),
16911
17012
  log
16912
- }).where(eq(taskRuns2.id, id)).returning();
17013
+ }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
16913
17014
  return result[0] || null;
16914
17015
  }
16915
17016
  static async heartbeat(id) {
16916
- const result = await db.update(taskRuns2).set({ heartbeatAt: Date.now() }).where(and(eq(taskRuns2.id, id), eq(taskRuns2.status, "running"))).returning();
17017
+ const result = await db.update(taskRuns3).set({ heartbeatAt: Date.now() }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
16917
17018
  return result[0] || null;
16918
17019
  }
16919
17020
  static async updatePid(id, workerPid, childPid) {
16920
- const result = await db.update(taskRuns2).set({
17021
+ const result = await db.update(taskRuns3).set({
16921
17022
  workerPid,
16922
17023
  childPid,
16923
17024
  lockedAt: Date.now(),
16924
17025
  lockedBy: `gateway-${process.pid}`
16925
- }).where(eq(taskRuns2.id, id)).returning();
17026
+ }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
16926
17027
  return result[0] || null;
16927
17028
  }
16928
17029
  static async getById(id) {
16929
- const result = await db.select().from(taskRuns2).where(eq(taskRuns2.id, id));
17030
+ const result = await db.select().from(taskRuns3).where(eq(taskRuns3.id, id));
16930
17031
  return result[0] || null;
16931
17032
  }
16932
17033
  static async listByTaskId(taskId) {
16933
- return await db.select().from(taskRuns2).where(eq(taskRuns2.taskId, taskId)).orderBy(desc(taskRuns2.startedAt), desc(taskRuns2.id));
17034
+ return await db.select().from(taskRuns3).where(eq(taskRuns3.taskId, taskId)).orderBy(desc(taskRuns3.startedAt), desc(taskRuns3.id));
16934
17035
  }
16935
17036
  static async getLatestByTaskId(taskId) {
16936
- const result = await db.select().from(taskRuns2).where(eq(taskRuns2.taskId, taskId)).orderBy(desc(taskRuns2.startedAt), desc(taskRuns2.id)).limit(1);
17037
+ const result = await db.select().from(taskRuns3).where(eq(taskRuns3.taskId, taskId)).orderBy(desc(taskRuns3.startedAt), desc(taskRuns3.id)).limit(1);
16937
17038
  return result[0] || null;
16938
17039
  }
16939
17040
  static async getLatestByTaskIds(taskIds) {
16940
17041
  if (taskIds.length === 0) return /* @__PURE__ */ new Map();
16941
- const latestRuns = await db.select().from(taskRuns2).where(inArray(taskRuns2.taskId, taskIds)).orderBy(desc(taskRuns2.startedAt));
17042
+ const latestRuns = await db.select().from(taskRuns3).where(inArray(taskRuns3.taskId, taskIds)).orderBy(desc(taskRuns3.startedAt), desc(taskRuns3.id));
16942
17043
  const result = /* @__PURE__ */ new Map();
16943
17044
  for (const run of latestRuns) {
16944
17045
  if (!result.has(run.taskId)) {
@@ -16952,22 +17053,23 @@ var TaskRunService = class {
16952
17053
  const cutoffSec = Math.floor(cutoffMs / 1e3);
16953
17054
  const { tasks: tasksTable } = schema_exports;
16954
17055
  const result = await db.select({
16955
- runId: taskRuns2.id,
16956
- taskId: taskRuns2.taskId,
16957
- childPid: taskRuns2.childPid,
17056
+ runId: taskRuns3.id,
17057
+ taskId: taskRuns3.taskId,
17058
+ childPid: taskRuns3.childPid,
16958
17059
  taskRetryCount: tasksTable.retryCount,
16959
- taskMaxRetries: tasksTable.maxRetries
16960
- }).from(taskRuns2).innerJoin(tasksTable, eq(taskRuns2.taskId, tasksTable.id)).where(
17060
+ taskMaxRetries: tasksTable.maxRetries,
17061
+ taskRetryBackoffMs: tasksTable.retryBackoffMs
17062
+ }).from(taskRuns3).innerJoin(tasksTable, eq(taskRuns3.taskId, tasksTable.id)).where(
16961
17063
  and(
16962
- eq(taskRuns2.status, "running"),
17064
+ eq(taskRuns3.status, "running"),
16963
17065
  or(
16964
17066
  and(
16965
- sql`${taskRuns2.heartbeatAt} IS NULL`,
16966
- sql`${taskRuns2.startedAt} < ${cutoffSec}`
17067
+ sql`${taskRuns3.heartbeatAt} IS NULL`,
17068
+ sql`${taskRuns3.startedAt} < ${cutoffSec}`
16967
17069
  ),
16968
17070
  and(
16969
- sql`${taskRuns2.heartbeatAt} IS NOT NULL`,
16970
- sql`${taskRuns2.heartbeatAt} < ${cutoffMs}`
17071
+ sql`${taskRuns3.heartbeatAt} IS NOT NULL`,
17072
+ sql`${taskRuns3.heartbeatAt} < ${cutoffMs}`
16971
17073
  )
16972
17074
  )
16973
17075
  )
@@ -16977,28 +17079,50 @@ var TaskRunService = class {
16977
17079
  taskId: row.taskId,
16978
17080
  childPid: row.childPid,
16979
17081
  taskRetryCount: row.taskRetryCount ?? 0,
16980
- taskMaxRetries: row.taskMaxRetries ?? 3
17082
+ taskMaxRetries: row.taskMaxRetries ?? 3,
17083
+ taskRetryBackoffMs: row.taskRetryBackoffMs ?? 3e4
16981
17084
  }));
16982
17085
  }
16983
17086
  static async getRunningRunByTaskId(taskId) {
16984
- 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);
17087
+ 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);
16985
17088
  return result[0] || null;
16986
17089
  }
16987
17090
  static async deleteByTaskIds(taskIds) {
16988
17091
  if (taskIds.length === 0) return 0;
16989
- const result = await db.delete(taskRuns2).where(inArray(taskRuns2.taskId, taskIds)).returning();
17092
+ const result = await db.delete(taskRuns3).where(inArray(taskRuns3.taskId, taskIds)).returning();
16990
17093
  return result.length;
16991
17094
  }
16992
17095
  static async getAllRunningRuns() {
16993
- return await db.select().from(taskRuns2).where(eq(taskRuns2.status, "running"));
17096
+ return await db.select().from(taskRuns3).where(eq(taskRuns3.status, "running"));
16994
17097
  }
16995
17098
  };
16996
17099
 
17100
+ // src/core/cron-parser.ts
17101
+ var import_cron_parser = __toESM(require_dist(), 1);
17102
+ function getNextCronRun(expr, afterMs) {
17103
+ try {
17104
+ const fromDate = afterMs != null ? new Date(afterMs) : /* @__PURE__ */ new Date();
17105
+ const interval = import_cron_parser.CronExpressionParser.parse(expr, { currentDate: fromDate });
17106
+ const next = interval.next();
17107
+ return next.getTime();
17108
+ } catch {
17109
+ return null;
17110
+ }
17111
+ }
17112
+ function isValidCronExpr(expr) {
17113
+ try {
17114
+ import_cron_parser.CronExpressionParser.parse(expr);
17115
+ return true;
17116
+ } catch {
17117
+ return false;
17118
+ }
17119
+ }
17120
+
16997
17121
  // src/core/services/task-template.service.ts
16998
- init_cron_parser();
16999
17122
  var { taskTemplates: taskTemplates2 } = schema_exports;
17000
17123
  var TaskTemplateService = class {
17001
17124
  static async create(data) {
17125
+ this.validate(data);
17002
17126
  const now = Date.now();
17003
17127
  const result = await db.insert(taskTemplates2).values({ ...data, createdAt: now, updatedAt: now }).returning();
17004
17128
  const tmpl = result[0];
@@ -17014,8 +17138,38 @@ var TaskTemplateService = class {
17014
17138
  }
17015
17139
  return tmpl;
17016
17140
  }
17141
+ static validate(data) {
17142
+ if (!data.name.trim()) throw new Error("name \u4E0D\u80FD\u4E3A\u7A7A");
17143
+ if (!data.agent.trim()) throw new Error("agent \u4E0D\u80FD\u4E3A\u7A7A");
17144
+ if (!data.prompt.trim()) throw new Error("prompt \u4E0D\u80FD\u4E3A\u7A7A");
17145
+ const scheduleType = data.scheduleType;
17146
+ if (!["cron", "delayed", "recurring"].includes(scheduleType)) {
17147
+ throw new Error("scheduleType \u5FC5\u987B\u662F cron\u3001delayed \u6216 recurring");
17148
+ }
17149
+ if (scheduleType === "cron" && (!data.cronExpr || !isValidCronExpr(data.cronExpr))) {
17150
+ throw new Error("cronExpr \u7F3A\u5931\u6216\u683C\u5F0F\u65E0\u6548");
17151
+ }
17152
+ if (scheduleType === "recurring" && (!Number.isInteger(data.intervalMs) || (data.intervalMs ?? 0) <= 0)) {
17153
+ throw new Error("intervalMs \u5FC5\u987B\u662F\u6B63\u6574\u6570");
17154
+ }
17155
+ if (scheduleType === "delayed" && (!Number.isInteger(data.runAt) || (data.runAt ?? 0) <= 0)) {
17156
+ throw new Error("runAt \u5FC5\u987B\u662F\u6B63\u6574\u6570\u65F6\u95F4\u6233");
17157
+ }
17158
+ this.validateInteger("importance", data.importance, 1, 5);
17159
+ this.validateInteger("urgency", data.urgency, 1, 5);
17160
+ this.validateInteger("maxInstances", data.maxInstances, 1, 1e3);
17161
+ this.validateInteger("maxRetries", data.maxRetries, 0, 1e3);
17162
+ this.validateInteger("retryBackoffMs", data.retryBackoffMs, 0, 864e5);
17163
+ this.validateInteger("timeoutMs", data.timeoutMs, 1e3, 6048e5);
17164
+ }
17165
+ static validateInteger(name, value, min, max) {
17166
+ if (value === void 0 || value === null) return;
17167
+ if (!Number.isInteger(value) || value < min || value > max) {
17168
+ throw new Error(`${name} \u5FC5\u987B\u662F ${min} \u5230 ${max} \u4E4B\u95F4\u7684\u6574\u6570`);
17169
+ }
17170
+ }
17017
17171
  static async list(limit = 50) {
17018
- return await db.select().from(taskTemplates2).orderBy(desc(taskTemplates2.createdAt)).limit(limit);
17172
+ return await db.select().from(taskTemplates2).orderBy(desc(taskTemplates2.createdAt), desc(taskTemplates2.id)).limit(limit);
17019
17173
  }
17020
17174
  static async getById(id) {
17021
17175
  const result = await db.select().from(taskTemplates2).where(eq(taskTemplates2.id, id));
@@ -17054,72 +17208,236 @@ var TaskTemplateService = class {
17054
17208
  };
17055
17209
 
17056
17210
  // src/gateway/config.ts
17057
- import { readFileSync, existsSync as existsSync2 } from "fs";
17211
+ import { existsSync as existsSync2, readFileSync } from "fs";
17058
17212
  import { homedir as homedir2 } from "os";
17059
17213
  import { join as join2 } from "path";
17060
17214
  var DEFAULT_CONFIG = {
17215
+ configVersion: 2,
17061
17216
  worker: {
17062
17217
  maxConcurrency: 2,
17063
17218
  pollIntervalMs: 1e3,
17064
17219
  heartbeatIntervalMs: 3e4,
17065
- taskTimeoutMs: 18e5
17220
+ taskTimeoutMs: 18e5,
17221
+ shutdownGracePeriodMs: 3e4
17066
17222
  },
17067
17223
  scheduler: {
17068
17224
  enabled: true,
17069
- checkIntervalMs: 1e3,
17070
- catchUp: "next"
17225
+ checkIntervalMs: 1e3
17071
17226
  },
17072
17227
  watchdog: {
17073
17228
  heartbeatTimeoutMs: 6e5,
17074
- cleanupIntervalMs: 6e4,
17229
+ checkIntervalMs: 6e4,
17230
+ cleanupIntervalMs: 864e5,
17075
17231
  retentionDays: 30
17076
17232
  },
17077
17233
  dashboard: {
17078
17234
  enabled: true,
17079
17235
  port: 4680
17080
- },
17081
- logging: {
17082
- level: "info",
17083
- format: "json"
17084
17236
  }
17085
17237
  };
17086
- var CONFIG_PATH = join2(homedir2(), ".config/opencode/supertask.json");
17087
- function deepMerge(base, override) {
17088
- const result = { ...base };
17089
- for (const key of Object.keys(override)) {
17090
- const val = override[key];
17091
- if (val !== null && typeof val === "object" && !Array.isArray(val) && key in result) {
17092
- const baseVal = result[key];
17093
- if (baseVal !== null && typeof baseVal === "object" && !Array.isArray(baseVal)) {
17094
- result[key] = deepMerge(baseVal, val);
17095
- continue;
17096
- }
17097
- }
17098
- if (val !== void 0) {
17099
- result[key] = val;
17100
- }
17238
+ var DEFAULT_CONFIG_PATH = join2(homedir2(), ".config/opencode/supertask.json");
17239
+ function getConfigPath() {
17240
+ return process.env.SUPERTASK_CONFIG_PATH ?? DEFAULT_CONFIG_PATH;
17241
+ }
17242
+ function objectAt(value, path) {
17243
+ if (value === void 0) return {};
17244
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
17245
+ throw new Error(`${path} \u5FC5\u987B\u662F\u5BF9\u8C61`);
17101
17246
  }
17102
- return result;
17247
+ return value;
17248
+ }
17249
+ function integerAt(source, key, fallback, min, max, path) {
17250
+ const value = source[key];
17251
+ if (value === void 0) return fallback;
17252
+ if (typeof value !== "number" || !Number.isInteger(value) || value < min || value > max) {
17253
+ throw new Error(`${path}.${key} \u5FC5\u987B\u662F ${min} \u5230 ${max} \u4E4B\u95F4\u7684\u6574\u6570`);
17254
+ }
17255
+ return value;
17256
+ }
17257
+ function booleanAt(source, key, fallback, path) {
17258
+ const value = source[key];
17259
+ if (value === void 0) return fallback;
17260
+ if (typeof value !== "boolean") throw new Error(`${path}.${key} \u5FC5\u987B\u662F\u5E03\u5C14\u503C`);
17261
+ return value;
17103
17262
  }
17104
- function loadConfig() {
17105
- if (!existsSync2(CONFIG_PATH)) {
17106
- return DEFAULT_CONFIG;
17263
+ function validateConfig(input) {
17264
+ const root = objectAt(input, "config");
17265
+ const version2 = root.configVersion ?? 1;
17266
+ if (version2 !== 1 && version2 !== 2) {
17267
+ throw new Error("config.configVersion \u53EA\u652F\u6301 1 \u6216 2");
17268
+ }
17269
+ const worker = objectAt(root.worker, "worker");
17270
+ const scheduler = objectAt(root.scheduler, "scheduler");
17271
+ const watchdog = objectAt(root.watchdog, "watchdog");
17272
+ const dashboard = objectAt(root.dashboard, "dashboard");
17273
+ const heartbeatIntervalMs = integerAt(worker, "heartbeatIntervalMs", DEFAULT_CONFIG.worker.heartbeatIntervalMs, 1e3, 36e5, "worker");
17274
+ const heartbeatTimeoutMs = integerAt(watchdog, "heartbeatTimeoutMs", DEFAULT_CONFIG.watchdog.heartbeatTimeoutMs, 1e3, 864e5, "watchdog");
17275
+ let checkIntervalMs;
17276
+ let cleanupIntervalMs;
17277
+ if (version2 === 1 && watchdog.checkIntervalMs === void 0 && watchdog.cleanupIntervalMs !== void 0) {
17278
+ checkIntervalMs = integerAt(watchdog, "cleanupIntervalMs", DEFAULT_CONFIG.watchdog.checkIntervalMs, 1e3, 36e5, "watchdog");
17279
+ cleanupIntervalMs = DEFAULT_CONFIG.watchdog.cleanupIntervalMs;
17280
+ } else {
17281
+ checkIntervalMs = integerAt(watchdog, "checkIntervalMs", DEFAULT_CONFIG.watchdog.checkIntervalMs, 1e3, 36e5, "watchdog");
17282
+ cleanupIntervalMs = integerAt(watchdog, "cleanupIntervalMs", DEFAULT_CONFIG.watchdog.cleanupIntervalMs, 6e4, 6048e5, "watchdog");
17283
+ }
17284
+ if (heartbeatIntervalMs >= heartbeatTimeoutMs) {
17285
+ throw new Error("worker.heartbeatIntervalMs \u5FC5\u987B\u5C0F\u4E8E watchdog.heartbeatTimeoutMs");
17286
+ }
17287
+ if (checkIntervalMs > heartbeatTimeoutMs) {
17288
+ throw new Error("watchdog.checkIntervalMs \u4E0D\u80FD\u5927\u4E8E watchdog.heartbeatTimeoutMs");
17107
17289
  }
17290
+ return {
17291
+ configVersion: 2,
17292
+ worker: {
17293
+ maxConcurrency: integerAt(worker, "maxConcurrency", DEFAULT_CONFIG.worker.maxConcurrency, 1, 64, "worker"),
17294
+ pollIntervalMs: integerAt(worker, "pollIntervalMs", DEFAULT_CONFIG.worker.pollIntervalMs, 50, 6e4, "worker"),
17295
+ heartbeatIntervalMs,
17296
+ taskTimeoutMs: integerAt(worker, "taskTimeoutMs", DEFAULT_CONFIG.worker.taskTimeoutMs, 1e3, 6048e5, "worker"),
17297
+ shutdownGracePeriodMs: integerAt(worker, "shutdownGracePeriodMs", DEFAULT_CONFIG.worker.shutdownGracePeriodMs, 0, 36e5, "worker")
17298
+ },
17299
+ scheduler: {
17300
+ enabled: booleanAt(scheduler, "enabled", DEFAULT_CONFIG.scheduler.enabled, "scheduler"),
17301
+ checkIntervalMs: integerAt(scheduler, "checkIntervalMs", DEFAULT_CONFIG.scheduler.checkIntervalMs, 100, 6e4, "scheduler")
17302
+ },
17303
+ watchdog: {
17304
+ heartbeatTimeoutMs,
17305
+ checkIntervalMs,
17306
+ cleanupIntervalMs,
17307
+ retentionDays: integerAt(watchdog, "retentionDays", DEFAULT_CONFIG.watchdog.retentionDays, 1, 3650, "watchdog")
17308
+ },
17309
+ dashboard: {
17310
+ enabled: booleanAt(dashboard, "enabled", DEFAULT_CONFIG.dashboard.enabled, "dashboard"),
17311
+ port: integerAt(dashboard, "port", DEFAULT_CONFIG.dashboard.port, 1, 65535, "dashboard")
17312
+ }
17313
+ };
17314
+ }
17315
+ function loadConfig(path = getConfigPath()) {
17316
+ if (!existsSync2(path)) return validateConfig({ configVersion: 2 });
17108
17317
  try {
17109
- const raw2 = readFileSync(CONFIG_PATH, "utf-8");
17110
- const userConfig = JSON.parse(raw2);
17111
- return deepMerge(DEFAULT_CONFIG, userConfig);
17112
- } catch (err) {
17113
- const msg = err instanceof Error ? err.message : String(err);
17114
- console.error(JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level: "warn", msg: "config load failed, using defaults", path: CONFIG_PATH, error: msg }));
17115
- return DEFAULT_CONFIG;
17318
+ return validateConfig(JSON.parse(readFileSync(path, "utf-8")));
17319
+ } catch (error) {
17320
+ const message = error instanceof Error ? error.message : String(error);
17321
+ throw new Error(`\u65E0\u6CD5\u8BFB\u53D6\u914D\u7F6E ${path}: ${message}`);
17116
17322
  }
17117
17323
  }
17118
17324
 
17119
17325
  // src/web/index.tsx
17120
- import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
17326
+ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync, mkdirSync as mkdirSync2, renameSync } from "fs";
17121
17327
  import { dirname as dirname2 } from "path";
17328
+
17329
+ // src/gateway/health.ts
17330
+ var LOCK_STALE_MS = 3e4;
17331
+ var state = null;
17332
+ function componentStatus(component, enabled, maxAgeMs, now) {
17333
+ const lastActivityAt = state?.lastActivityAt[component] ?? null;
17334
+ const ageMs = lastActivityAt == null ? null : Math.max(0, now - lastActivityAt);
17335
+ return {
17336
+ enabled,
17337
+ healthy: !enabled || ageMs != null && ageMs <= maxAgeMs,
17338
+ lastActivityAt,
17339
+ ageMs,
17340
+ maxAgeMs
17341
+ };
17342
+ }
17343
+ function getGatewayHealth(now = Date.now()) {
17344
+ const worker = componentStatus(
17345
+ "worker",
17346
+ true,
17347
+ Math.max((state?.config.workerPollIntervalMs ?? 1e3) * 5, 5e3),
17348
+ now
17349
+ );
17350
+ const scheduler = componentStatus(
17351
+ "scheduler",
17352
+ state?.config.schedulerEnabled ?? false,
17353
+ Math.max((state?.config.schedulerCheckIntervalMs ?? 1e3) * 5, 5e3),
17354
+ now
17355
+ );
17356
+ const watchdog = componentStatus(
17357
+ "watchdog",
17358
+ true,
17359
+ Math.max((state?.config.watchdogCheckIntervalMs ?? 6e4) * 3, 5e3),
17360
+ now
17361
+ );
17362
+ let lock = { pid: null, heartbeatAt: null, readyAt: null, ageMs: null, healthy: false };
17363
+ try {
17364
+ const row = sqlite.prepare(
17365
+ "SELECT pid, heartbeat_at, ready_at FROM gateway_lock WHERE id = 1"
17366
+ ).get();
17367
+ if (row) {
17368
+ const ageMs = Math.max(0, now - row.heartbeat_at);
17369
+ lock = {
17370
+ pid: row.pid,
17371
+ heartbeatAt: row.heartbeat_at,
17372
+ readyAt: row.ready_at,
17373
+ ageMs,
17374
+ healthy: row.pid === process.pid && row.ready_at != null && ageMs < LOCK_STALE_MS
17375
+ };
17376
+ }
17377
+ } catch {
17378
+ }
17379
+ const healthy = state != null && lock.healthy && worker.healthy && scheduler.healthy && watchdog.healthy;
17380
+ return {
17381
+ status: healthy ? "ok" : "degraded",
17382
+ pid: process.pid,
17383
+ uptimeMs: state ? Math.max(0, now - state.startedAt) : 0,
17384
+ lock,
17385
+ components: { worker, scheduler, watchdog }
17386
+ };
17387
+ }
17388
+
17389
+ // src/web/index.tsx
17122
17390
  var app = new Hono2();
17391
+ var TASK_STATUSES = /* @__PURE__ */ new Set([
17392
+ "pending",
17393
+ "running",
17394
+ "done",
17395
+ "failed",
17396
+ "dead_letter",
17397
+ "cancelled"
17398
+ ]);
17399
+ function parsePositiveInteger(value) {
17400
+ if (!/^\d+$/.test(value)) return null;
17401
+ const parsed = Number(value);
17402
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
17403
+ }
17404
+ function parseTaskStatus(value) {
17405
+ return TASK_STATUSES.has(value) ? value : null;
17406
+ }
17407
+ function safeStatus(value) {
17408
+ return value && TASK_STATUSES.has(value) ? value : "unknown";
17409
+ }
17410
+ app.use("*", async (c, next) => {
17411
+ await next();
17412
+ c.header("X-Content-Type-Options", "nosniff");
17413
+ c.header("X-Frame-Options", "DENY");
17414
+ c.header("Referrer-Policy", "no-referrer");
17415
+ });
17416
+ app.use("/api/*", async (c, next) => {
17417
+ if (["GET", "HEAD", "OPTIONS"].includes(c.req.method)) return next();
17418
+ const fetchSite = c.req.header("Sec-Fetch-Site");
17419
+ if (fetchSite && !["same-origin", "none"].includes(fetchSite)) {
17420
+ return c.json({ error: "cross-site request rejected" }, 403);
17421
+ }
17422
+ const origin = c.req.header("Origin");
17423
+ if (origin) {
17424
+ try {
17425
+ const originUrl = new URL(origin);
17426
+ const requestUrl = new URL(c.req.url);
17427
+ const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(originUrl.hostname);
17428
+ if (!loopback || originUrl.origin !== requestUrl.origin) {
17429
+ return c.json({ error: "cross-site request rejected" }, 403);
17430
+ }
17431
+ } catch {
17432
+ return c.json({ error: "invalid origin" }, 403);
17433
+ }
17434
+ }
17435
+ return next();
17436
+ });
17437
+ app.get("/health", (c) => {
17438
+ const health = getGatewayHealth();
17439
+ return c.json(health, health.status === "ok" ? 200 : 503);
17440
+ });
17123
17441
  function formatDuration(startAt, endAt) {
17124
17442
  if (!startAt) return "-";
17125
17443
  const start = new Date(startAt).getTime();
@@ -17157,17 +17475,21 @@ function esc(s) {
17157
17475
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
17158
17476
  }
17159
17477
  function readCurrentConfig() {
17160
- if (!existsSync3(CONFIG_PATH)) return {};
17478
+ const configPath = getConfigPath();
17479
+ if (!existsSync3(configPath)) return {};
17161
17480
  try {
17162
- return JSON.parse(readFileSync2(CONFIG_PATH, "utf-8"));
17481
+ return JSON.parse(readFileSync2(configPath, "utf-8"));
17163
17482
  } catch {
17164
17483
  return {};
17165
17484
  }
17166
17485
  }
17167
17486
  function writeConfig(cfg) {
17168
- const dir = dirname2(CONFIG_PATH);
17487
+ const configPath = getConfigPath();
17488
+ const dir = dirname2(configPath);
17169
17489
  if (!existsSync3(dir)) mkdirSync2(dir, { recursive: true });
17170
- writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2) + "\n");
17490
+ const tempPath = `${configPath}.${process.pid}.tmp`;
17491
+ writeFileSync(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
17492
+ renameSync(tempPath, configPath);
17171
17493
  }
17172
17494
  var SHARED_STYLES = html`
17173
17495
  <style>
@@ -17281,11 +17603,11 @@ async function saveConfig(){
17281
17603
  scheduler:{
17282
17604
  enabled:form.se.checked,
17283
17605
  checkIntervalMs:Number(form.si.value),
17284
- catchUp:form.cu.value,
17285
17606
  },
17286
17607
  watchdog:{
17287
17608
  heartbeatTimeoutMs:Number(form.wt.value)*1000,
17288
- cleanupIntervalMs:Number(form.wc.value)*1000,
17609
+ checkIntervalMs:Number(form.wci.value)*1000,
17610
+ cleanupIntervalMs:Number(form.wcl.value)*3600000,
17289
17611
  retentionDays:Number(form.rd.value),
17290
17612
  }
17291
17613
  };
@@ -17317,12 +17639,16 @@ async function saveConfig(){
17317
17639
  </body></html>`;
17318
17640
  }
17319
17641
  app.get("/", async (c) => {
17320
- const page = Number(c.req.query("page") || "1");
17642
+ const pageParam = c.req.query("page") || "1";
17643
+ const page = parsePositiveInteger(pageParam);
17644
+ if (page === null) return c.text("invalid page", 400);
17321
17645
  const statusFilter = c.req.query("status") || "";
17646
+ const parsedStatus = statusFilter ? parseTaskStatus(statusFilter) : null;
17647
+ if (statusFilter && !parsedStatus) return c.text("invalid status", 400);
17322
17648
  const limit = 50;
17323
17649
  const offset = (page - 1) * limit;
17324
17650
  const [tasks3, statsData] = await Promise.all([
17325
- TaskService.list({ limit, offset, ...statusFilter ? { status: statusFilter } : {} }),
17651
+ TaskService.list({ limit, offset, ...parsedStatus ? { status: parsedStatus } : {} }),
17326
17652
  TaskService.stats({})
17327
17653
  ]);
17328
17654
  const taskIds = tasks3.map((t) => t.id);
@@ -17345,12 +17671,13 @@ app.get("/", async (c) => {
17345
17671
  </div>`;
17346
17672
  let rows = "";
17347
17673
  for (const task of tasks3) {
17348
- const st = (task.status ?? "").toUpperCase();
17674
+ const status = safeStatus(task.status);
17675
+ const st = status.toUpperCase();
17349
17676
  rows += `<tr>
17350
17677
  <td class="mu">#${task.id}</td>
17351
17678
  <td><div style="font-weight:500">${esc(task.name)}</div><div class="mu sm el">${esc(task.prompt.substring(0, 120))}</div></td>
17352
17679
  <td><span class="tag">${esc(task.agent)}</span></td>
17353
- <td><span class="badge b-${task.status}">${st}</span></td>
17680
+ <td><span class="badge b-${status}">${st}</span></td>
17354
17681
  <td class="sm ${task.status === "running" ? "" : "mu"}">${formatDuration(task.startedAt, task.finishedAt)}</td>
17355
17682
  <td class="mu sm">${(task.retryCount ?? 0) > 0 ? task.retryCount : "-"}</td>
17356
17683
  <td>
@@ -17382,21 +17709,13 @@ app.get("/", async (c) => {
17382
17709
  });
17383
17710
  app.get("/templates", async (c) => {
17384
17711
  const templates = await TaskTemplateService.list(100);
17385
- for (const tmpl of templates) {
17386
- if (tmpl.enabled && tmpl.scheduleType === "cron" && tmpl.cronExpr) {
17387
- try {
17388
- const { getNextCronRun: getNextCronRun2 } = await Promise.resolve().then(() => (init_cron_parser(), cron_parser_exports));
17389
- tmpl.nextRunAt = getNextCronRun2(tmpl.cronExpr, Date.now());
17390
- } catch {
17391
- }
17392
- }
17393
- }
17394
17712
  const enabled = templates.filter((t) => t.enabled).length;
17395
17713
  const disabled = templates.length - enabled;
17396
17714
  let rows = "";
17397
17715
  for (const t of templates) {
17398
- const typeLabel = t.scheduleType === "cron" ? "Cron" : t.scheduleType === "recurring" ? "\u5FAA\u73AF" : "\u5B9A\u65F6";
17399
- const typeClass = "tag t-" + t.scheduleType;
17716
+ const scheduleType = ["cron", "recurring", "delayed"].includes(t.scheduleType) ? t.scheduleType : "unknown";
17717
+ const typeLabel = scheduleType === "cron" ? "Cron" : scheduleType === "recurring" ? "\u5FAA\u73AF" : scheduleType === "delayed" ? "\u5B9A\u65F6" : "\u672A\u77E5";
17718
+ const typeClass = "tag t-" + scheduleType;
17400
17719
  let rule = "-";
17401
17720
  if (t.scheduleType === "cron") rule = t.cronExpr || "-";
17402
17721
  else if (t.scheduleType === "recurring") rule = t.intervalMs ? `${Math.floor(t.intervalMs / 6e4)}\u5206\u949F` : "-";
@@ -17408,7 +17727,7 @@ app.get("/templates", async (c) => {
17408
17727
  <td><div style="font-weight:500">${esc(t.name)}</div><div class="mu sm el">${esc(t.prompt.substring(0, 100))}</div>
17409
17728
  <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>
17410
17729
  <td><span class="${typeClass}">${typeLabel}</span></td>
17411
- <td class="m sm">${rule}</td>
17730
+ <td class="m sm">${esc(rule)}</td>
17412
17731
  <td>${statusBadge}</td>
17413
17732
  <td class="sm">${t.lastRunAt ? timeAgo(t.lastRunAt) : "-"}</td>
17414
17733
  <td class="sm">${t.nextRunAt ? timeUntil(t.nextRunAt) : "-"}</td>
@@ -17436,7 +17755,8 @@ app.get("/templates", async (c) => {
17436
17755
  return c.html(renderLayout("\u5B9A\u65F6\u4EFB\u52A1", "templates", body));
17437
17756
  });
17438
17757
  app.get("/runs", async (c) => {
17439
- const page = Number(c.req.query("page") || "1");
17758
+ const page = parsePositiveInteger(c.req.query("page") || "1");
17759
+ if (page === null) return c.text("invalid page", 400);
17440
17760
  const limit = 50;
17441
17761
  const offset = (page - 1) * limit;
17442
17762
  const { taskRuns: tr, tasks: tk } = schema_exports;
@@ -17454,13 +17774,14 @@ app.get("/runs", async (c) => {
17454
17774
  childPid: tr.childPid,
17455
17775
  taskName: tk.name,
17456
17776
  taskAgent: tk.agent
17457
- }).from(tr).innerJoin(tk, eq(tr.taskId, tk.id)).orderBy(desc(tr.startedAt)).limit(limit).offset(offset);
17777
+ }).from(tr).innerJoin(tk, eq(tr.taskId, tk.id)).orderBy(desc(tr.startedAt), desc(tr.id)).limit(limit).offset(offset);
17458
17778
  const totalResult = await db.select({ count: sql`count(*)` }).from(tr);
17459
17779
  const total = Number(totalResult[0]?.count ?? 0);
17460
17780
  const totalPages = Math.ceil(total / limit);
17461
17781
  let rows = "";
17462
17782
  const logsHtml = [];
17463
17783
  for (const run of runs) {
17784
+ const status = safeStatus(run.status);
17464
17785
  const shortSession = run.sessionId ? run.sessionId.slice(4, 7) + "***" + run.sessionId.slice(-3) : "-";
17465
17786
  const logBtn = run.log ? `<button class="btn btn-sm" onclick="toggleLog(${run.id})">\u65E5\u5FD7</button>` : "";
17466
17787
  rows += `<tr>
@@ -17468,15 +17789,15 @@ app.get("/runs", async (c) => {
17468
17789
  <td><div style="font-weight:500">${esc(run.taskName)} <span class="mu">(#${run.taskId})</span></div>
17469
17790
  ${run.model ? `<div class="sm"><span class="tag">${esc(run.model)}</span></div>` : ""}</td>
17470
17791
  <td><span class="tag">${esc(run.taskAgent)}</span></td>
17471
- <td><span class="badge b-${run.status}">${(run.status ?? "").toUpperCase()}</span></td>
17792
+ <td><span class="badge b-${status}">${status.toUpperCase()}</span></td>
17472
17793
  <td class="sm">${formatDuration(run.startedAt, run.finishedAt)}</td>
17473
17794
  <td class="sm mu">${run.heartbeatAt ? timeAgo(run.heartbeatAt) : "-"}</td>
17474
17795
  <td><button class="btn btn-sm" onclick="showRunDetail(${run.id})">\u8BE6\u60C5</button>${logBtn}</td>
17475
17796
  </tr>`;
17476
17797
  if (run.log) {
17477
17798
  logsHtml.push(`<div id="log-${run.id}" style="display:none" class="mt8">
17478
- <div class="panel"><div class="ph"><h3>Run #${run.id} \u65E5\u5FD7 \u2014 ${run.taskName}</h3></div>
17479
- <div class="log-box">${run.log.replace(/</g, "&lt;")}</div></div></div>`);
17799
+ <div class="panel"><div class="ph"><h3>Run #${run.id} \u65E5\u5FD7 \u2014 ${esc(run.taskName)}</h3></div>
17800
+ <div class="log-box">${esc(run.log)}</div></div></div>`);
17480
17801
  }
17481
17802
  }
17482
17803
  let paging = `<div class="pn">`;
@@ -17502,17 +17823,18 @@ app.get("/runs", async (c) => {
17502
17823
  });
17503
17824
  app.get("/system", async (c) => {
17504
17825
  const config = loadConfig();
17826
+ const configPath = getConfigPath();
17505
17827
  const stats = await TaskService.stats({});
17506
17828
  const runningRuns = await TaskRunService.getAllRunningRuns();
17507
17829
  const templates = await TaskTemplateService.list(100);
17508
- const configExists = existsSync3(CONFIG_PATH);
17830
+ const configExists = existsSync3(configPath);
17509
17831
  let runRows = "";
17510
17832
  if (runningRuns.length > 0) {
17511
17833
  for (const run of runningRuns) {
17512
17834
  const shortS = run.sessionId ? run.sessionId.slice(4, 7) + "***" + run.sessionId.slice(-3) : "-";
17513
17835
  runRows += `<tr>
17514
17836
  <td class="mu">#${run.id}</td><td>#${run.taskId}</td>
17515
- <td class="m sm">${shortS}</td>
17837
+ <td class="m sm">${esc(shortS)}</td>
17516
17838
  <td class="sm">${esc(run.model) || "-"}</td>
17517
17839
  <td class="sm">${formatDate(run.startedAt)}</td>
17518
17840
  <td class="sm">${run.heartbeatAt ? timeAgo(run.heartbeatAt) : "-"}</td>
@@ -17537,19 +17859,14 @@ app.get("/system", async (c) => {
17537
17859
  <h3 style="margin:0 0 12px;font-size:14px">Scheduler \u914D\u7F6E</h3>
17538
17860
  <div class="form-row"><label>\u542F\u7528\u8C03\u5EA6</label><input type="checkbox" name="se" ${config.scheduler.enabled ? "checked" : ""}></div>
17539
17861
  <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>
17540
- <div class="form-row"><label>\u8FFD\u8D76\u6A21\u5F0F</label><select name="cu" style="width:100px">
17541
- <option value="next" ${config.scheduler.catchUp === "next" ? "selected" : ""}>next</option>
17542
- <option value="all" ${config.scheduler.catchUp === "all" ? "selected" : ""}>all</option>
17543
- <option value="latest" ${config.scheduler.catchUp === "latest" ? "selected" : ""}>latest</option>
17544
- </select></div>
17545
17862
  <div class="ir"><span class="ik">\u6D3B\u8DC3\u6A21\u677F</span><span class="iv">${templates.filter((t) => t.enabled).length} / ${templates.length}</span></div>
17546
17863
  </div>
17547
17864
  <div class="card">
17548
17865
  <h3 style="margin:0 0 12px;font-size:14px">Watchdog \u914D\u7F6E</h3>
17549
17866
  <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>
17550
- <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>
17867
+ <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>
17868
+ <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>
17551
17869
  <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>
17552
- <div class="ir"><span class="ik">\u65E5\u5FD7\u683C\u5F0F</span><span class="iv">${config.logging.format}</span></div>
17553
17870
  </div>
17554
17871
  </div>
17555
17872
  <div style="text-align:center;margin-bottom:24px">
@@ -17578,7 +17895,7 @@ app.get("/system", async (c) => {
17578
17895
 
17579
17896
  <div class="card mt16">
17580
17897
  <h3 style="margin:0 0 12px;font-size:14px">\u914D\u7F6E\u6587\u4EF6</h3>
17581
- <div class="ir"><span class="ik">\u8DEF\u5F84</span><span class="iv m sm">${CONFIG_PATH}</span></div>
17898
+ <div class="ir"><span class="ik">\u8DEF\u5F84</span><span class="iv m sm">${esc(configPath)}</span></div>
17582
17899
  <div class="ir"><span class="ik">\u6587\u4EF6\u5B58\u5728</span><span class="iv">${configFileStatus}</span></div>
17583
17900
  </div>
17584
17901
 
@@ -17590,46 +17907,61 @@ app.get("/system", async (c) => {
17590
17907
  return c.html(renderLayout("\u7CFB\u7EDF\u72B6\u6001", "system", body));
17591
17908
  });
17592
17909
  app.get("/api/tasks/:id", async (c) => {
17593
- const id = Number(c.req.param("id"));
17910
+ const id = parsePositiveInteger(c.req.param("id"));
17911
+ if (id === null) return c.json({ error: "invalid id" }, 400);
17594
17912
  const task = await TaskService.getById(id);
17595
17913
  if (!task) return c.json({ error: "not found" }, 404);
17596
17914
  const runs = await TaskRunService.listByTaskId(id);
17597
17915
  return c.json({ ...task, _runs: runs });
17598
17916
  });
17599
17917
  app.get("/api/runs/:id", async (c) => {
17600
- const id = Number(c.req.param("id"));
17918
+ const id = parsePositiveInteger(c.req.param("id"));
17919
+ if (id === null) return c.json({ error: "invalid id" }, 400);
17601
17920
  const run = await TaskRunService.getById(id);
17602
17921
  if (!run) return c.json({ error: "not found" }, 404);
17603
17922
  return c.json(run);
17604
17923
  });
17605
17924
  app.get("/api/templates/:id", async (c) => {
17606
- const id = Number(c.req.param("id"));
17925
+ const id = parsePositiveInteger(c.req.param("id"));
17926
+ if (id === null) return c.json({ error: "invalid id" }, 400);
17607
17927
  const tmpl = await TaskTemplateService.getById(id);
17608
17928
  if (!tmpl) return c.json({ error: "not found" }, 404);
17609
17929
  return c.json(tmpl);
17610
17930
  });
17611
17931
  app.post("/api/tasks/:id/retry", async (c) => {
17612
- await TaskService.retry(Number(c.req.param("id")));
17613
- return c.json({ success: true });
17932
+ const id = parsePositiveInteger(c.req.param("id"));
17933
+ if (id === null) return c.json({ error: "invalid id" }, 400);
17934
+ const task = await TaskService.retry(id);
17935
+ if (task) return c.json({ success: true });
17936
+ return await TaskService.getById(id) ? c.json({ error: "task status does not allow retry" }, 409) : c.json({ error: "not found" }, 404);
17614
17937
  });
17615
17938
  app.delete("/api/tasks/:id", async (c) => {
17616
- await TaskService.delete(Number(c.req.param("id")));
17617
- return c.json({ success: true });
17939
+ const id = parsePositiveInteger(c.req.param("id"));
17940
+ if (id === null) return c.json({ error: "invalid id" }, 400);
17941
+ const deleted = await TaskService.delete(id);
17942
+ return deleted ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
17618
17943
  });
17619
17944
  app.post("/api/templates/:id/enable", async (c) => {
17620
- const result = await TaskTemplateService.enable(Number(c.req.param("id")));
17621
- return c.json({ success: !!result });
17945
+ const id = parsePositiveInteger(c.req.param("id"));
17946
+ if (id === null) return c.json({ error: "invalid id" }, 400);
17947
+ const result = await TaskTemplateService.enable(id);
17948
+ return result ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
17622
17949
  });
17623
17950
  app.post("/api/templates/:id/disable", async (c) => {
17624
- const result = await TaskTemplateService.disable(Number(c.req.param("id")));
17625
- return c.json({ success: !!result });
17951
+ const id = parsePositiveInteger(c.req.param("id"));
17952
+ if (id === null) return c.json({ error: "invalid id" }, 400);
17953
+ const result = await TaskTemplateService.disable(id);
17954
+ return result ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
17626
17955
  });
17627
17956
  app.delete("/api/templates/:id", async (c) => {
17628
- const ok = await TaskTemplateService.delete(Number(c.req.param("id")));
17629
- return c.json({ success: ok });
17957
+ const id = parsePositiveInteger(c.req.param("id"));
17958
+ if (id === null) return c.json({ error: "invalid id" }, 400);
17959
+ const ok = await TaskTemplateService.delete(id);
17960
+ return ok ? c.json({ success: true }) : c.json({ error: "not found" }, 404);
17630
17961
  });
17631
17962
  app.post("/api/templates/:id/trigger", async (c) => {
17632
- const id = Number(c.req.param("id"));
17963
+ const id = parsePositiveInteger(c.req.param("id"));
17964
+ if (id === null) return c.json({ error: "invalid id" }, 400);
17633
17965
  const tmpl = await TaskTemplateService.getById(id);
17634
17966
  if (!tmpl) return c.json({ error: "not found" }, 404);
17635
17967
  const task = await TaskService.add({
@@ -17641,7 +17973,10 @@ app.post("/api/templates/:id/trigger", async (c) => {
17641
17973
  category: tmpl.category,
17642
17974
  importance: tmpl.importance,
17643
17975
  urgency: tmpl.urgency,
17976
+ batchId: tmpl.batchId,
17644
17977
  maxRetries: tmpl.maxRetries,
17978
+ retryBackoffMs: tmpl.retryBackoffMs,
17979
+ timeoutMs: tmpl.timeoutMs,
17645
17980
  templateId: tmpl.id
17646
17981
  });
17647
17982
  return c.json({ success: true, taskId: task.id });
@@ -17656,22 +17991,29 @@ app.put("/api/config", async (c) => {
17656
17991
  const bW = body.worker ?? {};
17657
17992
  const bS = body.scheduler ?? {};
17658
17993
  const bD = body.watchdog ?? {};
17659
- const merged = { ...current, ...body, worker: { ...curW, ...bW }, scheduler: { ...curS, ...bS }, watchdog: { ...curD, ...bD } };
17660
- writeConfig(merged);
17994
+ const merged = {
17995
+ ...current,
17996
+ ...body,
17997
+ configVersion: 2,
17998
+ worker: { ...curW, ...bW },
17999
+ scheduler: { ...curS, ...bS },
18000
+ watchdog: { ...curD, ...bD }
18001
+ };
18002
+ writeConfig(validateConfig(merged));
17661
18003
  return c.json({ success: true });
17662
18004
  } catch (err) {
17663
- return c.json({ success: false, error: err instanceof Error ? err.message : String(err) });
18005
+ return c.json({ success: false, error: err instanceof Error ? err.message : String(err) }, 400);
17664
18006
  }
17665
18007
  });
17666
18008
  app.post("/api/database/clear", async (c) => {
17667
18009
  try {
17668
- const { tasks: tasks3, taskRuns: taskRuns3, taskTemplates: taskTemplates3 } = schema_exports;
17669
- await db.delete(taskRuns3);
18010
+ const { tasks: tasks3, taskRuns: taskRuns4, taskTemplates: taskTemplates3 } = schema_exports;
18011
+ await db.delete(taskRuns4);
17670
18012
  await db.delete(taskTemplates3);
17671
18013
  await db.delete(tasks3);
17672
18014
  return c.json({ success: true });
17673
18015
  } catch (err) {
17674
- return c.json({ success: false, error: err instanceof Error ? err.message : String(err) });
18016
+ return c.json({ success: false, error: err instanceof Error ? err.message : String(err) }, 500);
17675
18017
  }
17676
18018
  });
17677
18019
  var dashboardApp = app;