opencode-supertask 0.1.20 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
  }
@@ -16774,8 +16841,20 @@ var TaskService = class {
16774
16841
  return result.length;
16775
16842
  }
16776
16843
  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();
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();
16779
16858
  return result[0] || null;
16780
16859
  }
16781
16860
  static async retry(id, scope = {}) {
@@ -16787,7 +16866,8 @@ var TaskService = class {
16787
16866
  status: "pending",
16788
16867
  startedAt: null,
16789
16868
  finishedAt: null,
16790
- retryAfter: null
16869
+ retryAfter: null,
16870
+ retryCount: 0
16791
16871
  }).where(and(...conditions)).returning();
16792
16872
  return result[0] || null;
16793
16873
  }
@@ -16801,7 +16881,8 @@ var TaskService = class {
16801
16881
  status: "pending",
16802
16882
  startedAt: null,
16803
16883
  finishedAt: null,
16804
- retryAfter: null
16884
+ retryAfter: null,
16885
+ retryCount: 0
16805
16886
  }).where(and(...conditions)).returning();
16806
16887
  return result.length;
16807
16888
  }
@@ -16869,6 +16950,9 @@ var TaskService = class {
16869
16950
  }
16870
16951
  static async delete(id, scope = {}) {
16871
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));
16872
16956
  const result = await db.delete(tasks2).where(and(...conditions)).returning();
16873
16957
  return result.length > 0;
16874
16958
  }
@@ -16886,59 +16970,59 @@ var TaskService = class {
16886
16970
  };
16887
16971
 
16888
16972
  // src/core/services/task-run.service.ts
16889
- var { taskRuns: taskRuns2 } = schema_exports;
16973
+ var { taskRuns: taskRuns3 } = schema_exports;
16890
16974
  var TaskRunService = class {
16891
16975
  static async create(data) {
16892
- const result = await db.insert(taskRuns2).values(data).returning();
16976
+ const result = await db.insert(taskRuns3).values(data).returning();
16893
16977
  return result[0];
16894
16978
  }
16895
16979
  static async updateSessionId(id, sessionId) {
16896
- 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();
16897
16981
  return result[0] || null;
16898
16982
  }
16899
16983
  static async done(id, log) {
16900
- const result = await db.update(taskRuns2).set({
16984
+ const result = await db.update(taskRuns3).set({
16901
16985
  status: "done",
16902
16986
  finishedAt: /* @__PURE__ */ new Date(),
16903
16987
  log
16904
- }).where(eq(taskRuns2.id, id)).returning();
16988
+ }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
16905
16989
  return result[0] || null;
16906
16990
  }
16907
16991
  static async fail(id, log) {
16908
- const result = await db.update(taskRuns2).set({
16992
+ const result = await db.update(taskRuns3).set({
16909
16993
  status: "failed",
16910
16994
  finishedAt: /* @__PURE__ */ new Date(),
16911
16995
  log
16912
- }).where(eq(taskRuns2.id, id)).returning();
16996
+ }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
16913
16997
  return result[0] || null;
16914
16998
  }
16915
16999
  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();
17000
+ const result = await db.update(taskRuns3).set({ heartbeatAt: Date.now() }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
16917
17001
  return result[0] || null;
16918
17002
  }
16919
17003
  static async updatePid(id, workerPid, childPid) {
16920
- const result = await db.update(taskRuns2).set({
17004
+ const result = await db.update(taskRuns3).set({
16921
17005
  workerPid,
16922
17006
  childPid,
16923
17007
  lockedAt: Date.now(),
16924
17008
  lockedBy: `gateway-${process.pid}`
16925
- }).where(eq(taskRuns2.id, id)).returning();
17009
+ }).where(and(eq(taskRuns3.id, id), eq(taskRuns3.status, "running"))).returning();
16926
17010
  return result[0] || null;
16927
17011
  }
16928
17012
  static async getById(id) {
16929
- 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));
16930
17014
  return result[0] || null;
16931
17015
  }
16932
17016
  static async listByTaskId(taskId) {
16933
- 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));
16934
17018
  }
16935
17019
  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);
17020
+ const result = await db.select().from(taskRuns3).where(eq(taskRuns3.taskId, taskId)).orderBy(desc(taskRuns3.startedAt), desc(taskRuns3.id)).limit(1);
16937
17021
  return result[0] || null;
16938
17022
  }
16939
17023
  static async getLatestByTaskIds(taskIds) {
16940
17024
  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));
17025
+ const latestRuns = await db.select().from(taskRuns3).where(inArray(taskRuns3.taskId, taskIds)).orderBy(desc(taskRuns3.startedAt), desc(taskRuns3.id));
16942
17026
  const result = /* @__PURE__ */ new Map();
16943
17027
  for (const run of latestRuns) {
16944
17028
  if (!result.has(run.taskId)) {
@@ -16952,22 +17036,23 @@ var TaskRunService = class {
16952
17036
  const cutoffSec = Math.floor(cutoffMs / 1e3);
16953
17037
  const { tasks: tasksTable } = schema_exports;
16954
17038
  const result = await db.select({
16955
- runId: taskRuns2.id,
16956
- taskId: taskRuns2.taskId,
16957
- childPid: taskRuns2.childPid,
17039
+ runId: taskRuns3.id,
17040
+ taskId: taskRuns3.taskId,
17041
+ childPid: taskRuns3.childPid,
16958
17042
  taskRetryCount: tasksTable.retryCount,
16959
- taskMaxRetries: tasksTable.maxRetries
16960
- }).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(
16961
17046
  and(
16962
- eq(taskRuns2.status, "running"),
17047
+ eq(taskRuns3.status, "running"),
16963
17048
  or(
16964
17049
  and(
16965
- sql`${taskRuns2.heartbeatAt} IS NULL`,
16966
- sql`${taskRuns2.startedAt} < ${cutoffSec}`
17050
+ sql`${taskRuns3.heartbeatAt} IS NULL`,
17051
+ sql`${taskRuns3.startedAt} < ${cutoffSec}`
16967
17052
  ),
16968
17053
  and(
16969
- sql`${taskRuns2.heartbeatAt} IS NOT NULL`,
16970
- sql`${taskRuns2.heartbeatAt} < ${cutoffMs}`
17054
+ sql`${taskRuns3.heartbeatAt} IS NOT NULL`,
17055
+ sql`${taskRuns3.heartbeatAt} < ${cutoffMs}`
16971
17056
  )
16972
17057
  )
16973
17058
  )
@@ -16977,28 +17062,50 @@ var TaskRunService = class {
16977
17062
  taskId: row.taskId,
16978
17063
  childPid: row.childPid,
16979
17064
  taskRetryCount: row.taskRetryCount ?? 0,
16980
- taskMaxRetries: row.taskMaxRetries ?? 3
17065
+ taskMaxRetries: row.taskMaxRetries ?? 3,
17066
+ taskRetryBackoffMs: row.taskRetryBackoffMs ?? 3e4
16981
17067
  }));
16982
17068
  }
16983
17069
  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);
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);
16985
17071
  return result[0] || null;
16986
17072
  }
16987
17073
  static async deleteByTaskIds(taskIds) {
16988
17074
  if (taskIds.length === 0) return 0;
16989
- 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();
16990
17076
  return result.length;
16991
17077
  }
16992
17078
  static async getAllRunningRuns() {
16993
- return await db.select().from(taskRuns2).where(eq(taskRuns2.status, "running"));
17079
+ return await db.select().from(taskRuns3).where(eq(taskRuns3.status, "running"));
16994
17080
  }
16995
17081
  };
16996
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
+
16997
17104
  // src/core/services/task-template.service.ts
16998
- init_cron_parser();
16999
17105
  var { taskTemplates: taskTemplates2 } = schema_exports;
17000
17106
  var TaskTemplateService = class {
17001
17107
  static async create(data) {
17108
+ this.validate(data);
17002
17109
  const now = Date.now();
17003
17110
  const result = await db.insert(taskTemplates2).values({ ...data, createdAt: now, updatedAt: now }).returning();
17004
17111
  const tmpl = result[0];
@@ -17014,8 +17121,38 @@ var TaskTemplateService = class {
17014
17121
  }
17015
17122
  return tmpl;
17016
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
+ }
17017
17154
  static async list(limit = 50) {
17018
- 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);
17019
17156
  }
17020
17157
  static async getById(id) {
17021
17158
  const result = await db.select().from(taskTemplates2).where(eq(taskTemplates2.id, id));
@@ -17054,72 +17191,236 @@ var TaskTemplateService = class {
17054
17191
  };
17055
17192
 
17056
17193
  // src/gateway/config.ts
17057
- import { readFileSync, existsSync as existsSync2 } from "fs";
17194
+ import { existsSync as existsSync2, readFileSync } from "fs";
17058
17195
  import { homedir as homedir2 } from "os";
17059
17196
  import { join as join2 } from "path";
17060
17197
  var DEFAULT_CONFIG = {
17198
+ configVersion: 2,
17061
17199
  worker: {
17062
17200
  maxConcurrency: 2,
17063
17201
  pollIntervalMs: 1e3,
17064
17202
  heartbeatIntervalMs: 3e4,
17065
- taskTimeoutMs: 18e5
17203
+ taskTimeoutMs: 18e5,
17204
+ shutdownGracePeriodMs: 3e4
17066
17205
  },
17067
17206
  scheduler: {
17068
17207
  enabled: true,
17069
- checkIntervalMs: 1e3,
17070
- catchUp: "next"
17208
+ checkIntervalMs: 1e3
17071
17209
  },
17072
17210
  watchdog: {
17073
17211
  heartbeatTimeoutMs: 6e5,
17074
- cleanupIntervalMs: 6e4,
17212
+ checkIntervalMs: 6e4,
17213
+ cleanupIntervalMs: 864e5,
17075
17214
  retentionDays: 30
17076
17215
  },
17077
17216
  dashboard: {
17078
17217
  enabled: true,
17079
17218
  port: 4680
17080
- },
17081
- logging: {
17082
- level: "info",
17083
- format: "json"
17084
17219
  }
17085
17220
  };
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
- }
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`);
17101
17229
  }
17102
- 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;
17103
17239
  }
17104
- function loadConfig() {
17105
- if (!existsSync2(CONFIG_PATH)) {
17106
- return DEFAULT_CONFIG;
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;
17245
+ }
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");
17269
+ }
17270
+ if (checkIntervalMs > heartbeatTimeoutMs) {
17271
+ throw new Error("watchdog.checkIntervalMs \u4E0D\u80FD\u5927\u4E8E watchdog.heartbeatTimeoutMs");
17107
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 });
17108
17300
  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;
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}`);
17116
17305
  }
17117
17306
  }
17118
17307
 
17119
17308
  // src/web/index.tsx
17120
- 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";
17121
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
17122
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
+ });
17123
17424
  function formatDuration(startAt, endAt) {
17124
17425
  if (!startAt) return "-";
17125
17426
  const start = new Date(startAt).getTime();
@@ -17157,17 +17458,21 @@ function esc(s) {
17157
17458
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
17158
17459
  }
17159
17460
  function readCurrentConfig() {
17160
- if (!existsSync3(CONFIG_PATH)) return {};
17461
+ const configPath = getConfigPath();
17462
+ if (!existsSync3(configPath)) return {};
17161
17463
  try {
17162
- return JSON.parse(readFileSync2(CONFIG_PATH, "utf-8"));
17464
+ return JSON.parse(readFileSync2(configPath, "utf-8"));
17163
17465
  } catch {
17164
17466
  return {};
17165
17467
  }
17166
17468
  }
17167
17469
  function writeConfig(cfg) {
17168
- const dir = dirname2(CONFIG_PATH);
17470
+ const configPath = getConfigPath();
17471
+ const dir = dirname2(configPath);
17169
17472
  if (!existsSync3(dir)) mkdirSync2(dir, { recursive: true });
17170
- 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);
17171
17476
  }
17172
17477
  var SHARED_STYLES = html`
17173
17478
  <style>
@@ -17281,11 +17586,11 @@ async function saveConfig(){
17281
17586
  scheduler:{
17282
17587
  enabled:form.se.checked,
17283
17588
  checkIntervalMs:Number(form.si.value),
17284
- catchUp:form.cu.value,
17285
17589
  },
17286
17590
  watchdog:{
17287
17591
  heartbeatTimeoutMs:Number(form.wt.value)*1000,
17288
- cleanupIntervalMs:Number(form.wc.value)*1000,
17592
+ checkIntervalMs:Number(form.wci.value)*1000,
17593
+ cleanupIntervalMs:Number(form.wcl.value)*3600000,
17289
17594
  retentionDays:Number(form.rd.value),
17290
17595
  }
17291
17596
  };
@@ -17317,12 +17622,16 @@ async function saveConfig(){
17317
17622
  </body></html>`;
17318
17623
  }
17319
17624
  app.get("/", async (c) => {
17320
- 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);
17321
17628
  const statusFilter = c.req.query("status") || "";
17629
+ const parsedStatus = statusFilter ? parseTaskStatus(statusFilter) : null;
17630
+ if (statusFilter && !parsedStatus) return c.text("invalid status", 400);
17322
17631
  const limit = 50;
17323
17632
  const offset = (page - 1) * limit;
17324
17633
  const [tasks3, statsData] = await Promise.all([
17325
- TaskService.list({ limit, offset, ...statusFilter ? { status: statusFilter } : {} }),
17634
+ TaskService.list({ limit, offset, ...parsedStatus ? { status: parsedStatus } : {} }),
17326
17635
  TaskService.stats({})
17327
17636
  ]);
17328
17637
  const taskIds = tasks3.map((t) => t.id);
@@ -17345,12 +17654,13 @@ app.get("/", async (c) => {
17345
17654
  </div>`;
17346
17655
  let rows = "";
17347
17656
  for (const task of tasks3) {
17348
- const st = (task.status ?? "").toUpperCase();
17657
+ const status = safeStatus(task.status);
17658
+ const st = status.toUpperCase();
17349
17659
  rows += `<tr>
17350
17660
  <td class="mu">#${task.id}</td>
17351
17661
  <td><div style="font-weight:500">${esc(task.name)}</div><div class="mu sm el">${esc(task.prompt.substring(0, 120))}</div></td>
17352
17662
  <td><span class="tag">${esc(task.agent)}</span></td>
17353
- <td><span class="badge b-${task.status}">${st}</span></td>
17663
+ <td><span class="badge b-${status}">${st}</span></td>
17354
17664
  <td class="sm ${task.status === "running" ? "" : "mu"}">${formatDuration(task.startedAt, task.finishedAt)}</td>
17355
17665
  <td class="mu sm">${(task.retryCount ?? 0) > 0 ? task.retryCount : "-"}</td>
17356
17666
  <td>
@@ -17382,21 +17692,13 @@ app.get("/", async (c) => {
17382
17692
  });
17383
17693
  app.get("/templates", async (c) => {
17384
17694
  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
17695
  const enabled = templates.filter((t) => t.enabled).length;
17395
17696
  const disabled = templates.length - enabled;
17396
17697
  let rows = "";
17397
17698
  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;
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;
17400
17702
  let rule = "-";
17401
17703
  if (t.scheduleType === "cron") rule = t.cronExpr || "-";
17402
17704
  else if (t.scheduleType === "recurring") rule = t.intervalMs ? `${Math.floor(t.intervalMs / 6e4)}\u5206\u949F` : "-";
@@ -17408,7 +17710,7 @@ app.get("/templates", async (c) => {
17408
17710
  <td><div style="font-weight:500">${esc(t.name)}</div><div class="mu sm el">${esc(t.prompt.substring(0, 100))}</div>
17409
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>
17410
17712
  <td><span class="${typeClass}">${typeLabel}</span></td>
17411
- <td class="m sm">${rule}</td>
17713
+ <td class="m sm">${esc(rule)}</td>
17412
17714
  <td>${statusBadge}</td>
17413
17715
  <td class="sm">${t.lastRunAt ? timeAgo(t.lastRunAt) : "-"}</td>
17414
17716
  <td class="sm">${t.nextRunAt ? timeUntil(t.nextRunAt) : "-"}</td>
@@ -17436,7 +17738,8 @@ app.get("/templates", async (c) => {
17436
17738
  return c.html(renderLayout("\u5B9A\u65F6\u4EFB\u52A1", "templates", body));
17437
17739
  });
17438
17740
  app.get("/runs", async (c) => {
17439
- 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);
17440
17743
  const limit = 50;
17441
17744
  const offset = (page - 1) * limit;
17442
17745
  const { taskRuns: tr, tasks: tk } = schema_exports;
@@ -17454,13 +17757,14 @@ app.get("/runs", async (c) => {
17454
17757
  childPid: tr.childPid,
17455
17758
  taskName: tk.name,
17456
17759
  taskAgent: tk.agent
17457
- }).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);
17458
17761
  const totalResult = await db.select({ count: sql`count(*)` }).from(tr);
17459
17762
  const total = Number(totalResult[0]?.count ?? 0);
17460
17763
  const totalPages = Math.ceil(total / limit);
17461
17764
  let rows = "";
17462
17765
  const logsHtml = [];
17463
17766
  for (const run of runs) {
17767
+ const status = safeStatus(run.status);
17464
17768
  const shortSession = run.sessionId ? run.sessionId.slice(4, 7) + "***" + run.sessionId.slice(-3) : "-";
17465
17769
  const logBtn = run.log ? `<button class="btn btn-sm" onclick="toggleLog(${run.id})">\u65E5\u5FD7</button>` : "";
17466
17770
  rows += `<tr>
@@ -17468,15 +17772,15 @@ app.get("/runs", async (c) => {
17468
17772
  <td><div style="font-weight:500">${esc(run.taskName)} <span class="mu">(#${run.taskId})</span></div>
17469
17773
  ${run.model ? `<div class="sm"><span class="tag">${esc(run.model)}</span></div>` : ""}</td>
17470
17774
  <td><span class="tag">${esc(run.taskAgent)}</span></td>
17471
- <td><span class="badge b-${run.status}">${(run.status ?? "").toUpperCase()}</span></td>
17775
+ <td><span class="badge b-${status}">${status.toUpperCase()}</span></td>
17472
17776
  <td class="sm">${formatDuration(run.startedAt, run.finishedAt)}</td>
17473
17777
  <td class="sm mu">${run.heartbeatAt ? timeAgo(run.heartbeatAt) : "-"}</td>
17474
17778
  <td><button class="btn btn-sm" onclick="showRunDetail(${run.id})">\u8BE6\u60C5</button>${logBtn}</td>
17475
17779
  </tr>`;
17476
17780
  if (run.log) {
17477
17781
  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>`);
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>`);
17480
17784
  }
17481
17785
  }
17482
17786
  let paging = `<div class="pn">`;
@@ -17502,17 +17806,18 @@ app.get("/runs", async (c) => {
17502
17806
  });
17503
17807
  app.get("/system", async (c) => {
17504
17808
  const config = loadConfig();
17809
+ const configPath = getConfigPath();
17505
17810
  const stats = await TaskService.stats({});
17506
17811
  const runningRuns = await TaskRunService.getAllRunningRuns();
17507
17812
  const templates = await TaskTemplateService.list(100);
17508
- const configExists = existsSync3(CONFIG_PATH);
17813
+ const configExists = existsSync3(configPath);
17509
17814
  let runRows = "";
17510
17815
  if (runningRuns.length > 0) {
17511
17816
  for (const run of runningRuns) {
17512
17817
  const shortS = run.sessionId ? run.sessionId.slice(4, 7) + "***" + run.sessionId.slice(-3) : "-";
17513
17818
  runRows += `<tr>
17514
17819
  <td class="mu">#${run.id}</td><td>#${run.taskId}</td>
17515
- <td class="m sm">${shortS}</td>
17820
+ <td class="m sm">${esc(shortS)}</td>
17516
17821
  <td class="sm">${esc(run.model) || "-"}</td>
17517
17822
  <td class="sm">${formatDate(run.startedAt)}</td>
17518
17823
  <td class="sm">${run.heartbeatAt ? timeAgo(run.heartbeatAt) : "-"}</td>
@@ -17537,19 +17842,14 @@ app.get("/system", async (c) => {
17537
17842
  <h3 style="margin:0 0 12px;font-size:14px">Scheduler \u914D\u7F6E</h3>
17538
17843
  <div class="form-row"><label>\u542F\u7528\u8C03\u5EA6</label><input type="checkbox" name="se" ${config.scheduler.enabled ? "checked" : ""}></div>
17539
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>
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
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>
17546
17846
  </div>
17547
17847
  <div class="card">
17548
17848
  <h3 style="margin:0 0 12px;font-size:14px">Watchdog \u914D\u7F6E</h3>
17549
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>
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>
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>
17551
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>
17552
- <div class="ir"><span class="ik">\u65E5\u5FD7\u683C\u5F0F</span><span class="iv">${config.logging.format}</span></div>
17553
17853
  </div>
17554
17854
  </div>
17555
17855
  <div style="text-align:center;margin-bottom:24px">
@@ -17578,7 +17878,7 @@ app.get("/system", async (c) => {
17578
17878
 
17579
17879
  <div class="card mt16">
17580
17880
  <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>
17881
+ <div class="ir"><span class="ik">\u8DEF\u5F84</span><span class="iv m sm">${esc(configPath)}</span></div>
17582
17882
  <div class="ir"><span class="ik">\u6587\u4EF6\u5B58\u5728</span><span class="iv">${configFileStatus}</span></div>
17583
17883
  </div>
17584
17884
 
@@ -17590,46 +17890,61 @@ app.get("/system", async (c) => {
17590
17890
  return c.html(renderLayout("\u7CFB\u7EDF\u72B6\u6001", "system", body));
17591
17891
  });
17592
17892
  app.get("/api/tasks/:id", async (c) => {
17593
- 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);
17594
17895
  const task = await TaskService.getById(id);
17595
17896
  if (!task) return c.json({ error: "not found" }, 404);
17596
17897
  const runs = await TaskRunService.listByTaskId(id);
17597
17898
  return c.json({ ...task, _runs: runs });
17598
17899
  });
17599
17900
  app.get("/api/runs/:id", async (c) => {
17600
- 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);
17601
17903
  const run = await TaskRunService.getById(id);
17602
17904
  if (!run) return c.json({ error: "not found" }, 404);
17603
17905
  return c.json(run);
17604
17906
  });
17605
17907
  app.get("/api/templates/:id", async (c) => {
17606
- 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);
17607
17910
  const tmpl = await TaskTemplateService.getById(id);
17608
17911
  if (!tmpl) return c.json({ error: "not found" }, 404);
17609
17912
  return c.json(tmpl);
17610
17913
  });
17611
17914
  app.post("/api/tasks/:id/retry", async (c) => {
17612
- await TaskService.retry(Number(c.req.param("id")));
17613
- 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);
17614
17920
  });
17615
17921
  app.delete("/api/tasks/:id", async (c) => {
17616
- await TaskService.delete(Number(c.req.param("id")));
17617
- 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);
17618
17926
  });
17619
17927
  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 });
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);
17622
17932
  });
17623
17933
  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 });
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);
17626
17938
  });
17627
17939
  app.delete("/api/templates/:id", async (c) => {
17628
- const ok = await TaskTemplateService.delete(Number(c.req.param("id")));
17629
- 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);
17630
17944
  });
17631
17945
  app.post("/api/templates/:id/trigger", async (c) => {
17632
- 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);
17633
17948
  const tmpl = await TaskTemplateService.getById(id);
17634
17949
  if (!tmpl) return c.json({ error: "not found" }, 404);
17635
17950
  const task = await TaskService.add({
@@ -17641,7 +17956,10 @@ app.post("/api/templates/:id/trigger", async (c) => {
17641
17956
  category: tmpl.category,
17642
17957
  importance: tmpl.importance,
17643
17958
  urgency: tmpl.urgency,
17959
+ batchId: tmpl.batchId,
17644
17960
  maxRetries: tmpl.maxRetries,
17961
+ retryBackoffMs: tmpl.retryBackoffMs,
17962
+ timeoutMs: tmpl.timeoutMs,
17645
17963
  templateId: tmpl.id
17646
17964
  });
17647
17965
  return c.json({ success: true, taskId: task.id });
@@ -17656,22 +17974,29 @@ app.put("/api/config", async (c) => {
17656
17974
  const bW = body.worker ?? {};
17657
17975
  const bS = body.scheduler ?? {};
17658
17976
  const bD = body.watchdog ?? {};
17659
- const merged = { ...current, ...body, worker: { ...curW, ...bW }, scheduler: { ...curS, ...bS }, watchdog: { ...curD, ...bD } };
17660
- 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));
17661
17986
  return c.json({ success: true });
17662
17987
  } catch (err) {
17663
- 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);
17664
17989
  }
17665
17990
  });
17666
17991
  app.post("/api/database/clear", async (c) => {
17667
17992
  try {
17668
- const { tasks: tasks3, taskRuns: taskRuns3, taskTemplates: taskTemplates3 } = schema_exports;
17669
- await db.delete(taskRuns3);
17993
+ const { tasks: tasks3, taskRuns: taskRuns4, taskTemplates: taskTemplates3 } = schema_exports;
17994
+ await db.delete(taskRuns4);
17670
17995
  await db.delete(taskTemplates3);
17671
17996
  await db.delete(tasks3);
17672
17997
  return c.json({ success: true });
17673
17998
  } catch (err) {
17674
- 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);
17675
18000
  }
17676
18001
  });
17677
18002
  var dashboardApp = app;