opencode-supertask 0.1.31 → 0.1.32-rc.1

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/cli/index.js CHANGED
@@ -9583,6 +9583,54 @@ var init_backoff = __esm({
9583
9583
  }
9584
9584
  });
9585
9585
 
9586
+ // src/core/task-working-directory.ts
9587
+ import { statSync } from "fs";
9588
+ import { isAbsolute } from "path";
9589
+ function validateTaskWorkingDirectory(cwd) {
9590
+ if (cwd == null) return;
9591
+ if (!cwd.trim()) {
9592
+ throw new InvalidTaskWorkingDirectoryError("\u4EFB\u52A1\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u80FD\u4E3A\u7A7A");
9593
+ }
9594
+ if (!isAbsolute(cwd)) {
9595
+ throw new InvalidTaskWorkingDirectoryError(`\u4EFB\u52A1\u5DE5\u4F5C\u76EE\u5F55\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84\uFF1A${cwd}`);
9596
+ }
9597
+ let stat;
9598
+ try {
9599
+ stat = statSync(cwd);
9600
+ } catch (error) {
9601
+ const detail = error instanceof Error ? error.message : String(error);
9602
+ throw new InvalidTaskWorkingDirectoryError(`\u4EFB\u52A1\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u65E0\u6CD5\u8BBF\u95EE\uFF1A${cwd}\uFF08${detail}\uFF09`);
9603
+ }
9604
+ if (!stat.isDirectory()) {
9605
+ throw new InvalidTaskWorkingDirectoryError(`\u4EFB\u52A1\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u662F\u76EE\u5F55\uFF1A${cwd}`);
9606
+ }
9607
+ }
9608
+ var InvalidTaskWorkingDirectoryError;
9609
+ var init_task_working_directory = __esm({
9610
+ "src/core/task-working-directory.ts"() {
9611
+ "use strict";
9612
+ InvalidTaskWorkingDirectoryError = class extends Error {
9613
+ constructor(message) {
9614
+ super(message);
9615
+ this.name = "InvalidTaskWorkingDirectoryError";
9616
+ }
9617
+ };
9618
+ }
9619
+ });
9620
+
9621
+ // src/core/task-batch.ts
9622
+ function normalizeTaskBatchId(batchId) {
9623
+ if (batchId == null) return batchId;
9624
+ return batchId.trim() || null;
9625
+ }
9626
+ var TASK_BATCH_TRIM_CHARACTERS;
9627
+ var init_task_batch = __esm({
9628
+ "src/core/task-batch.ts"() {
9629
+ "use strict";
9630
+ TASK_BATCH_TRIM_CHARACTERS = " \n\v\f\r\xA0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF";
9631
+ }
9632
+ });
9633
+
9586
9634
  // src/core/services/task.service.ts
9587
9635
  function hasNoExecutableDependents() {
9588
9636
  return sql`NOT EXISTS (
@@ -9636,6 +9684,8 @@ var init_task_service = __esm({
9636
9684
  init_db2();
9637
9685
  init_drizzle_orm();
9638
9686
  init_backoff();
9687
+ init_task_working_directory();
9688
+ init_task_batch();
9639
9689
  ({ tasks: tasks2, taskRuns: taskRuns2 } = schema_exports);
9640
9690
  cleanupInvocation = 0;
9641
9691
  TaskDeletionConflictError = class extends Error {
@@ -9653,34 +9703,75 @@ var init_task_service = __esm({
9653
9703
  return conditions;
9654
9704
  }
9655
9705
  static async add(data) {
9656
- this.validateNewTask(data);
9706
+ const normalizedData = {
9707
+ ...data,
9708
+ batchId: normalizeTaskBatchId(data.batchId)
9709
+ };
9710
+ this.validateNewTask(normalizedData);
9657
9711
  return db.transaction((tx) => {
9658
- if (data.dependsOn != null) {
9712
+ if (normalizedData.dependsOn != null) {
9659
9713
  const dependency = tx.select({
9660
9714
  id: tasks2.id,
9661
9715
  cwd: tasks2.cwd,
9662
9716
  status: tasks2.status,
9663
9717
  retryCount: tasks2.retryCount,
9664
9718
  maxRetries: tasks2.maxRetries
9665
- }).from(tasks2).where(eq(tasks2.id, data.dependsOn)).get();
9719
+ }).from(tasks2).where(eq(tasks2.id, normalizedData.dependsOn)).get();
9666
9720
  if (!dependency) {
9667
- throw new Error(`dependsOn \u6307\u5411\u7684\u4EFB\u52A1 #${data.dependsOn} \u4E0D\u5B58\u5728`);
9721
+ throw new Error(`dependsOn \u6307\u5411\u7684\u4EFB\u52A1 #${normalizedData.dependsOn} \u4E0D\u5B58\u5728`);
9668
9722
  }
9669
- if ((dependency.cwd ?? null) !== (data.cwd ?? null)) {
9723
+ if ((dependency.cwd ?? null) !== (normalizedData.cwd ?? null)) {
9670
9724
  throw new Error("dependsOn \u5FC5\u987B\u6307\u5411\u540C\u4E00 cwd \u7684\u4EFB\u52A1");
9671
9725
  }
9672
9726
  const dependencyIsRecoverable = dependency.status === "pending" || dependency.status === "running" || dependency.status === "done" || dependency.status === "failed" && (dependency.retryCount ?? 0) <= (dependency.maxRetries ?? 3);
9673
9727
  if (!dependencyIsRecoverable) {
9674
- throw new Error(`dependsOn \u6307\u5411\u7684\u4EFB\u52A1 #${data.dependsOn} \u5DF2\u8FDB\u5165\u4E0D\u53EF\u6062\u590D\u7EC8\u6001`);
9728
+ throw new Error(`dependsOn \u6307\u5411\u7684\u4EFB\u52A1 #${normalizedData.dependsOn} \u5DF2\u8FDB\u5165\u4E0D\u53EF\u6062\u590D\u7EC8\u6001`);
9675
9729
  }
9676
9730
  }
9677
- return tx.insert(tasks2).values(data).returning().get();
9731
+ return tx.insert(tasks2).values(normalizedData).returning().get();
9732
+ }, { behavior: "immediate" });
9733
+ }
9734
+ static async update(id, data, scope = {}) {
9735
+ if (Object.keys(data).length === 0) throw new Error("\u81F3\u5C11\u63D0\u4F9B\u4E00\u4E2A\u8981\u4FEE\u6539\u7684\u5B57\u6BB5");
9736
+ const normalizedData = data.batchId === void 0 ? data : { ...data, batchId: normalizeTaskBatchId(data.batchId) ?? null };
9737
+ return db.transaction((tx) => {
9738
+ const task = tx.select().from(tasks2).where(and(
9739
+ eq(tasks2.id, id),
9740
+ sql`${tasks2.status} IN ('pending', 'failed', 'dead_letter')`,
9741
+ ...this.buildScopeWhere(scope)
9742
+ )).get();
9743
+ if (!task) return null;
9744
+ this.validateNewTask({
9745
+ name: normalizedData.name ?? task.name,
9746
+ agent: normalizedData.agent ?? task.agent,
9747
+ model: normalizedData.model ?? task.model,
9748
+ prompt: normalizedData.prompt ?? task.prompt,
9749
+ cwd: task.cwd,
9750
+ category: normalizedData.category ?? task.category,
9751
+ importance: normalizedData.importance ?? task.importance,
9752
+ urgency: normalizedData.urgency ?? task.urgency,
9753
+ batchId: normalizedData.batchId === void 0 ? task.batchId : normalizedData.batchId,
9754
+ maxRetries: normalizedData.maxRetries ?? task.maxRetries,
9755
+ retryBackoffMs: normalizedData.retryBackoffMs ?? task.retryBackoffMs,
9756
+ timeoutMs: normalizedData.timeoutMs === void 0 ? task.timeoutMs : normalizedData.timeoutMs,
9757
+ dependsOn: task.dependsOn
9758
+ });
9759
+ const maxRetries = normalizedData.maxRetries ?? task.maxRetries ?? 3;
9760
+ const exhausted = task.status === "failed" && (task.retryCount ?? 0) > maxRetries;
9761
+ return tx.update(tasks2).set({
9762
+ ...normalizedData,
9763
+ ...exhausted ? {
9764
+ status: "dead_letter",
9765
+ retryAfter: null
9766
+ } : {}
9767
+ }).where(eq(tasks2.id, id)).returning().get() ?? null;
9678
9768
  }, { behavior: "immediate" });
9679
9769
  }
9680
9770
  static validateNewTask(data) {
9681
9771
  if (!data.name.trim()) throw new Error("name \u4E0D\u80FD\u4E3A\u7A7A");
9682
9772
  if (!data.agent.trim()) throw new Error("agent \u4E0D\u80FD\u4E3A\u7A7A");
9683
9773
  if (!data.prompt.trim()) throw new Error("prompt \u4E0D\u80FD\u4E3A\u7A7A");
9774
+ validateTaskWorkingDirectory(data.cwd);
9684
9775
  this.validateInteger("importance", data.importance, 1, 5);
9685
9776
  this.validateInteger("urgency", data.urgency, 1, 5);
9686
9777
  this.validateInteger("maxRetries", data.maxRetries, 0, 1e3);
@@ -9701,12 +9792,14 @@ var init_task_service = __esm({
9701
9792
  isNull(tasks2.retryAfter),
9702
9793
  sql`${tasks2.retryAfter} <= ${nowMs}`
9703
9794
  );
9704
- const hasExcludedBatches = scope.excludedBatchIds && scope.excludedBatchIds.length > 0;
9795
+ const excludedBatchIds = [...new Set((scope.excludedBatchIds ?? []).map((batchId) => normalizeTaskBatchId(batchId)).filter((batchId) => Boolean(batchId)))];
9796
+ const hasExcludedBatches = excludedBatchIds.length > 0;
9705
9797
  let batchFilter;
9706
9798
  if (hasExcludedBatches) {
9707
9799
  batchFilter = or(
9708
9800
  isNull(tasks2.batchId),
9709
- sql`${tasks2.batchId} NOT IN ${scope.excludedBatchIds}`
9801
+ sql`trim(${tasks2.batchId}, ${TASK_BATCH_TRIM_CHARACTERS}) = ''`,
9802
+ sql`trim(${tasks2.batchId}, ${TASK_BATCH_TRIM_CHARACTERS}) NOT IN ${excludedBatchIds}`
9710
9803
  );
9711
9804
  }
9712
9805
  const statusConditions = or(
@@ -9740,9 +9833,11 @@ var init_task_service = __esm({
9740
9833
  ),
9741
9834
  or(
9742
9835
  isNull(tasks2.batchId),
9836
+ sql`trim(${tasks2.batchId}, ${TASK_BATCH_TRIM_CHARACTERS}) = ''`,
9743
9837
  sql`NOT EXISTS (
9744
9838
  SELECT 1 FROM tasks AS running_batch_task
9745
- WHERE running_batch_task.batch_id = ${tasks2.batchId}
9839
+ WHERE trim(running_batch_task.batch_id, ${TASK_BATCH_TRIM_CHARACTERS})
9840
+ = trim(${tasks2.batchId}, ${TASK_BATCH_TRIM_CHARACTERS})
9746
9841
  AND (
9747
9842
  running_batch_task.status = 'running'
9748
9843
  OR EXISTS (
@@ -9766,12 +9861,18 @@ var init_task_service = __esm({
9766
9861
  ).limit(1);
9767
9862
  return result[0] ?? null;
9768
9863
  }
9769
- static async countRunning() {
9864
+ static async countRunning(scope = {}) {
9865
+ const scopeConditions = scope.legacyCwd ? [sql`${tasks2.cwd} IS NULL OR trim(${tasks2.cwd}) = ''`] : this.buildScopeWhere(scope);
9866
+ if (scope.batchId !== void 0) {
9867
+ const batchId = normalizeTaskBatchId(scope.batchId);
9868
+ scopeConditions.push(batchId ? sql`trim(${tasks2.batchId}, ${TASK_BATCH_TRIM_CHARACTERS}) = ${batchId}` : sql`0`);
9869
+ }
9770
9870
  return db.transaction((tx) => {
9771
- const runningTasks = tx.select({ count: sql`count(*)` }).from(tasks2).where(eq(tasks2.status, "running")).get();
9871
+ const runningTasks = tx.select({ count: sql`count(*)` }).from(tasks2).where(and(eq(tasks2.status, "running"), ...scopeConditions)).get();
9772
9872
  const runsWithoutRunningTask = tx.select({ count: sql`count(DISTINCT ${taskRuns2.taskId})` }).from(taskRuns2).innerJoin(tasks2, eq(tasks2.id, taskRuns2.taskId)).where(and(
9773
9873
  eq(taskRuns2.status, "running"),
9774
- sql`${tasks2.status} <> 'running'`
9874
+ sql`${tasks2.status} <> 'running'`,
9875
+ ...scopeConditions
9775
9876
  )).get();
9776
9877
  return Number(runningTasks?.count ?? 0) + Number(runsWithoutRunningTask?.count ?? 0);
9777
9878
  }, { behavior: "deferred" });
@@ -10104,15 +10205,17 @@ var init_task_service = __esm({
10104
10205
  }).where(and(...conditions, hasViableDependency())).returning().get() ?? null, { behavior: "immediate" });
10105
10206
  }
10106
10207
  static async retryBatch(batchId, scope = {}) {
10208
+ const normalizedBatchId = normalizeTaskBatchId(batchId);
10209
+ if (!normalizedBatchId) return 0;
10107
10210
  const sqlite2 = getSqlite();
10108
10211
  const scopeFilter = scope.cwd === void 0 ? "" : "AND candidate.cwd = ?";
10109
- const parameters = scope.cwd === void 0 ? [batchId] : [batchId, scope.cwd];
10212
+ const parameters = scope.cwd === void 0 ? [TASK_BATCH_TRIM_CHARACTERS, normalizedBatchId] : [TASK_BATCH_TRIM_CHARACTERS, normalizedBatchId, scope.cwd];
10110
10213
  return db.transaction(() => sqlite2.query(`
10111
10214
  WITH RECURSIVE
10112
10215
  candidate(id, cwd, depends_on) AS MATERIALIZED (
10113
10216
  SELECT candidate.id, candidate.cwd, candidate.depends_on
10114
10217
  FROM tasks AS candidate
10115
- WHERE candidate.batch_id = ?
10218
+ WHERE trim(candidate.batch_id, ?) = ?
10116
10219
  AND candidate.status IN ('failed', 'dead_letter')
10117
10220
  ${scopeFilter}
10118
10221
  ),
@@ -10166,16 +10269,28 @@ var init_task_service = __esm({
10166
10269
  static async list(options = {}) {
10167
10270
  let query = db.select().from(tasks2).$dynamic();
10168
10271
  const conditions = [];
10169
- if (options.status) {
10272
+ if (options.activeExecution) {
10273
+ conditions.push(or(
10274
+ eq(tasks2.status, "running"),
10275
+ sql`EXISTS (
10276
+ SELECT 1 FROM task_runs AS active_list_run
10277
+ WHERE active_list_run.task_id = ${tasks2.id}
10278
+ AND active_list_run.status = 'running'
10279
+ )`
10280
+ ));
10281
+ } else if (options.status) {
10170
10282
  conditions.push(eq(tasks2.status, options.status));
10171
10283
  }
10172
- if (options.batchId) {
10173
- conditions.push(eq(tasks2.batchId, options.batchId));
10284
+ if (options.batchId !== void 0) {
10285
+ const batchId = normalizeTaskBatchId(options.batchId);
10286
+ conditions.push(batchId ? sql`trim(${tasks2.batchId}, ${TASK_BATCH_TRIM_CHARACTERS}) = ${batchId}` : sql`0`);
10174
10287
  }
10175
10288
  if (options.category) {
10176
10289
  conditions.push(eq(tasks2.category, options.category));
10177
10290
  }
10178
- if (options.cwd !== void 0) {
10291
+ if (options.legacyCwd) {
10292
+ conditions.push(sql`${tasks2.cwd} IS NULL OR trim(${tasks2.cwd}) = ''`);
10293
+ } else if (options.cwd !== void 0) {
10179
10294
  conditions.push(eq(tasks2.cwd, options.cwd));
10180
10295
  }
10181
10296
  if (conditions.length > 0) {
@@ -10193,9 +10308,12 @@ var init_task_service = __esm({
10193
10308
  static async stats(options = {}) {
10194
10309
  const conditions = [];
10195
10310
  if (options.batchId !== void 0) {
10196
- conditions.push(eq(tasks2.batchId, options.batchId));
10311
+ const batchId = normalizeTaskBatchId(options.batchId);
10312
+ conditions.push(batchId ? sql`trim(${tasks2.batchId}, ${TASK_BATCH_TRIM_CHARACTERS}) = ${batchId}` : sql`0`);
10197
10313
  }
10198
- if (options.cwd !== void 0) {
10314
+ if (options.legacyCwd) {
10315
+ conditions.push(sql`${tasks2.cwd} IS NULL OR trim(${tasks2.cwd}) = ''`);
10316
+ } else if (options.cwd !== void 0) {
10199
10317
  conditions.push(eq(tasks2.cwd, options.cwd));
10200
10318
  }
10201
10319
  const whereCondition = conditions.length > 0 ? and(...conditions) : void 0;
@@ -10220,6 +10338,33 @@ var init_task_service = __esm({
10220
10338
  }
10221
10339
  return stats;
10222
10340
  }
10341
+ static async projectSummaries(limit = 100) {
10342
+ this.validateInteger("limit", limit, 1, 1e3);
10343
+ const lastCreatedAt = sql`max(${tasks2.createdAt})`;
10344
+ const lastTaskId = sql`max(${tasks2.id})`;
10345
+ const rows = await db.select({
10346
+ cwd: tasks2.cwd,
10347
+ total: sql`count(*)`,
10348
+ pending: sql`sum(CASE WHEN ${tasks2.status} = 'pending' THEN 1 ELSE 0 END)`,
10349
+ running: sql`sum(CASE WHEN ${tasks2.status} = 'running' OR EXISTS (
10350
+ SELECT 1 FROM task_runs AS active_project_run
10351
+ WHERE active_project_run.task_id = ${tasks2.id}
10352
+ AND active_project_run.status = 'running'
10353
+ ) THEN 1 ELSE 0 END)`,
10354
+ failed: sql`sum(CASE WHEN ${tasks2.status} IN ('failed', 'dead_letter') THEN 1 ELSE 0 END)`,
10355
+ done: sql`sum(CASE WHEN ${tasks2.status} = 'done' THEN 1 ELSE 0 END)`,
10356
+ lastCreatedAt
10357
+ }).from(tasks2).where(sql`${tasks2.cwd} IS NOT NULL AND trim(${tasks2.cwd}) <> ''`).groupBy(tasks2.cwd).orderBy(desc(lastCreatedAt), desc(lastTaskId)).limit(limit);
10358
+ return rows.flatMap((row) => row.cwd === null ? [] : [{
10359
+ cwd: row.cwd,
10360
+ total: Number(row.total),
10361
+ pending: Number(row.pending),
10362
+ running: Number(row.running),
10363
+ failed: Number(row.failed),
10364
+ done: Number(row.done),
10365
+ lastCreatedAt: row.lastCreatedAt === null ? null : Number(row.lastCreatedAt) * 1e3
10366
+ }]);
10367
+ }
10223
10368
  static async delete(id, scope = {}) {
10224
10369
  const conditions = [
10225
10370
  eq(tasks2.id, id),
@@ -10353,12 +10498,15 @@ var init_task_service = __esm({
10353
10498
  function isLaunchIdentity(value) {
10354
10499
  return value != null && /^gateway-[1-9]\d*:launch:[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
10355
10500
  }
10501
+ function drainProofAckForIdentity(launchIdentity) {
10502
+ return { type: DRAIN_PROOF_ACK_MESSAGE_TYPE, identity: launchIdentity };
10503
+ }
10356
10504
  function isMatchingDrainProof(message, launchIdentity) {
10357
10505
  if (typeof message !== "object" || message == null) return false;
10358
10506
  const candidate = message;
10359
10507
  return candidate.type === DRAIN_PROOF_MESSAGE_TYPE && candidate.identity === launchIdentity;
10360
10508
  }
10361
- var LEGACY_GUARDIAN_LAUNCH_PROTOCOL, TOKEN_GUARDIAN_LAUNCH_PROTOCOL, LAUNCH_IDENTITY_ARGUMENT, DRAIN_PROOF_MESSAGE_TYPE, MANAGED_RUN_ENV, MANAGED_RUN_ENV_VALUE;
10509
+ var LEGACY_GUARDIAN_LAUNCH_PROTOCOL, TOKEN_GUARDIAN_LAUNCH_PROTOCOL, LAUNCH_IDENTITY_ARGUMENT, DRAIN_PROOF_MESSAGE_TYPE, DRAIN_PROOF_ACK_MESSAGE_TYPE, MANAGED_RUN_ENV, MANAGED_RUN_ENV_VALUE;
10362
10510
  var init_launch_protocol = __esm({
10363
10511
  "src/core/launch-protocol.ts"() {
10364
10512
  "use strict";
@@ -10366,6 +10514,7 @@ var init_launch_protocol = __esm({
10366
10514
  TOKEN_GUARDIAN_LAUNCH_PROTOCOL = "gated-v3-token-guardian";
10367
10515
  LAUNCH_IDENTITY_ARGUMENT = "--supertask-launch-identity";
10368
10516
  DRAIN_PROOF_MESSAGE_TYPE = "supertask-drained";
10517
+ DRAIN_PROOF_ACK_MESSAGE_TYPE = "supertask-drained-ack";
10369
10518
  MANAGED_RUN_ENV = "SUPERTASK_MANAGED_RUN";
10370
10519
  MANAGED_RUN_ENV_VALUE = "1";
10371
10520
  }
@@ -19955,27 +20104,34 @@ var init_task_template_service = __esm({
19955
20104
  init_db2();
19956
20105
  init_drizzle_orm();
19957
20106
  init_cron_parser();
20107
+ init_task_working_directory();
20108
+ init_task_batch();
19958
20109
  ({ taskTemplates: taskTemplates2 } = schema_exports);
19959
20110
  TaskTemplateService = class {
19960
20111
  static async create(data) {
19961
- this.validate(data);
20112
+ const normalizedData = {
20113
+ ...data,
20114
+ batchId: normalizeTaskBatchId(data.batchId)
20115
+ };
20116
+ this.validate(normalizedData);
19962
20117
  const now = Date.now();
19963
- const nextRunAt = data.nextRunAt ?? this.calculateNextRunAt(
19964
- data.scheduleType,
20118
+ const nextRunAt = normalizedData.nextRunAt ?? this.calculateNextRunAt(
20119
+ normalizedData.scheduleType,
19965
20120
  {
19966
- cronExpr: data.cronExpr ?? null,
19967
- intervalMs: data.intervalMs ?? null,
19968
- runAt: data.runAt ?? null
20121
+ cronExpr: normalizedData.cronExpr ?? null,
20122
+ intervalMs: normalizedData.intervalMs ?? null,
20123
+ runAt: normalizedData.runAt ?? null
19969
20124
  },
19970
20125
  now
19971
20126
  );
19972
- const result = await db.insert(taskTemplates2).values({ ...data, nextRunAt, createdAt: now, updatedAt: now }).returning();
20127
+ const result = await db.insert(taskTemplates2).values({ ...normalizedData, nextRunAt, createdAt: now, updatedAt: now }).returning();
19973
20128
  return result[0];
19974
20129
  }
19975
20130
  static validate(data) {
19976
20131
  if (!data.name.trim()) throw new Error("name \u4E0D\u80FD\u4E3A\u7A7A");
19977
20132
  if (!data.agent.trim()) throw new Error("agent \u4E0D\u80FD\u4E3A\u7A7A");
19978
20133
  if (!data.prompt.trim()) throw new Error("prompt \u4E0D\u80FD\u4E3A\u7A7A");
20134
+ validateTaskWorkingDirectory(data.cwd);
19979
20135
  const scheduleType = data.scheduleType;
19980
20136
  if (!["cron", "delayed", "recurring"].includes(scheduleType)) {
19981
20137
  throw new Error("scheduleType \u5FC5\u987B\u662F cron\u3001delayed \u6216 recurring");
@@ -20002,13 +20158,40 @@ var init_task_template_service = __esm({
20002
20158
  throw new Error(`${name} \u5FC5\u987B\u662F ${min} \u5230 ${max} \u4E4B\u95F4\u7684\u6574\u6570`);
20003
20159
  }
20004
20160
  }
20005
- static async list(limit = 50) {
20006
- return await db.select().from(taskTemplates2).orderBy(desc(taskTemplates2.createdAt), desc(taskTemplates2.id)).limit(limit);
20161
+ static async list(limit = 50, offset = 0) {
20162
+ return await db.select().from(taskTemplates2).orderBy(desc(taskTemplates2.createdAt), desc(taskTemplates2.id)).limit(limit).offset(offset);
20163
+ }
20164
+ static async stats() {
20165
+ const result = await db.select({
20166
+ total: sql`count(*)`,
20167
+ enabled: sql`sum(case when ${taskTemplates2.enabled} = 1 then 1 else 0 end)`
20168
+ }).from(taskTemplates2);
20169
+ const total = Number(result[0]?.total ?? 0);
20170
+ const enabled = Number(result[0]?.enabled ?? 0);
20171
+ return { total, enabled, disabled: total - enabled };
20007
20172
  }
20008
20173
  static async getById(id) {
20009
20174
  const result = await db.select().from(taskTemplates2).where(eq(taskTemplates2.id, id));
20010
20175
  return result[0] || null;
20011
20176
  }
20177
+ static async update(id, data) {
20178
+ const normalizedData = {
20179
+ ...data,
20180
+ batchId: normalizeTaskBatchId(data.batchId) ?? null
20181
+ };
20182
+ this.validate(normalizedData);
20183
+ const now = Date.now();
20184
+ const nextRunAt = this.calculateNextRunAt(
20185
+ normalizedData.scheduleType,
20186
+ normalizedData,
20187
+ now
20188
+ );
20189
+ return db.transaction((tx) => {
20190
+ const existing = tx.select({ id: taskTemplates2.id }).from(taskTemplates2).where(eq(taskTemplates2.id, id)).limit(1).get();
20191
+ if (!existing) return null;
20192
+ return tx.update(taskTemplates2).set({ ...normalizedData, nextRunAt, updatedAt: now }).where(eq(taskTemplates2.id, id)).returning().get() ?? null;
20193
+ }, { behavior: "immediate" });
20194
+ }
20012
20195
  static async enable(id) {
20013
20196
  return db.transaction((tx) => {
20014
20197
  const template = tx.select().from(taskTemplates2).where(eq(taskTemplates2.id, id)).limit(1).get();
@@ -20102,7 +20285,7 @@ import {
20102
20285
  existsSync as existsSync2,
20103
20286
  mkdirSync as mkdirSync2,
20104
20287
  renameSync,
20105
- statSync,
20288
+ statSync as statSync2,
20106
20289
  unlinkSync,
20107
20290
  writeFileSync
20108
20291
  } from "fs";
@@ -20204,9 +20387,9 @@ var init_database_maintenance_service = __esm({
20204
20387
  if (!existsSync2(source)) {
20205
20388
  throw new Error(`\u5907\u4EFD\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${source}`);
20206
20389
  }
20207
- const sourceStat = statSync(source);
20390
+ const sourceStat = statSync2(source);
20208
20391
  if (!sourceStat.isFile()) throw new Error(`\u5907\u4EFD\u8DEF\u5F84\u4E0D\u662F\u6587\u4EF6\uFF1A${source}`);
20209
- const liveStat = statSync(livePath);
20392
+ const liveStat = statSync2(livePath);
20210
20393
  if (sourceStat.dev === liveStat.dev && sourceStat.ino === liveStat.ino) {
20211
20394
  throw new Error("\u6062\u590D\u6765\u6E90\u4E0D\u80FD\u662F\u5F53\u524D\u6570\u636E\u5E93\u7684\u7B26\u53F7\u94FE\u63A5\u6216\u786C\u94FE\u63A5\u522B\u540D");
20212
20395
  }
@@ -20447,7 +20630,7 @@ var init_database_maintenance_service = __esm({
20447
20630
  const counts = missingTables.length === 0 ? this.readCounts(sqlite2) : { tasks: 0, taskRuns: 0, taskTemplates: 0 };
20448
20631
  const runningTasks = missingTables.includes("tasks") ? 0 : this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM tasks WHERE status = 'running'");
20449
20632
  const runningRuns = missingTables.includes("task_runs") ? 0 : this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM task_runs WHERE status = 'running'");
20450
- const sizeBytes = path === ":memory:" || !existsSync2(path) ? sqlite2.serialize().byteLength : statSync(path).size;
20633
+ const sizeBytes = path === ":memory:" || !existsSync2(path) ? sqlite2.serialize().byteLength : statSync2(path).size;
20451
20634
  return {
20452
20635
  ok: integrityMessages.length === 1 && integrityMessages[0] === "ok" && foreignKeyViolations === 0 && missingTables.length === 0,
20453
20636
  path: normalizedPath(path),
@@ -20484,7 +20667,7 @@ var init_database_maintenance_service = __esm({
20484
20667
  const check = this.inspectFile(temporary);
20485
20668
  if (!check.ok) throw new Error("\u65B0\u5907\u4EFD\u672A\u901A\u8FC7\u5B8C\u6574\u6027\u6821\u9A8C");
20486
20669
  renameSync(temporary, output);
20487
- const finalCheck = { ...check, path: output, sizeBytes: statSync(output).size };
20670
+ const finalCheck = { ...check, path: output, sizeBytes: statSync2(output).size };
20488
20671
  return { path: output, sizeBytes: finalCheck.sizeBytes, check: finalCheck };
20489
20672
  } catch (error) {
20490
20673
  safeUnlink(temporary);
@@ -20530,6 +20713,41 @@ var init_database_maintenance_service = __esm({
20530
20713
  }
20531
20714
  });
20532
20715
 
20716
+ // src/core/duration.ts
20717
+ function parseDuration(input) {
20718
+ const trimmed = input.trim();
20719
+ const simple = DURATION_REGEX.exec(trimmed);
20720
+ if (simple) {
20721
+ const value = parseFloat(simple[1]);
20722
+ const unit = simple[2].toLowerCase();
20723
+ if (unit === "ms") return value;
20724
+ if (unit === "s" || unit === "sec" || unit === "second" || unit === "seconds") return value * 1e3;
20725
+ if (unit === "min" || unit === "minute" || unit === "minutes" || unit === "m") return value * 6e4;
20726
+ if (unit === "h" || unit === "hour" || unit === "hours") return value * 36e5;
20727
+ if (unit === "d" || unit === "day" || unit === "days") return value * 864e5;
20728
+ if (unit === "w" || unit === "week" || unit === "weeks") return value * 6048e5;
20729
+ }
20730
+ const iso = ISO8601_REGEX.exec(trimmed);
20731
+ if (iso) {
20732
+ const days = parseFloat(iso[1] ?? "0");
20733
+ const hours = parseFloat(iso[2] ?? "0");
20734
+ const minutes = parseFloat(iso[3] ?? "0");
20735
+ const seconds = parseFloat(iso[4] ?? "0");
20736
+ return (days * 86400 + hours * 3600 + minutes * 60 + seconds) * 1e3;
20737
+ }
20738
+ const asNumber = Number(trimmed);
20739
+ if (!isNaN(asNumber) && asNumber > 0) return asNumber;
20740
+ return null;
20741
+ }
20742
+ var DURATION_REGEX, ISO8601_REGEX;
20743
+ var init_duration = __esm({
20744
+ "src/core/duration.ts"() {
20745
+ "use strict";
20746
+ DURATION_REGEX = /^(\d+(?:\.\d+)?)\s*(ms|s|sec|seconds?|min|minutes?|m|h|hours?|d|days?|w|weeks?)$/i;
20747
+ ISO8601_REGEX = /^P(?:([.\d]+)D)?(?:T(?:([.\d]+)H)?(?:([.\d]+)M)?(?:([.\d]+)S)?)?$/i;
20748
+ }
20749
+ });
20750
+
20533
20751
  // src/gateway/config.ts
20534
20752
  var config_exports = {};
20535
20753
  __export(config_exports, {
@@ -20765,11 +20983,11 @@ import {
20765
20983
  mkdirSync as mkdirSync4,
20766
20984
  readFileSync as readFileSync3,
20767
20985
  rmSync,
20768
- statSync as statSync2,
20986
+ statSync as statSync3,
20769
20987
  writeFileSync as writeFileSync2
20770
20988
  } from "fs";
20771
20989
  import { homedir as homedir3, userInfo } from "os";
20772
- import { delimiter, dirname as dirname6, isAbsolute, join as join4, resolve as resolve3 } from "path";
20990
+ import { delimiter, dirname as dirname6, isAbsolute as isAbsolute2, join as join4, resolve as resolve3 } from "path";
20773
20991
  import { fileURLToPath as fileURLToPath4 } from "url";
20774
20992
  import { Database as Database5 } from "bun:sqlite";
20775
20993
  function resolveGatewayEntry() {
@@ -20804,8 +21022,28 @@ function getRunningVersion(env = process.env, cwd = process.cwd()) {
20804
21022
  return null;
20805
21023
  }
20806
21024
  }
21025
+ function packageVersionFromGatewayEntry(gatewayEntry) {
21026
+ if (gatewayEntry === null) return null;
21027
+ let directory = dirname6(gatewayEntry);
21028
+ for (let depth = 0; depth < 6; depth += 1) {
21029
+ const packagePath = join4(directory, "package.json");
21030
+ if (existsSync5(packagePath)) {
21031
+ try {
21032
+ const pkg = JSON.parse(readFileSync3(packagePath, "utf8"));
21033
+ if (pkg.name === "opencode-supertask" && typeof pkg.version === "string") {
21034
+ return pkg.version;
21035
+ }
21036
+ } catch {
21037
+ }
21038
+ }
21039
+ const parent = dirname6(directory);
21040
+ if (parent === directory) break;
21041
+ directory = parent;
21042
+ }
21043
+ return null;
21044
+ }
20807
21045
  function resolveRuntimeExecutable(command, env, cwd) {
20808
- if (isAbsolute(command) || command.includes("/")) return runtimePath(command, cwd);
21046
+ if (isAbsolute2(command) || command.includes("/")) return runtimePath(command, cwd);
20809
21047
  for (const entry of (env.PATH ?? "").split(delimiter)) {
20810
21048
  if (!entry) continue;
20811
21049
  const candidate = resolve3(cwd, entry, command);
@@ -20846,6 +21084,8 @@ function getGatewayDiagnostic() {
20846
21084
  pid: null,
20847
21085
  ready: false,
20848
21086
  runningVersion: getRunningVersion(),
21087
+ gatewayEntry: null,
21088
+ gatewayPackageVersion: null,
20849
21089
  logRotationInstalled: false,
20850
21090
  startupConfigured: process.platform === "darwin" || process.platform === "linux" ? false : null,
20851
21091
  currentScope: producerScope,
@@ -20861,6 +21101,7 @@ function getGatewayDiagnostic() {
20861
21101
  const pid = typeof gateway?.pid === "number" ? gateway.pid : null;
20862
21102
  const readyPath = managedScope?.databasePath ?? databasePath();
20863
21103
  const lockedVersion = pid == null ? void 0 : gatewayVersionFromLock(pid, readyPath);
21104
+ const gatewayEntry = runtime?.gatewayEntry ?? null;
20864
21105
  return {
20865
21106
  pm2Installed: true,
20866
21107
  processFound: gateway != null,
@@ -20868,6 +21109,8 @@ function getGatewayDiagnostic() {
20868
21109
  pid,
20869
21110
  ready: pid != null && isGatewayReady(pid, readyPath),
20870
21111
  runningVersion: lockedVersion === void 0 ? getRunningVersion(gatewayEnv, runtime?.cwd) : lockedVersion,
21112
+ gatewayEntry,
21113
+ gatewayPackageVersion: packageVersionFromGatewayEntry(gatewayEntry),
20871
21114
  logRotationInstalled: processes.some((item) => item.name === "pm2-logrotate" && item.pm2_env?.status === "online"),
20872
21115
  startupConfigured: process.platform === "darwin" ? isMacLaunchAgentConfigured(
20873
21116
  gatewayEnv.PM2_HOME ?? join4(runtimeHome(gatewayEnv), ".pm2"),
@@ -20895,7 +21138,7 @@ function xmlEscape(value) {
20895
21138
  }
20896
21139
  function resolvePm2Bin() {
20897
21140
  const configured = pm2Bin();
20898
- if (isAbsolute(configured)) return configured;
21141
+ if (isAbsolute2(configured)) return configured;
20899
21142
  const result = spawnSync2("which", [configured], { encoding: "utf8" });
20900
21143
  const resolved = result.status === 0 ? result.stdout.trim().split("\n")[0] : "";
20901
21144
  if (!resolved) throw new Error(`[supertask] \u65E0\u6CD5\u89E3\u6790 pm2 \u53EF\u6267\u884C\u6587\u4EF6: ${configured}`);
@@ -21257,7 +21500,7 @@ function gatewayRuntimeFromProcess(processInfo) {
21257
21500
  if (typeof savedBunPath !== "string" || typeof savedCwd !== "string") return null;
21258
21501
  try {
21259
21502
  accessSync(savedBunPath, constants.X_OK);
21260
- if (!statSync2(savedCwd).isDirectory()) return null;
21503
+ if (!statSync3(savedCwd).isDirectory()) return null;
21261
21504
  if (spawnSync2(savedBunPath, ["--version"], {
21262
21505
  stdio: "ignore",
21263
21506
  env: savedEnv,
@@ -21464,7 +21707,7 @@ function pm2StartGateway(runtime = currentGatewayRuntime()) {
21464
21707
  try {
21465
21708
  accessSync(runtime.bunPath, constants.X_OK);
21466
21709
  accessSync(runtime.gatewayEntry, constants.R_OK);
21467
- if (!statSync2(runtime.cwd).isDirectory()) throw new Error("cwd is not a directory");
21710
+ if (!statSync3(runtime.cwd).isDirectory()) throw new Error("cwd is not a directory");
21468
21711
  } catch (error) {
21469
21712
  throw new Error(`[supertask] Gateway \u76EE\u6807\u8FD0\u884C\u65F6\u4E0D\u53EF\u7528: ${error instanceof Error ? error.message : String(error)}`);
21470
21713
  }
@@ -21854,148 +22097,419 @@ var init_pm2 = __esm({
21854
22097
  }
21855
22098
  });
21856
22099
 
21857
- // src/gateway/health.ts
21858
- function initializeGatewayHealth(config) {
21859
- const now = Date.now();
21860
- state = {
21861
- startedAt: now,
21862
- config,
21863
- lastActivityAt: {
21864
- worker: now,
21865
- scheduler: now,
21866
- watchdog: now,
21867
- watchdogCleanup: now
21868
- },
21869
- lastSuccessAt: {
21870
- worker: now,
21871
- scheduler: now,
21872
- watchdog: now,
21873
- watchdogCleanup: now
21874
- },
21875
- consecutiveFailures: {
21876
- worker: 0,
21877
- scheduler: 0,
21878
- watchdog: 0,
21879
- watchdogCleanup: 0
21880
- },
21881
- lastError: {
21882
- worker: null,
21883
- scheduler: null,
21884
- watchdog: null,
21885
- watchdogCleanup: null
21886
- }
21887
- };
21888
- }
21889
- function markGatewayActivity(component) {
21890
- if (state) state.lastActivityAt[component] = Date.now();
21891
- }
21892
- function markGatewaySuccess(component) {
21893
- if (!state) return;
21894
- const now = Date.now();
21895
- state.lastActivityAt[component] = now;
21896
- state.lastSuccessAt[component] = now;
21897
- state.consecutiveFailures[component] = 0;
21898
- }
21899
- function markGatewayFailure(component, error) {
21900
- if (!state) return;
21901
- const now = Date.now();
21902
- state.lastActivityAt[component] = now;
21903
- state.consecutiveFailures[component] += 1;
21904
- state.lastError[component] = {
21905
- at: now,
21906
- message: error instanceof Error ? error.message : String(error)
21907
- };
21908
- }
21909
- function resetGatewayHealth() {
21910
- state = null;
21911
- }
21912
- function componentStatus(component, enabled, maxAgeMs, now) {
21913
- const lastActivityAt = state?.lastActivityAt[component] ?? null;
21914
- const ageMs = lastActivityAt == null ? null : Math.max(0, now - lastActivityAt);
21915
- const consecutiveFailures = state?.consecutiveFailures[component] ?? 0;
22100
+ // src/core/semver.ts
22101
+ function parseSemanticVersion(version2) {
22102
+ const match2 = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec(version2);
22103
+ if (!match2) return null;
22104
+ const prerelease = match2[4]?.split(".") ?? [];
22105
+ if (prerelease.some((identifier) => /^\d+$/.test(identifier) && identifier.length > 1 && identifier.startsWith("0"))) {
22106
+ return null;
22107
+ }
21916
22108
  return {
21917
- enabled,
21918
- healthy: !enabled || ageMs != null && ageMs <= maxAgeMs && consecutiveFailures === 0,
21919
- lastActivityAt,
21920
- lastSuccessAt: state?.lastSuccessAt[component] ?? null,
21921
- consecutiveFailures,
21922
- lastError: state?.lastError[component] ?? null,
21923
- ageMs,
21924
- maxAgeMs
22109
+ major: Number(match2[1]),
22110
+ minor: Number(match2[2]),
22111
+ patch: Number(match2[3]),
22112
+ prerelease
21925
22113
  };
21926
22114
  }
21927
- function getGatewayHealth(now = Date.now()) {
21928
- const worker = componentStatus(
21929
- "worker",
21930
- true,
21931
- Math.max((state?.config.workerPollIntervalMs ?? 1e3) * 5, 5e3),
21932
- now
21933
- );
21934
- const scheduler = componentStatus(
21935
- "scheduler",
21936
- state?.config.schedulerEnabled ?? false,
21937
- Math.max((state?.config.schedulerCheckIntervalMs ?? 1e3) * 5, 5e3),
21938
- now
21939
- );
21940
- const watchdog = componentStatus(
21941
- "watchdog",
21942
- true,
21943
- Math.max((state?.config.watchdogCheckIntervalMs ?? 6e4) * 3, 5e3),
21944
- now
21945
- );
21946
- const watchdogCleanup = componentStatus(
21947
- "watchdogCleanup",
21948
- true,
21949
- Math.max((state?.config.watchdogCleanupIntervalMs ?? 864e5) * 2, 6e4),
21950
- now
21951
- );
21952
- let lock = { pid: null, heartbeatAt: null, readyAt: null, ageMs: null, healthy: false };
21953
- try {
21954
- const row = sqlite.prepare(
21955
- "SELECT pid, heartbeat_at, ready_at FROM gateway_lock WHERE id = 1"
21956
- ).get();
21957
- if (row) {
21958
- const ageMs = Math.max(0, now - row.heartbeat_at);
21959
- lock = {
21960
- pid: row.pid,
21961
- heartbeatAt: row.heartbeat_at,
21962
- readyAt: row.ready_at,
21963
- ageMs,
21964
- healthy: row.pid === process.pid && row.ready_at != null && ageMs < LOCK_STALE_MS
21965
- };
22115
+ function compareSemanticVersions(left, right) {
22116
+ const a = parseSemanticVersion(left);
22117
+ const b = parseSemanticVersion(right);
22118
+ if (!a || !b) return null;
22119
+ for (const key of ["major", "minor", "patch"]) {
22120
+ if (a[key] !== b[key]) return a[key] - b[key];
22121
+ }
22122
+ if (a.prerelease.length === 0 || b.prerelease.length === 0) {
22123
+ if (a.prerelease.length === b.prerelease.length) return 0;
22124
+ return a.prerelease.length === 0 ? 1 : -1;
22125
+ }
22126
+ const identifiers = Math.max(a.prerelease.length, b.prerelease.length);
22127
+ for (let index2 = 0; index2 < identifiers; index2 += 1) {
22128
+ const leftIdentifier = a.prerelease[index2];
22129
+ const rightIdentifier = b.prerelease[index2];
22130
+ if (leftIdentifier === void 0 || rightIdentifier === void 0) {
22131
+ return leftIdentifier === rightIdentifier ? 0 : leftIdentifier === void 0 ? -1 : 1;
21966
22132
  }
21967
- } catch {
22133
+ if (leftIdentifier === rightIdentifier) continue;
22134
+ const leftNumeric = /^\d+$/.test(leftIdentifier);
22135
+ const rightNumeric = /^\d+$/.test(rightIdentifier);
22136
+ if (leftNumeric && rightNumeric) return Number(leftIdentifier) - Number(rightIdentifier);
22137
+ if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1;
22138
+ return leftIdentifier < rightIdentifier ? -1 : 1;
21968
22139
  }
21969
- const healthy = state != null && lock.healthy && worker.healthy && scheduler.healthy && watchdog.healthy && watchdogCleanup.healthy;
21970
- return {
21971
- status: healthy ? "ok" : "degraded",
21972
- pid: process.pid,
21973
- uptimeMs: state ? Math.max(0, now - state.startedAt) : 0,
21974
- lock,
21975
- components: { worker, scheduler, watchdog, watchdogCleanup }
21976
- };
22140
+ return 0;
21977
22141
  }
21978
- var LOCK_STALE_MS, state;
21979
- var init_health = __esm({
21980
- "src/gateway/health.ts"() {
22142
+ function isSemanticVersion(version2) {
22143
+ return parseSemanticVersion(version2) !== null;
22144
+ }
22145
+ var init_semver = __esm({
22146
+ "src/core/semver.ts"() {
21981
22147
  "use strict";
21982
- init_db2();
21983
- LOCK_STALE_MS = 3e4;
21984
- state = null;
21985
22148
  }
21986
22149
  });
21987
22150
 
21988
- // src/worker/index.ts
21989
- import { spawn } from "child_process";
21990
- import { fileURLToPath as fileURLToPath5 } from "url";
21991
- import { existsSync as existsSync6 } from "fs";
21992
- import { dirname as dirname7, join as join5 } from "path";
21993
- import { randomUUID as randomUUID2 } from "crypto";
21994
- function assertWorkerProcessIsolationSupported(platform = process.platform) {
21995
- if (platform === "win32") {
21996
- throw new Error(
21997
- "Windows Worker \u5DF2\u5B89\u5168\u7981\u7528\uFF1A\u5F53\u524D\u8FD0\u884C\u65F6\u65E0\u6CD5\u7528 Job Object \u8BC1\u660E\u6574\u4E2A OpenCode \u8FDB\u7A0B\u6811\u5DF2\u9000\u51FA"
21998
- );
22151
+ // src/daemon/update.ts
22152
+ var update_exports = {};
22153
+ __export(update_exports, {
22154
+ getOpenCodePluginDiagnostic: () => getOpenCodePluginDiagnostic,
22155
+ installLatestPlugin: () => installLatestPlugin,
22156
+ installPluginVersion: () => installPluginVersion,
22157
+ resolveConfiguredPluginSpec: () => resolveConfiguredPluginSpec,
22158
+ resolveInstalledPlugin: () => resolveInstalledPlugin,
22159
+ resolveInstalledPluginVersion: () => resolveInstalledPluginVersion
22160
+ });
22161
+ import { spawnSync as spawnSync3 } from "child_process";
22162
+ import { existsSync as existsSync6, readFileSync as readFileSync4, readdirSync } from "fs";
22163
+ import { homedir as homedir4 } from "os";
22164
+ import { join as join5 } from "path";
22165
+ function pluginAt(packageDir) {
22166
+ const packageJson = join5(packageDir, "package.json");
22167
+ const gatewayEntry = join5(packageDir, "dist/gateway/index.js");
22168
+ if (!existsSync6(packageJson) || !existsSync6(gatewayEntry)) return null;
22169
+ try {
22170
+ const pkg = JSON.parse(readFileSync4(packageJson, "utf8"));
22171
+ if (pkg.name !== PACKAGE_NAME || typeof pkg.version !== "string") return null;
22172
+ return { packageDir, gatewayEntry, version: pkg.version };
22173
+ } catch {
22174
+ return null;
22175
+ }
22176
+ }
22177
+ function compareVersions(left, right) {
22178
+ return compareSemanticVersions(left, right) ?? left.localeCompare(right);
22179
+ }
22180
+ function cacheRoot() {
22181
+ return process.env.SUPERTASK_OPENCODE_CACHE_DIR ?? join5(homedir4(), ".cache/opencode/packages");
22182
+ }
22183
+ function installedPlugins() {
22184
+ const root = cacheRoot();
22185
+ const packageDirs = existsSync6(root) ? readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && (entry.name === PACKAGE_NAME || entry.name.startsWith(`${PACKAGE_NAME}@`))).map((entry) => join5(root, entry.name, "node_modules", PACKAGE_NAME)) : [];
22186
+ return packageDirs.map(pluginAt).filter((plugin) => plugin !== null);
22187
+ }
22188
+ function resolveInstalledPlugin() {
22189
+ const override = process.env.SUPERTASK_PLUGIN_PACKAGE_DIR;
22190
+ if (override) {
22191
+ const plugin = pluginAt(override);
22192
+ if (!plugin) throw new Error(`[supertask] \u5B89\u88C5\u5305\u65E0\u6548\u6216\u7F3A\u5C11 Gateway \u6784\u5EFA\u4EA7\u7269: ${override}`);
22193
+ return plugin;
22194
+ }
22195
+ const installed = installedPlugins().sort((left, right) => compareVersions(right.version, left.version));
22196
+ if (!installed[0]) {
22197
+ throw new Error(`[supertask] OpenCode \u63D2\u4EF6\u7F13\u5B58\u4E2D\u627E\u4E0D\u5230\u53EF\u8FD0\u884C\u7684 ${PACKAGE_NAME}`);
22198
+ }
22199
+ return installed[0];
22200
+ }
22201
+ function opencodeBin() {
22202
+ return process.env.SUPERTASK_OPENCODE_BIN ?? "opencode";
22203
+ }
22204
+ function npmBin() {
22205
+ return process.env.SUPERTASK_NPM_BIN ?? "npm";
22206
+ }
22207
+ function latestVersion() {
22208
+ const result = spawnSync3(npmBin(), [
22209
+ "view",
22210
+ PACKAGE_NAME,
22211
+ "dist-tags.latest",
22212
+ "--json"
22213
+ ], {
22214
+ encoding: "utf8",
22215
+ env: process.env,
22216
+ timeout: 3e4
22217
+ });
22218
+ const output = `${result.stdout ?? ""}`.trim();
22219
+ if (result.error) {
22220
+ throw new Error(`[supertask] \u67E5\u8BE2 npm latest \u5931\u8D25: ${result.error.message}`);
22221
+ }
22222
+ if (result.status !== 0) {
22223
+ const detail = `${result.stderr ?? ""}`.trim();
22224
+ throw new Error(`[supertask] \u67E5\u8BE2 npm latest \u5931\u8D25: ${detail || `\u9000\u51FA\u7801 ${result.status}`}`);
22225
+ }
22226
+ let response;
22227
+ try {
22228
+ response = JSON.parse(output);
22229
+ } catch {
22230
+ throw new Error(`[supertask] npm latest \u8FD4\u56DE\u65E0\u6CD5\u89E3\u6790: ${output || "(empty)"}`);
22231
+ }
22232
+ const versions = typeof response === "string" ? [response] : Array.isArray(response) && response.every((value) => typeof value === "string") ? response : [];
22233
+ const uniqueVersions = [...new Set(versions)];
22234
+ if (uniqueVersions.length !== 1 || !isSemanticVersion(uniqueVersions[0])) {
22235
+ throw new Error(`[supertask] npm latest \u7248\u672C\u65E0\u6548: ${String(response)}`);
22236
+ }
22237
+ return uniqueVersions[0];
22238
+ }
22239
+ function resolveInstalledPluginVersion(expectedVersion) {
22240
+ const override = process.env.SUPERTASK_PLUGIN_PACKAGE_DIR;
22241
+ const installed = override ? [pluginAt(override)].filter((plugin) => plugin !== null) : installedPlugins();
22242
+ const matched = installed.find((plugin) => plugin.version === expectedVersion);
22243
+ if (matched) return matched;
22244
+ const actual = installed.length > 0 ? installed.map((plugin) => plugin.version).join(", ") : "\u672A\u627E\u5230\u53EF\u8FD0\u884C\u7F13\u5B58";
22245
+ throw new Error(`[supertask] OpenCode \u63D2\u4EF6\u7F13\u5B58\u7248\u672C\u4E0D\u5339\u914D\uFF1A\u671F\u671B ${expectedVersion}\uFF0C\u5B9E\u9645 ${actual}`);
22246
+ }
22247
+ function resolveConfiguredPluginSpec(value) {
22248
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
22249
+ throw new Error("[supertask] OpenCode \u6700\u7EC8\u914D\u7F6E\u4E0D\u662F\u5BF9\u8C61");
22250
+ }
22251
+ const plugins = value.plugin;
22252
+ if (!Array.isArray(plugins)) {
22253
+ throw new Error(`[supertask] OpenCode \u6700\u7EC8\u914D\u7F6E\u672A\u542F\u7528 ${PACKAGE_NAME}`);
22254
+ }
22255
+ const matches = plugins.flatMap((entry) => {
22256
+ if (typeof entry === "string") return [entry];
22257
+ if (Array.isArray(entry) && typeof entry[0] === "string") return [entry[0]];
22258
+ return [];
22259
+ }).filter((spec2) => spec2 === PACKAGE_NAME || spec2.startsWith(`${PACKAGE_NAME}@`));
22260
+ if (matches.length === 0) {
22261
+ throw new Error(`[supertask] OpenCode \u6700\u7EC8\u914D\u7F6E\u672A\u542F\u7528 ${PACKAGE_NAME}`);
22262
+ }
22263
+ if (matches.length !== 1) {
22264
+ throw new Error(`[supertask] OpenCode \u6700\u7EC8\u914D\u7F6E\u5305\u542B\u591A\u4E2A ${PACKAGE_NAME} \u58F0\u660E: ${matches.join(", ")}`);
22265
+ }
22266
+ const spec = matches[0];
22267
+ const version2 = spec.startsWith(`${PACKAGE_NAME}@`) ? spec.slice(PACKAGE_NAME.length + 1) : null;
22268
+ return {
22269
+ spec,
22270
+ version: version2 !== null && isSemanticVersion(version2) ? version2 : null,
22271
+ exact: version2 !== null && isSemanticVersion(version2)
22272
+ };
22273
+ }
22274
+ function getOpenCodePluginDiagnostic() {
22275
+ const result = spawnSync3(opencodeBin(), ["debug", "config", "--pure"], {
22276
+ encoding: "utf8",
22277
+ env: process.env,
22278
+ timeout: 3e4
22279
+ });
22280
+ const failed = (message) => ({
22281
+ ok: false,
22282
+ spec: "",
22283
+ version: null,
22284
+ exact: false,
22285
+ cachedVersion: null,
22286
+ packageDir: null,
22287
+ error: message
22288
+ });
22289
+ if (result.error) {
22290
+ return failed(`[supertask] \u65E0\u6CD5\u8BFB\u53D6 OpenCode \u6700\u7EC8\u914D\u7F6E: ${result.error.message}`);
22291
+ }
22292
+ if (result.status !== 0) {
22293
+ const detail = `${result.stderr ?? ""}`.trim();
22294
+ return failed(`[supertask] \u65E0\u6CD5\u8BFB\u53D6 OpenCode \u6700\u7EC8\u914D\u7F6E: ${detail || `\u9000\u51FA\u7801 ${result.status}`}`);
22295
+ }
22296
+ let config;
22297
+ try {
22298
+ config = JSON.parse(`${result.stdout ?? ""}`);
22299
+ } catch {
22300
+ return failed("[supertask] OpenCode \u6700\u7EC8\u914D\u7F6E\u4E0D\u662F\u6709\u6548 JSON");
22301
+ }
22302
+ let configured;
22303
+ try {
22304
+ configured = resolveConfiguredPluginSpec(config);
22305
+ } catch (error) {
22306
+ return failed(error instanceof Error ? error.message : String(error));
22307
+ }
22308
+ if (!configured.exact || configured.version === null) {
22309
+ return {
22310
+ ok: false,
22311
+ ...configured,
22312
+ cachedVersion: null,
22313
+ packageDir: null,
22314
+ error: `[supertask] OpenCode \u63D2\u4EF6\u5FC5\u987B\u56FA\u5B9A\u7CBE\u786E\u7248\u672C\uFF0C\u4E0D\u80FD\u4F7F\u7528 ${configured.spec}`
22315
+ };
22316
+ }
22317
+ try {
22318
+ const installed = resolveInstalledPluginVersion(configured.version);
22319
+ return {
22320
+ ok: true,
22321
+ ...configured,
22322
+ cachedVersion: installed.version,
22323
+ packageDir: installed.packageDir,
22324
+ error: null
22325
+ };
22326
+ } catch (error) {
22327
+ return {
22328
+ ok: false,
22329
+ ...configured,
22330
+ cachedVersion: null,
22331
+ packageDir: null,
22332
+ error: error instanceof Error ? error.message : String(error)
22333
+ };
22334
+ }
22335
+ }
22336
+ function installPluginVersion(version2) {
22337
+ if (!isSemanticVersion(version2)) {
22338
+ throw new Error(`[supertask] OpenCode \u63D2\u4EF6\u7248\u672C\u65E0\u6548: ${version2}`);
22339
+ }
22340
+ const result = spawnSync3(opencodeBin(), [
22341
+ "plugin",
22342
+ `${PACKAGE_NAME}@${version2}`,
22343
+ "--global",
22344
+ "--force"
22345
+ ], {
22346
+ encoding: "utf8",
22347
+ env: process.env,
22348
+ timeout: 12e4
22349
+ });
22350
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
22351
+ if (result.error) {
22352
+ throw new Error(`[supertask] OpenCode \u63D2\u4EF6\u66F4\u65B0\u5931\u8D25: ${result.error.message}`);
22353
+ }
22354
+ if (result.status !== 0) {
22355
+ throw new Error(`[supertask] OpenCode \u63D2\u4EF6\u66F4\u65B0\u5931\u8D25: ${output || `\u9000\u51FA\u7801 ${result.status}`}`);
22356
+ }
22357
+ return resolveInstalledPluginVersion(version2);
22358
+ }
22359
+ function installLatestPlugin() {
22360
+ return installPluginVersion(latestVersion());
22361
+ }
22362
+ var PACKAGE_NAME;
22363
+ var init_update2 = __esm({
22364
+ "src/daemon/update.ts"() {
22365
+ "use strict";
22366
+ init_semver();
22367
+ PACKAGE_NAME = "opencode-supertask";
22368
+ }
22369
+ });
22370
+
22371
+ // src/gateway/health.ts
22372
+ function initializeGatewayHealth(config) {
22373
+ const now = Date.now();
22374
+ state = {
22375
+ startedAt: now,
22376
+ config,
22377
+ lastActivityAt: {
22378
+ worker: now,
22379
+ scheduler: now,
22380
+ watchdog: now,
22381
+ watchdogCleanup: now
22382
+ },
22383
+ lastSuccessAt: {
22384
+ worker: now,
22385
+ scheduler: now,
22386
+ watchdog: now,
22387
+ watchdogCleanup: now
22388
+ },
22389
+ consecutiveFailures: {
22390
+ worker: 0,
22391
+ scheduler: 0,
22392
+ watchdog: 0,
22393
+ watchdogCleanup: 0
22394
+ },
22395
+ lastError: {
22396
+ worker: null,
22397
+ scheduler: null,
22398
+ watchdog: null,
22399
+ watchdogCleanup: null
22400
+ }
22401
+ };
22402
+ }
22403
+ function markGatewayActivity(component) {
22404
+ if (state) state.lastActivityAt[component] = Date.now();
22405
+ }
22406
+ function markGatewaySuccess(component) {
22407
+ if (!state) return;
22408
+ const now = Date.now();
22409
+ state.lastActivityAt[component] = now;
22410
+ state.lastSuccessAt[component] = now;
22411
+ state.consecutiveFailures[component] = 0;
22412
+ }
22413
+ function markGatewayFailure(component, error) {
22414
+ if (!state) return;
22415
+ const now = Date.now();
22416
+ state.lastActivityAt[component] = now;
22417
+ state.consecutiveFailures[component] += 1;
22418
+ state.lastError[component] = {
22419
+ at: now,
22420
+ message: error instanceof Error ? error.message : String(error)
22421
+ };
22422
+ }
22423
+ function resetGatewayHealth() {
22424
+ state = null;
22425
+ }
22426
+ function componentStatus(component, enabled, maxAgeMs, now) {
22427
+ const lastActivityAt = state?.lastActivityAt[component] ?? null;
22428
+ const ageMs = lastActivityAt == null ? null : Math.max(0, now - lastActivityAt);
22429
+ const consecutiveFailures = state?.consecutiveFailures[component] ?? 0;
22430
+ return {
22431
+ enabled,
22432
+ healthy: !enabled || ageMs != null && ageMs <= maxAgeMs && consecutiveFailures === 0,
22433
+ lastActivityAt,
22434
+ lastSuccessAt: state?.lastSuccessAt[component] ?? null,
22435
+ consecutiveFailures,
22436
+ lastError: state?.lastError[component] ?? null,
22437
+ ageMs,
22438
+ maxAgeMs
22439
+ };
22440
+ }
22441
+ function getGatewayHealth(now = Date.now()) {
22442
+ const worker = componentStatus(
22443
+ "worker",
22444
+ true,
22445
+ Math.max((state?.config.workerPollIntervalMs ?? 1e3) * 5, 5e3),
22446
+ now
22447
+ );
22448
+ const scheduler = componentStatus(
22449
+ "scheduler",
22450
+ state?.config.schedulerEnabled ?? false,
22451
+ Math.max((state?.config.schedulerCheckIntervalMs ?? 1e3) * 5, 5e3),
22452
+ now
22453
+ );
22454
+ const watchdog = componentStatus(
22455
+ "watchdog",
22456
+ true,
22457
+ Math.max((state?.config.watchdogCheckIntervalMs ?? 6e4) * 3, 5e3),
22458
+ now
22459
+ );
22460
+ const watchdogCleanup = componentStatus(
22461
+ "watchdogCleanup",
22462
+ true,
22463
+ Math.max((state?.config.watchdogCleanupIntervalMs ?? 864e5) * 2, 6e4),
22464
+ now
22465
+ );
22466
+ let lock = { pid: null, heartbeatAt: null, readyAt: null, ageMs: null, healthy: false };
22467
+ try {
22468
+ const row = sqlite.prepare(
22469
+ "SELECT pid, heartbeat_at, ready_at FROM gateway_lock WHERE id = 1"
22470
+ ).get();
22471
+ if (row) {
22472
+ const ageMs = Math.max(0, now - row.heartbeat_at);
22473
+ lock = {
22474
+ pid: row.pid,
22475
+ heartbeatAt: row.heartbeat_at,
22476
+ readyAt: row.ready_at,
22477
+ ageMs,
22478
+ healthy: row.pid === process.pid && row.ready_at != null && ageMs < LOCK_STALE_MS
22479
+ };
22480
+ }
22481
+ } catch {
22482
+ }
22483
+ const healthy = state != null && lock.healthy && worker.healthy && scheduler.healthy && watchdog.healthy && watchdogCleanup.healthy;
22484
+ return {
22485
+ status: healthy ? "ok" : "degraded",
22486
+ pid: process.pid,
22487
+ uptimeMs: state ? Math.max(0, now - state.startedAt) : 0,
22488
+ lock,
22489
+ components: { worker, scheduler, watchdog, watchdogCleanup }
22490
+ };
22491
+ }
22492
+ var LOCK_STALE_MS, state;
22493
+ var init_health = __esm({
22494
+ "src/gateway/health.ts"() {
22495
+ "use strict";
22496
+ init_db2();
22497
+ LOCK_STALE_MS = 3e4;
22498
+ state = null;
22499
+ }
22500
+ });
22501
+
22502
+ // src/worker/index.ts
22503
+ import { spawn } from "child_process";
22504
+ import { fileURLToPath as fileURLToPath5 } from "url";
22505
+ import { existsSync as existsSync7 } from "fs";
22506
+ import { dirname as dirname7, join as join6 } from "path";
22507
+ import { randomUUID as randomUUID2 } from "crypto";
22508
+ function assertWorkerProcessIsolationSupported(platform = process.platform) {
22509
+ if (platform === "win32") {
22510
+ throw new Error(
22511
+ "Windows Worker \u5DF2\u5B89\u5168\u7981\u7528\uFF1A\u5F53\u524D\u8FD0\u884C\u65F6\u65E0\u6CD5\u7528 Job Object \u8BC1\u660E\u6574\u4E2A OpenCode \u8FDB\u7A0B\u6811\u5DF2\u9000\u51FA"
22512
+ );
21999
22513
  }
22000
22514
  }
22001
22515
  var DEFAULT_MAX_OUTPUT_CHARS, FORBIDDEN_AGENT, WorkerEngine;
@@ -22007,6 +22521,8 @@ var init_worker = __esm({
22007
22521
  init_health();
22008
22522
  init_process_control();
22009
22523
  init_launch_protocol();
22524
+ init_task_working_directory();
22525
+ init_task_batch();
22010
22526
  DEFAULT_MAX_OUTPUT_CHARS = 64 * 1024;
22011
22527
  FORBIDDEN_AGENT = "supertask-runner";
22012
22528
  WorkerEngine = class {
@@ -22112,7 +22628,8 @@ var init_worker = __esm({
22112
22628
  if (!task) break;
22113
22629
  if (this.stopped) break;
22114
22630
  if (!await TaskService.start(task.id)) continue;
22115
- if (task.batchId) this.activeBatchIds.add(task.batchId);
22631
+ const batchId = normalizeTaskBatchId(task.batchId);
22632
+ if (batchId) this.activeBatchIds.add(batchId);
22116
22633
  if (this.stopped) {
22117
22634
  await TaskService.resetRunningToPending([task.id]);
22118
22635
  this.releaseBatch(task);
@@ -22143,6 +22660,14 @@ var init_worker = __esm({
22143
22660
  this.releaseBatch(task);
22144
22661
  continue;
22145
22662
  }
22663
+ try {
22664
+ validateTaskWorkingDirectory(task.cwd);
22665
+ } catch (error) {
22666
+ const message = error instanceof Error ? error.message : String(error);
22667
+ await TaskService.failRun(task.id, run.id, message, { setDeadLetter: true });
22668
+ this.releaseBatch(task);
22669
+ continue;
22670
+ }
22146
22671
  await this.spawnTask(task, run.id, launchIdentity);
22147
22672
  } catch (err) {
22148
22673
  this.releaseBatch(task);
@@ -22169,10 +22694,10 @@ var init_worker = __esm({
22169
22694
  const extension = import.meta.url.endsWith(".ts") ? "ts" : "js";
22170
22695
  const moduleDir = dirname7(fileURLToPath5(import.meta.url));
22171
22696
  const candidates = [
22172
- join5(moduleDir, `launcher.${extension}`),
22173
- join5(moduleDir, `../worker/launcher.${extension}`)
22697
+ join6(moduleDir, `launcher.${extension}`),
22698
+ join6(moduleDir, `../worker/launcher.${extension}`)
22174
22699
  ];
22175
- const entry = candidates.find((candidate) => existsSync6(candidate));
22700
+ const entry = candidates.find((candidate) => existsSync7(candidate));
22176
22701
  if (!entry) throw new Error(`Worker launcher \u4E0D\u5B58\u5728\uFF1A${candidates.join(", ")}`);
22177
22702
  return entry;
22178
22703
  }
@@ -22228,6 +22753,11 @@ var init_worker = __esm({
22228
22753
  child.on("message", (message) => {
22229
22754
  if (isMatchingDrainProof(message, entry.launchIdentity)) {
22230
22755
  entry.guardianDrained = true;
22756
+ try {
22757
+ child.send(drainProofAckForIdentity(entry.launchIdentity));
22758
+ } catch (error) {
22759
+ this.logError("drain proof acknowledgment failed", error, task.id);
22760
+ }
22231
22761
  }
22232
22762
  });
22233
22763
  let spawnError = null;
@@ -22507,7 +23037,8 @@ ${output}` : ""}` : output;
22507
23037
  );
22508
23038
  }
22509
23039
  releaseBatch(task) {
22510
- if (task.batchId) this.activeBatchIds.delete(task.batchId);
23040
+ const batchId = normalizeTaskBatchId(task.batchId);
23041
+ if (batchId) this.activeBatchIds.delete(batchId);
22511
23042
  }
22512
23043
  resolveModel(taskModel) {
22513
23044
  if (!taskModel || taskModel === "default") return null;
@@ -22828,8 +23359,11 @@ var init_watchdog = __esm({
22828
23359
  });
22829
23360
 
22830
23361
  // src/gateway/scheduler/job-templates.ts
22831
- async function cloneTaskFromTemplate(templateId) {
22832
- return createTaskFromTemplate(templateId, { advanceSchedule: true });
23362
+ async function cloneTaskFromTemplate(templateId, expectedNextRunAt) {
23363
+ return createTaskFromTemplate(templateId, {
23364
+ advanceSchedule: true,
23365
+ expectedNextRunAt
23366
+ });
22833
23367
  }
22834
23368
  async function triggerTaskFromTemplate(templateId) {
22835
23369
  return createTaskFromTemplate(templateId, {
@@ -22842,37 +23376,40 @@ function createTaskFromTemplate(templateId, options) {
22842
23376
  return db.transaction((tx) => {
22843
23377
  const tmpl = tx.select().from(taskTemplates3).where(eq(taskTemplates3.id, templateId)).limit(1).get();
22844
23378
  if (!tmpl || options.advanceSchedule && !tmpl.enabled) return null;
22845
- const activeTasks = tx.select({ count: sql`count(*)` }).from(schema_exports.tasks).where(and(
22846
- eq(schema_exports.tasks.templateId, templateId),
22847
- sql`${schema_exports.tasks.status} IN ('pending', 'running')`
22848
- )).get();
22849
- const retryableTasks = tx.select({ count: sql`count(*)` }).from(schema_exports.tasks).where(and(
22850
- eq(schema_exports.tasks.templateId, templateId),
22851
- eq(schema_exports.tasks.status, "failed"),
22852
- sql`${schema_exports.tasks.retryCount} <= ${schema_exports.tasks.maxRetries}`
22853
- )).get();
22854
- const detachedRuns = tx.select({ count: sql`count(DISTINCT ${schema_exports.taskRuns.taskId})` }).from(schema_exports.taskRuns).innerJoin(schema_exports.tasks, eq(schema_exports.tasks.id, schema_exports.taskRuns.taskId)).where(and(
22855
- eq(schema_exports.taskRuns.status, "running"),
22856
- eq(schema_exports.tasks.templateId, templateId),
22857
- sql`NOT (
22858
- ${schema_exports.tasks.status} IN ('pending', 'running')
22859
- OR (
22860
- ${schema_exports.tasks.status} = 'failed'
22861
- AND ${schema_exports.tasks.retryCount} <= ${schema_exports.tasks.maxRetries}
22862
- )
22863
- )`
22864
- )).get();
22865
- const activeCount = Number(activeTasks?.count ?? 0) + Number(retryableTasks?.count ?? 0) + Number(detachedRuns?.count ?? 0);
22866
- if (activeCount >= (tmpl.maxInstances ?? 1)) {
22867
- if (options.advanceSchedule && tmpl.scheduleType !== "delayed") {
22868
- const nextRunAt2 = TaskTemplateService.calculateNextRunAt(
22869
- tmpl.scheduleType,
22870
- tmpl,
22871
- nowMs
22872
- );
22873
- tx.update(taskTemplates3).set({ nextRunAt: nextRunAt2, updatedAt: nowMs }).where(and(eq(taskTemplates3.id, templateId), eq(taskTemplates3.enabled, true))).run();
23379
+ if (options.advanceSchedule && options.expectedNextRunAt !== void 0 && tmpl.nextRunAt !== options.expectedNextRunAt) return null;
23380
+ if (options.advanceSchedule) {
23381
+ const activeTasks = tx.select({ count: sql`count(*)` }).from(schema_exports.tasks).where(and(
23382
+ eq(schema_exports.tasks.templateId, templateId),
23383
+ sql`${schema_exports.tasks.status} IN ('pending', 'running')`
23384
+ )).get();
23385
+ const retryableTasks = tx.select({ count: sql`count(*)` }).from(schema_exports.tasks).where(and(
23386
+ eq(schema_exports.tasks.templateId, templateId),
23387
+ eq(schema_exports.tasks.status, "failed"),
23388
+ sql`${schema_exports.tasks.retryCount} <= ${schema_exports.tasks.maxRetries}`
23389
+ )).get();
23390
+ const detachedRuns = tx.select({ count: sql`count(DISTINCT ${schema_exports.taskRuns.taskId})` }).from(schema_exports.taskRuns).innerJoin(schema_exports.tasks, eq(schema_exports.tasks.id, schema_exports.taskRuns.taskId)).where(and(
23391
+ eq(schema_exports.taskRuns.status, "running"),
23392
+ eq(schema_exports.tasks.templateId, templateId),
23393
+ sql`NOT (
23394
+ ${schema_exports.tasks.status} IN ('pending', 'running')
23395
+ OR (
23396
+ ${schema_exports.tasks.status} = 'failed'
23397
+ AND ${schema_exports.tasks.retryCount} <= ${schema_exports.tasks.maxRetries}
23398
+ )
23399
+ )`
23400
+ )).get();
23401
+ const activeCount = Number(activeTasks?.count ?? 0) + Number(retryableTasks?.count ?? 0) + Number(detachedRuns?.count ?? 0);
23402
+ if (activeCount >= (tmpl.maxInstances ?? 1)) {
23403
+ if (tmpl.scheduleType !== "delayed") {
23404
+ const nextRunAt2 = TaskTemplateService.calculateNextRunAt(
23405
+ tmpl.scheduleType,
23406
+ tmpl,
23407
+ nowMs
23408
+ );
23409
+ tx.update(taskTemplates3).set({ nextRunAt: nextRunAt2, updatedAt: nowMs }).where(and(eq(taskTemplates3.id, templateId), eq(taskTemplates3.enabled, true))).run();
23410
+ }
23411
+ return null;
22874
23412
  }
22875
- return null;
22876
23413
  }
22877
23414
  const isDelayed = tmpl.scheduleType === "delayed";
22878
23415
  const nextRunAt = isDelayed ? null : TaskTemplateService.calculateNextRunAt(
@@ -22889,7 +23426,7 @@ function createTaskFromTemplate(templateId, options) {
22889
23426
  category: tmpl.category ?? "general",
22890
23427
  importance: tmpl.importance ?? 3,
22891
23428
  urgency: tmpl.urgency ?? 3,
22892
- batchId: tmpl.batchId,
23429
+ batchId: normalizeTaskBatchId(tmpl.batchId),
22893
23430
  maxRetries: tmpl.maxRetries ?? 3,
22894
23431
  retryBackoffMs: tmpl.retryBackoffMs ?? 3e4,
22895
23432
  timeoutMs: tmpl.timeoutMs,
@@ -22950,6 +23487,7 @@ var init_job_templates = __esm({
22950
23487
  init_db2();
22951
23488
  init_drizzle_orm();
22952
23489
  init_task_template_service();
23490
+ init_task_batch();
22953
23491
  ({ taskTemplates: taskTemplates3 } = schema_exports);
22954
23492
  DUE_TEMPLATE_BATCH_SIZE = 100;
22955
23493
  }
@@ -22999,7 +23537,7 @@ var init_scheduler = __esm({
22999
23537
  const lastTemplate = dueTemplates.at(-1);
23000
23538
  for (const tmpl of dueTemplates) {
23001
23539
  try {
23002
- const task = await cloneTaskFromTemplate(tmpl.id);
23540
+ const task = await cloneTaskFromTemplate(tmpl.id, tmpl.nextRunAt);
23003
23541
  if (task) {
23004
23542
  console.log(JSON.stringify({
23005
23543
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -25287,6 +25825,14 @@ function statusText(locale, status) {
25287
25825
  const key = `status.${status}`;
25288
25826
  return key in ZH ? t(locale, key) : t(locale, "status.unknown");
25289
25827
  }
25828
+ function runStatusText(locale, status) {
25829
+ const key = `runStatus.${status}`;
25830
+ return key in ZH ? t(locale, key) : t(locale, "status.unknown");
25831
+ }
25832
+ function resolveEditedRunAt(originalEpoch, originalLocal, currentLocal) {
25833
+ if (originalEpoch !== null && originalLocal === currentLocal) return originalEpoch;
25834
+ return new Date(currentLocal).getTime();
25835
+ }
25290
25836
  function formatRelative(timestamp2, locale) {
25291
25837
  if (!timestamp2) return "\u2014";
25292
25838
  const deltaMs = timestamp2 - Date.now();
@@ -25363,13 +25909,31 @@ function clientMessages(locale) {
25363
25909
  "dialog.clearTitle",
25364
25910
  "dialog.clearBody",
25365
25911
  "dialog.clearInstruction",
25912
+ "dialog.restartGateway",
25913
+ "dialog.restartGatewayBody",
25914
+ "dialog.restartGatewayRunningBody",
25366
25915
  "feedback.retryFailed",
25367
25916
  "feedback.cancelFailed",
25368
25917
  "feedback.deleteFailed",
25369
25918
  "feedback.requestFailed",
25370
25919
  "feedback.triggered",
25920
+ "feedback.taskCreated",
25921
+ "feedback.taskUpdated",
25922
+ "task.projectExisting",
25923
+ "task.projectNew",
25924
+ "task.createTitle",
25925
+ "task.editTitle",
25926
+ "action.saveTask",
25927
+ "action.updateTask",
25371
25928
  "feedback.configSaved",
25372
25929
  "feedback.databaseCleared",
25930
+ "feedback.templateCreated",
25931
+ "feedback.templateUpdated",
25932
+ "feedback.sessionCommandCopied",
25933
+ "feedback.restarting",
25934
+ "feedback.restartTimeout",
25935
+ "template.createTitle",
25936
+ "template.editTitle",
25373
25937
  "filter.noResults"
25374
25938
  ];
25375
25939
  return Object.fromEntries(keys.map((key) => [key, t(locale, key)]));
@@ -25463,6 +26027,7 @@ function renderLayout(options) {
25463
26027
  themeMedia.addEventListener?.('change',()=>{if((localStorage.getItem('supertask-theme')||'system')==='system')applyTheme('system');});
25464
26028
  applyTheme(localStorage.getItem('supertask-theme')||'system');
25465
26029
  requestAnimationFrame(()=>document.documentElement.classList.add('ui-ready'));
26030
+ ${resolveEditedRunAt.toString()}
25466
26031
  function refreshPage(button){button.classList.add('refreshing');button.setAttribute('aria-label','${t(locale, "a11y.refreshing")}');location.reload();}
25467
26032
  function showToast(message,type='ok'){const region=document.getElementById('toast-region');const node=document.createElement('div');node.className='toast '+type;node.innerHTML=(type==='error'?'${icon("alert")}':'${icon("check")}')+'<span></span>';node.querySelector('span').textContent=message;region.appendChild(node);setTimeout(()=>{node.classList.add('leaving');setTimeout(()=>node.remove(),220)},3600);}
25468
26033
  async function readJson(response){const data=await response.json().catch(()=>({}));if(!response.ok)throw new Error(data.error||text('feedback.requestFailed'));return data;}
@@ -25474,6 +26039,22 @@ function renderLayout(options) {
25474
26039
  async function showRecord(url){try{const data=await readJson(await fetch(url));document.getElementById('detail-content').textContent=JSON.stringify(data,null,2);document.getElementById('detail-dialog').showModal()}catch(error){showToast(error.message,'error')}}
25475
26040
  const showDetail=id=>showRecord('/api/tasks/'+id);const showRunDetail=id=>showRecord('/api/runs/'+id);const showTemplateDetail=id=>showRecord('/api/templates/'+id);
25476
26041
  async function copyDetails(){try{await navigator.clipboard.writeText(document.getElementById('detail-content').textContent);showToast(text('details.copySuccess'))}catch{showToast(text('feedback.copyFailed'),'error')}}
26042
+ async function copySessionCommand(id){try{const data=await readJson(await fetch('/api/runs/'+id+'/session-command'));await navigator.clipboard.writeText(data.command);showToast(text('feedback.sessionCommandCopied'))}catch(error){showToast(error.message||text('feedback.copyFailed'),'error')}}
26043
+ function taskField(name){return document.getElementById('task-'+name)}
26044
+ function taskProjects(){const node=document.getElementById('task-project-data');if(!node)return {};try{return JSON.parse(node.textContent||'{}')}catch{return {}}}
26045
+ function updateTaskProjectStatus(){const node=taskField('project-status');if(!node)return;const cwd=taskField('cwd').value.trim();if(!cwd){node.textContent='';return}const project=taskProjects()[cwd];node.textContent=project?text('task.projectExisting',project):text('task.projectNew')}
26046
+ function openTaskCreator(){const form=document.getElementById('task-form');form.reset();taskField('id').value='';taskField('cwd').readOnly=false;taskField('cwd').value=form.dataset.defaultCwd||'';taskField('dialog-title').textContent=text('task.createTitle');taskField('save').textContent=text('action.saveTask');updateTaskProjectStatus();document.getElementById('task-dialog').showModal();setTimeout(()=>taskField('name').focus(),50)}
26047
+ async function openTaskEditor(id){try{const data=await readJson(await fetch('/api/tasks/'+id));taskField('id').value=String(id);taskField('name').value=data.name||'';taskField('cwd').value=data.cwd||'';taskField('cwd').readOnly=true;taskField('agent').value=data.agent||'';taskField('model').value=data.model||'default';taskField('prompt').value=data.prompt||'';taskField('category').value=data.category||'general';taskField('batch').value=data.batchId||'';taskField('importance').value=String(data.importance??3);taskField('urgency').value=String(data.urgency??3);taskField('max-retries').value=String(data.maxRetries??3);taskField('retry-backoff').value=durationInput(data.retryBackoffMs??30000);taskField('timeout').value=durationInput(data.timeoutMs);taskField('dialog-title').textContent=text('task.editTitle');taskField('save').textContent=text('action.updateTask');updateTaskProjectStatus();document.getElementById('task-dialog').showModal();setTimeout(()=>taskField('name').focus(),50)}catch(error){showToast(error.message,'error')}}
26048
+ async function saveTask(event){event.preventDefault();const form=document.getElementById('task-form');if(!form.reportValidity())return;const id=taskField('id').value;const body={name:taskField('name').value,cwd:taskField('cwd').value,agent:taskField('agent').value,model:taskField('model').value,prompt:taskField('prompt').value,category:taskField('category').value,batchId:taskField('batch').value,importance:Number(taskField('importance').value),urgency:Number(taskField('urgency').value),maxRetries:Number(taskField('max-retries').value),retryBackoff:taskField('retry-backoff').value,timeout:taskField('timeout').value};const button=taskField('save');button.disabled=true;try{const data=await readJson(await fetch(id?'/api/tasks/'+id:'/api/tasks',{method:id?'PUT':'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}));showToast(text(id?'feedback.taskUpdated':'feedback.taskCreated',{id:data.task.id}));document.getElementById('task-dialog').close();setTimeout(()=>location.assign(id?location.href:'/?cwd='+encodeURIComponent(data.task.cwd||'')),450)}catch(error){showToast(error.message,'error')}finally{button.disabled=false}}
26049
+ function templateField(name){return document.getElementById('template-'+name)}
26050
+ function durationInput(milliseconds){if(milliseconds==null)return '';if(milliseconds===0)return '0';const units=[['d',86400000],['h',3600000],['min',60000],['s',1000],['ms',1]];for(const [unit,factor] of units){if(milliseconds%factor===0)return String(milliseconds/factor)+unit}return String(milliseconds)+'ms'}
26051
+ function localDateTime(milliseconds){const date=new Date(milliseconds);const local=new Date(milliseconds-date.getTimezoneOffset()*60000);return local.toISOString().slice(0,23)}
26052
+ function updateTemplateScheduleFields(){const type=templateField('schedule-type').value;const fields={cron:templateField('cron-field'),recurring:templateField('interval-field'),delayed:templateField('run-at-field')};for(const [name,node] of Object.entries(fields)){node.hidden=name!==type;node.querySelector('input').required=name===type}}
26053
+ function setOriginalRunAt(epoch){const input=templateField('run-at');const local=epoch?localDateTime(epoch):'';input.value=local;input.dataset.originalEpoch=epoch?String(epoch):'';input.dataset.originalLocal=local}
26054
+ function selectedRunAt(){const input=templateField('run-at');return resolveEditedRunAt(input.dataset.originalEpoch?Number(input.dataset.originalEpoch):null,input.dataset.originalLocal||'',input.value)}
26055
+ function openTemplateCreator(){const form=document.getElementById('template-form');form.reset();templateField('id').value='';templateField('dialog-title').textContent=text('template.createTitle');setOriginalRunAt(null);templateField('run-at').value=localDateTime(Date.now()+3600000);updateTemplateScheduleFields();document.getElementById('template-dialog').showModal();setTimeout(()=>templateField('name').focus(),50)}
26056
+ async function openTemplateEditor(id){try{const data=await readJson(await fetch('/api/templates/'+id));templateField('id').value=String(id);templateField('dialog-title').textContent=text('template.editTitle');templateField('name').value=data.name||'';templateField('cwd').value=data.cwd||'';templateField('agent').value=data.agent||'';templateField('model').value=data.model||'default';templateField('prompt').value=data.prompt||'';templateField('schedule-type').value=data.scheduleType;templateField('cron').value=data.cronExpr||'';templateField('interval').value=durationInput(data.intervalMs);setOriginalRunAt(data.runAt||null);if(!data.runAt)templateField('run-at').value=localDateTime(Date.now()+3600000);templateField('category').value=data.category||'general';templateField('batch').value=data.batchId||'';templateField('importance').value=String(data.importance??3);templateField('urgency').value=String(data.urgency??3);templateField('max-instances').value=String(data.maxInstances??1);templateField('max-retries').value=String(data.maxRetries??3);templateField('retry-backoff').value=durationInput(data.retryBackoffMs??30000);templateField('timeout').value=durationInput(data.timeoutMs);updateTemplateScheduleFields();document.getElementById('template-dialog').showModal();setTimeout(()=>templateField('name').focus(),50)}catch(error){showToast(error.message,'error')}}
26057
+ async function saveTemplate(event){event.preventDefault();const form=document.getElementById('template-form');if(!form.reportValidity())return;const id=templateField('id').value;const type=templateField('schedule-type').value;const body={name:templateField('name').value,cwd:templateField('cwd').value,agent:templateField('agent').value,model:templateField('model').value,prompt:templateField('prompt').value,scheduleType:type,cronExpr:templateField('cron').value,interval:templateField('interval').value,runAt:type==='delayed'?selectedRunAt():null,category:templateField('category').value,batchId:templateField('batch').value,importance:Number(templateField('importance').value),urgency:Number(templateField('urgency').value),maxInstances:Number(templateField('max-instances').value),maxRetries:Number(templateField('max-retries').value),retryBackoff:templateField('retry-backoff').value,timeout:templateField('timeout').value};const button=templateField('save');button.disabled=true;try{await readJson(await fetch(id?'/api/templates/'+id:'/api/templates',{method:id?'PUT':'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}));showToast(text(id?'feedback.templateUpdated':'feedback.templateCreated'));document.getElementById('template-dialog').close();setTimeout(()=>location.assign(id?location.href:'/templates'),450)}catch(error){showToast(error.message,'error')}finally{button.disabled=false}}
25477
26058
  async function enableTmpl(id){try{await readJson(await fetch('/api/templates/'+id+'/enable',{method:'POST'}));location.reload()}catch(error){showToast(error.message,'error')}}
25478
26059
  async function disableTmpl(id){if(!await ask(text('dialog.disableTemplate'),text('dialog.disableTemplateBody')))return;try{await readJson(await fetch('/api/templates/'+id+'/disable',{method:'POST'}));location.reload()}catch(error){showToast(error.message,'error')}}
25479
26060
  async function deleteTmpl(id){if(!await ask(text('dialog.deleteTemplate'),text('dialog.deleteTemplateBody'),true))return;try{await readJson(await fetch('/api/templates/'+id,{method:'DELETE'}));location.reload()}catch(error){showToast(error.message,'error')}}
@@ -25481,7 +26062,9 @@ function renderLayout(options) {
25481
26062
  function toggleLog(id,button){const panel=document.getElementById('log-'+id);const hidden=!panel.hidden;panel.hidden=hidden;button.setAttribute('aria-expanded',String(!hidden));button.textContent=text(hidden?'action.logs':'action.hideLogs');}
25482
26063
  function filterTasks(value){const query=value.trim().toLocaleLowerCase();let visible=0;document.querySelectorAll('[data-task-row]').forEach(row=>{const match=!query||row.dataset.search.toLocaleLowerCase().includes(query);row.hidden=!match;if(match)visible++});const empty=document.getElementById('search-empty');if(empty)empty.hidden=visible!==0;}
25483
26064
  async function clearDatabase(){if(!await askDanger())return;try{const data=await readJson(await fetch('/api/database/clear',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({confirmation:'CLEAR'})}));showToast(text('feedback.databaseCleared',{path:data.backupPath}));setTimeout(()=>location.reload(),1000)}catch(error){showToast(error.message,'error')}}
25484
- async function saveConfig(){const form=document.getElementById('config-form');const data={worker:{maxConcurrency:Number(form.mc.value),pollIntervalMs:Number(form.pi.value),heartbeatIntervalMs:Number(form.hi.value)*1000,taskTimeoutMs:Number(form.to.value)*60000},scheduler:{enabled:form.se.checked,checkIntervalMs:Number(form.si.value)},watchdog:{heartbeatTimeoutMs:Number(form.wt.value)*1000,checkIntervalMs:Number(form.wci.value)*1000,cleanupIntervalMs:Number(form.wcl.value)*3600000,retentionDays:Number(form.rd.value)}};try{await readJson(await fetch('/api/config',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)}));showToast(text('feedback.configSaved'))}catch(error){showToast(error.message,'error')}}
26065
+ async function confirmGatewayRestart(runningCount=0){const body=runningCount>0?text('dialog.restartGatewayRunningBody',{count:runningCount}):text('dialog.restartGatewayBody');return await ask(text('dialog.restartGateway'),body)}
26066
+ async function restartGateway(){try{const data=await readJson(await fetch('/api/gateway/restart',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({confirmation:'RESTART'})}));showToast(text('feedback.restarting'));for(let attempt=0;attempt<120;attempt++){await new Promise(resolve=>setTimeout(resolve,500));try{const status=await readJson(await fetch('/api/gateway/status',{cache:'no-store'}));if(status.pid!==data.previousPid&&status.managed&&status.ready&&!status.restartRequired){location.reload();return true}}catch{}}showToast(text('feedback.restartTimeout'),'error');return false}catch(error){showToast(error.message,'error');return false}}
26067
+ async function saveConfig(restartAfterSave=false,runningCount=0){if(restartAfterSave&&!await confirmGatewayRestart(runningCount))return;const form=document.getElementById('config-form');const data={worker:{maxConcurrency:Number(form.mc.value),pollIntervalMs:Number(form.pi.value),heartbeatIntervalMs:Number(form.hi.value)*1000,taskTimeoutMs:Number(form.to.value)*60000},scheduler:{enabled:form.se.checked,checkIntervalMs:Number(form.si.value)},watchdog:{heartbeatTimeoutMs:Number(form.wt.value)*1000,checkIntervalMs:Number(form.wci.value)*1000,cleanupIntervalMs:Number(form.wcl.value)*3600000,retentionDays:Number(form.rd.value)}};try{await readJson(await fetch('/api/config',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)}));showToast(text('feedback.configSaved'));if(restartAfterSave){const restarted=await restartGateway();if(!restarted)setTimeout(()=>location.reload(),500)}else{setTimeout(()=>location.reload(),500)}}catch(error){showToast(error.message,'error')}}
25485
26068
  </script>
25486
26069
  </body>
25487
26070
  </html>`;
@@ -25493,17 +26076,17 @@ var init_ui = __esm({
25493
26076
  ZH = {
25494
26077
  "app.name": "SuperTask",
25495
26078
  "app.dashboard": "\u63A7\u5236\u53F0",
25496
- "app.tagline": "\u53EF\u9760\u7684\u672C\u5730 Agent \u8C03\u5EA6\u4E2D\u5FC3",
26079
+ "app.tagline": "\u53EF\u9760\u7684\u672C\u5730 Agent \u4EFB\u52A1\u4E2D\u5FC3",
25497
26080
  "app.local": "\u672C\u5730\u8FD0\u884C",
25498
26081
  "app.footer": "Local-first \xB7 \u6570\u636E\u7559\u5728\u4F60\u7684\u8BBE\u5907\u4E0A",
25499
26082
  "nav.tasks": "\u4EFB\u52A1",
25500
- "nav.templates": "\u8C03\u5EA6",
26083
+ "nav.templates": "\u5B9A\u65F6\u4EFB\u52A1",
25501
26084
  "nav.runs": "\u6267\u884C\u8BB0\u5F55",
25502
26085
  "nav.system": "\u7CFB\u7EDF",
25503
26086
  "page.tasks.title": "\u4EFB\u52A1\u961F\u5217",
25504
26087
  "page.tasks.description": "\u67E5\u770B\u4F18\u5148\u7EA7\u3001\u6267\u884C\u72B6\u6001\u4E0E\u91CD\u8BD5\u60C5\u51B5\uFF0C\u5FEB\u901F\u5904\u7406\u9700\u8981\u5173\u6CE8\u7684\u4EFB\u52A1\u3002",
25505
- "page.templates.title": "\u8C03\u5EA6\u6A21\u677F",
25506
- "page.templates.description": "\u7BA1\u7406 Cron\u3001\u5EF6\u8FDF\u548C\u5FAA\u73AF\u4EFB\u52A1\uFF0C\u8BA9\u91CD\u590D\u5DE5\u4F5C\u6309\u8BA1\u5212\u81EA\u52A8\u8FD0\u884C\u3002",
26088
+ "page.templates.title": "\u5B9A\u65F6\u4EFB\u52A1",
26089
+ "page.templates.description": "\u5728\u7F51\u9875\u521B\u5EFA\u548C\u7F16\u8F91 Cron\u3001\u56FA\u5B9A\u95F4\u9694\u53CA\u4E00\u6B21\u6027\u4EFB\u52A1\uFF0C\u5305\u62EC\u6A21\u578B\u3001Agent \u548C\u63D0\u793A\u8BCD\u3002",
25507
26090
  "page.runs.title": "\u6267\u884C\u8BB0\u5F55",
25508
26091
  "page.runs.description": "\u8FFD\u8E2A\u6BCF\u6B21 Agent \u6267\u884C\u7684\u72B6\u6001\u3001\u8017\u65F6\u3001\u5FC3\u8DF3\u4E0E\u8F93\u51FA\u3002",
25509
26092
  "page.system.title": "\u7CFB\u7EDF\u8BBE\u7F6E",
@@ -25513,29 +26096,43 @@ var init_ui = __esm({
25513
26096
  "action.retry": "\u91CD\u8BD5",
25514
26097
  "action.cancel": "\u53D6\u6D88",
25515
26098
  "action.delete": "\u5220\u9664",
26099
+ "action.edit": "\u7F16\u8F91",
25516
26100
  "action.enable": "\u542F\u7528",
25517
26101
  "action.disable": "\u7981\u7528",
25518
- "action.trigger": "\u7ACB\u5373\u89E6\u53D1",
26102
+ "action.trigger": "\u7ACB\u5373\u8FD0\u884C\u4E00\u6B21",
26103
+ "action.continueSession": "\u7EE7\u7EED\u4F1A\u8BDD",
25519
26104
  "action.logs": "\u67E5\u770B\u65E5\u5FD7",
25520
26105
  "action.hideLogs": "\u6536\u8D77\u65E5\u5FD7",
25521
26106
  "action.save": "\u4FDD\u5B58\u8BBE\u7F6E",
26107
+ "action.saveAndRestart": "\u4FDD\u5B58\u5E76\u91CD\u542F",
25522
26108
  "action.copy": "\u590D\u5236 JSON",
25523
26109
  "action.close": "\u5173\u95ED",
25524
26110
  "action.confirm": "\u786E\u8BA4",
25525
26111
  "action.clearDatabase": "\u6E05\u7A7A\u6570\u636E\u5E93",
26112
+ "action.createTask": "\u65B0\u5EFA\u4EFB\u52A1",
26113
+ "action.saveTask": "\u52A0\u5165\u961F\u5217",
26114
+ "action.updateTask": "\u4FDD\u5B58\u4FEE\u6539",
26115
+ "action.createTemplate": "\u65B0\u5EFA\u5B9A\u65F6\u4EFB\u52A1",
26116
+ "action.saveTemplate": "\u4FDD\u5B58\u5B9A\u65F6\u4EFB\u52A1",
25526
26117
  "status.pending": "\u5F85\u6267\u884C",
25527
26118
  "status.running": "\u8FD0\u884C\u4E2D",
25528
26119
  "status.done": "\u5DF2\u5B8C\u6210",
25529
- "status.failed": "\u5931\u8D25",
25530
- "status.dead_letter": "\u6B7B\u4FE1",
26120
+ "status.failed": "\u7B49\u5F85\u91CD\u8BD5",
26121
+ "status.dead_letter": "\u5DF2\u505C\u6B62",
26122
+ "status.deadLetterHint": "\u7CFB\u7EDF\u4E0D\u4F1A\u518D\u81EA\u52A8\u8FD0\u884C\u3002\u53EF\u80FD\u662F\u91CD\u8BD5\u6B21\u6570\u5DF2\u7528\u5B8C\uFF0C\u6216\u4F9D\u8D56\u4EFB\u52A1\u65E0\u6CD5\u7EE7\u7EED\uFF1B\u8BF7\u67E5\u770B\u5931\u8D25\u539F\u56E0\uFF0C\u786E\u8BA4\u540E\u624B\u52A8\u91CD\u8BD5\u3002",
26123
+ "status.deadLetterAction": "\u9700\u68C0\u67E5\u539F\u56E0\u540E\u624B\u52A8\u91CD\u8BD5",
25531
26124
  "status.cancelled": "\u5DF2\u53D6\u6D88",
26125
+ "status.executionStillActive": "\u6267\u884C\u8FDB\u7A0B\u4ECD\u5728\u9000\u51FA\uFF0C\u6682\u65F6\u5360\u7528\u5E76\u53D1",
25532
26126
  "status.unknown": "\u672A\u77E5",
26127
+ "runStatus.running": "\u8FD0\u884C\u4E2D",
26128
+ "runStatus.done": "\u6210\u529F",
26129
+ "runStatus.failed": "\u5931\u8D25",
25533
26130
  "stats.total": "\u603B\u4EFB\u52A1",
25534
26131
  "stats.pending": "\u5F85\u6267\u884C",
25535
26132
  "stats.running": "\u8FD0\u884C\u4E2D",
25536
26133
  "stats.done": "\u5DF2\u5B8C\u6210",
25537
- "stats.failedDead": "\u5931\u8D25\u4E0E\u6B7B\u4FE1",
25538
- "stats.templates": "\u6A21\u677F\u603B\u6570",
26134
+ "stats.failedDead": "\u7B49\u5F85\u91CD\u8BD5\u4E0E\u5DF2\u505C\u6B62",
26135
+ "stats.templates": "\u5B9A\u65F6\u4EFB\u52A1\u603B\u6570",
25539
26136
  "stats.enabled": "\u5DF2\u542F\u7528",
25540
26137
  "stats.disabled": "\u5DF2\u7981\u7528",
25541
26138
  "stats.records": "\u6267\u884C\u603B\u6570",
@@ -25543,7 +26140,7 @@ var init_ui = __esm({
25543
26140
  "stats.pageFailed": "\u672C\u9875\u5931\u8D25",
25544
26141
  "stats.pageRunning": "\u672C\u9875\u8FD0\u884C\u4E2D",
25545
26142
  "filter.all": "\u5168\u90E8",
25546
- "filter.searchTasks": "\u641C\u7D22\u5F53\u524D\u9875\u7684\u4EFB\u52A1\u3001Agent \u6216\u63D0\u793A\u8BCD",
26143
+ "filter.searchTasks": "\u641C\u7D22\u5F53\u524D\u9875\u7684\u4EFB\u52A1\u3001\u9879\u76EE\u3001\u6279\u6B21\u3001Agent \u6216\u63D0\u793A\u8BCD",
25547
26144
  "filter.noResults": "\u6CA1\u6709\u7B26\u5408\u5F53\u524D\u641C\u7D22\u6761\u4EF6\u7684\u4EFB\u52A1",
25548
26145
  "table.id": "ID",
25549
26146
  "table.task": "\u4EFB\u52A1",
@@ -25559,7 +26156,7 @@ var init_ui = __esm({
25559
26156
  "table.nextRun": "\u4E0B\u6B21\u6267\u884C",
25560
26157
  "table.run": "Run",
25561
26158
  "table.heartbeat": "\u5FC3\u8DF3",
25562
- "table.session": "Session",
26159
+ "table.session": "\u4F1A\u8BDD",
25563
26160
  "table.model": "\u6A21\u578B",
25564
26161
  "table.startedAt": "\u542F\u52A8\u65F6\u95F4",
25565
26162
  "table.pid": "PID",
@@ -25567,14 +26164,14 @@ var init_ui = __esm({
25567
26164
  "pagination.next": "\u4E0B\u4E00\u9875",
25568
26165
  "pagination.summary": "\u7B2C {page} \u9875\uFF0C\u5171 {pages} \u9875 \xB7 {total} \u6761",
25569
26166
  "empty.tasks": "\u961F\u5217\u91CC\u8FD8\u6CA1\u6709\u4EFB\u52A1",
25570
- "empty.tasksHint": "\u901A\u8FC7 OpenCode \u63D2\u4EF6\u6216 supertask add \u521B\u5EFA\u7B2C\u4E00\u4E2A\u4EFB\u52A1\u3002",
25571
- "empty.templates": "\u8FD8\u6CA1\u6709\u8C03\u5EA6\u6A21\u677F",
25572
- "empty.templatesHint": "\u4F7F\u7528 CLI \u521B\u5EFA\uFF1Asupertask template add",
26167
+ "empty.tasksHint": "\u70B9\u51FB\u201C\u65B0\u5EFA\u4EFB\u52A1\u201D\uFF0C\u6216\u901A\u8FC7 OpenCode \u63D2\u4EF6\u548C supertask add \u521B\u5EFA\u7B2C\u4E00\u4E2A\u4EFB\u52A1\u3002",
26168
+ "empty.templates": "\u8FD8\u6CA1\u6709\u5B9A\u65F6\u4EFB\u52A1",
26169
+ "empty.templatesHint": "\u70B9\u51FB\u53F3\u4E0A\u89D2\u201C\u65B0\u5EFA\u5B9A\u65F6\u4EFB\u52A1\u201D\u5F00\u59CB\u521B\u5EFA\u3002",
25573
26170
  "empty.runs": "\u8FD8\u6CA1\u6709\u6267\u884C\u8BB0\u5F55",
25574
26171
  "empty.running": "\u5F53\u524D\u6CA1\u6709\u8FD0\u884C\u4E2D\u7684\u4EFB\u52A1",
25575
26172
  "schedule.cron": "Cron",
25576
- "schedule.recurring": "\u5FAA\u73AF",
25577
- "schedule.delayed": "\u5EF6\u8FDF",
26173
+ "schedule.recurring": "\u56FA\u5B9A\u95F4\u9694",
26174
+ "schedule.delayed": "\u4E00\u6B21\u6027",
25578
26175
  "schedule.unknown": "\u672A\u77E5",
25579
26176
  "schedule.enabled": "\u5DF2\u542F\u7528",
25580
26177
  "schedule.disabled": "\u5DF2\u7981\u7528",
@@ -25583,14 +26180,14 @@ var init_ui = __esm({
25583
26180
  "schedule.hours": "{count} \u5C0F\u65F6",
25584
26181
  "schedule.days": "{count} \u5929",
25585
26182
  "schedule.overdue": "\u5DF2\u5230\u671F",
25586
- "system.worker": "Worker",
25587
- "system.scheduler": "Scheduler",
25588
- "system.watchdog": "Watchdog",
26183
+ "system.worker": "\u4EFB\u52A1\u6267\u884C",
26184
+ "system.scheduler": "\u5B9A\u65F6\u4EFB\u52A1\u670D\u52A1",
26185
+ "system.watchdog": "\u8FD0\u884C\u76D1\u63A7",
25589
26186
  "system.maxConcurrency": "\u6700\u5927\u5E76\u53D1",
25590
26187
  "system.pollInterval": "\u8F6E\u8BE2\u95F4\u9694",
25591
26188
  "system.heartbeatInterval": "\u5FC3\u8DF3\u95F4\u9694",
25592
26189
  "system.taskTimeout": "\u4EFB\u52A1\u8D85\u65F6",
25593
- "system.schedulerEnabled": "\u542F\u7528\u8C03\u5EA6",
26190
+ "system.schedulerEnabled": "\u542F\u7528\u5B9A\u65F6\u4EFB\u52A1",
25594
26191
  "system.checkInterval": "\u68C0\u67E5\u95F4\u9694",
25595
26192
  "system.heartbeatTimeout": "\u5FC3\u8DF3\u8D85\u65F6",
25596
26193
  "system.cleanupInterval": "\u6E05\u7406\u95F4\u9694",
@@ -25600,8 +26197,12 @@ var init_ui = __esm({
25600
26197
  "system.minutes": "\u5206\u949F",
25601
26198
  "system.hours": "\u5C0F\u65F6",
25602
26199
  "system.days": "\u5929",
25603
- "system.activeTemplates": "\u6D3B\u8DC3\u6A21\u677F",
25604
- "system.saveHint": "\u4FDD\u5B58\u540E\u9700\u91CD\u542F Gateway \u751F\u6548",
26200
+ "system.activeTemplates": "\u5DF2\u542F\u7528\u5B9A\u65F6\u4EFB\u52A1",
26201
+ "system.saveHint": "\u8BBE\u7F6E\u4FDD\u5B58\u540E\u9700\u8981\u91CD\u542F Gateway \u624D\u80FD\u751F\u6548\u3002",
26202
+ "system.configApplied": "\u9875\u9762\u4E2D\u7684\u8BBE\u7F6E\u6B63\u5728\u751F\u6548\u3002",
26203
+ "system.configPending": "\u8BBE\u7F6E\u5DF2\u4FDD\u5B58\uFF0C\u4F46\u5F53\u524D Gateway \u4ECD\u5728\u4F7F\u7528\u4E0A\u6B21\u542F\u52A8\u65F6\u7684\u8BBE\u7F6E\u3002",
26204
+ "system.configForeground": "\u6B64\u9875\u9762\u672A\u8FDE\u63A5\u5230 Gateway \u7684\u8FD0\u884C\u914D\u7F6E\uFF1B\u4FDD\u5B58\u540E\u8BF7\u91CD\u542F Gateway\u3002",
26205
+ "system.configRestartManually": "\u8BBE\u7F6E\u5DF2\u4FDD\u5B58\u4F46\u5C1A\u672A\u751F\u6548\u3002\u8BF7\u56DE\u5230\u542F\u52A8 Gateway \u7684\u7EC8\u7AEF\uFF0C\u6309 Ctrl-C \u540E\u91CD\u65B0\u8FD0\u884C supertask gateway\u3002",
25605
26206
  "system.runningTasks": "\u5F53\u524D\u8FD0\u884C\u4EFB\u52A1\uFF08{running} / {limit} \u5E76\u53D1\uFF09",
25606
26207
  "system.taskStats": "\u4EFB\u52A1\u6982\u89C8",
25607
26208
  "system.configFile": "\u914D\u7F6E\u6587\u4EF6",
@@ -25610,7 +26211,46 @@ var init_ui = __esm({
25610
26211
  "system.yes": "\u662F",
25611
26212
  "system.noDefault": "\u5426\uFF0C\u5F53\u524D\u4F7F\u7528\u9ED8\u8BA4\u503C",
25612
26213
  "system.danger": "\u5371\u9669\u64CD\u4F5C",
25613
- "system.dangerDescription": "\u7CFB\u7EDF\u4F1A\u5148\u521B\u5EFA\u53EF\u6821\u9A8C\u5907\u4EFD\uFF0C\u518D\u4E8B\u52A1\u6027\u6E05\u7A7A\u4EFB\u52A1\u3001\u6267\u884C\u8BB0\u5F55\u548C\u8C03\u5EA6\u6A21\u677F\uFF1B\u5B58\u5728\u8FD0\u884C\u4EFB\u52A1\u65F6\u4F1A\u62D2\u7EDD\u64CD\u4F5C\u3002",
26214
+ "system.dangerDescription": "\u7CFB\u7EDF\u4F1A\u5148\u521B\u5EFA\u53EF\u6821\u9A8C\u5907\u4EFD\uFF0C\u518D\u4E8B\u52A1\u6027\u6E05\u7A7A\u4EFB\u52A1\u3001\u6267\u884C\u8BB0\u5F55\u548C\u5B9A\u65F6\u4EFB\u52A1\uFF1B\u5B58\u5728\u8FD0\u884C\u4EFB\u52A1\u65F6\u4F1A\u62D2\u7EDD\u64CD\u4F5C\u3002",
26215
+ "template.createTitle": "\u65B0\u5EFA\u5B9A\u65F6\u4EFB\u52A1",
26216
+ "template.editTitle": "\u7F16\u8F91\u5B9A\u65F6\u4EFB\u52A1",
26217
+ "template.formSubtitle": "\u8BBE\u7F6E\u4F55\u65F6\u8FD0\u884C\uFF0C\u4EE5\u53CA OpenCode \u8981\u4F7F\u7528\u7684\u9879\u76EE\u3001\u6A21\u578B\u548C\u63D0\u793A\u8BCD\u3002",
26218
+ "template.name": "\u540D\u79F0",
26219
+ "template.cwd": "\u9879\u76EE\u76EE\u5F55",
26220
+ "template.cwdHint": "OpenCode \u4F1A\u5728\u8FD9\u4E2A\u76EE\u5F55\u4E2D\u6267\u884C\u4EFB\u52A1\u3002",
26221
+ "template.agent": "Agent",
26222
+ "template.model": "\u6A21\u578B",
26223
+ "template.prompt": "\u63D0\u793A\u8BCD",
26224
+ "template.scheduleType": "\u6267\u884C\u65B9\u5F0F",
26225
+ "template.cronExpr": "Cron \u8868\u8FBE\u5F0F",
26226
+ "template.cronHint": "\u4F8B\u5982\uFF1A0 9 * * *\uFF08\u6BCF\u5929 09:00\uFF09",
26227
+ "template.interval": "\u6267\u884C\u95F4\u9694",
26228
+ "template.runAt": "\u6267\u884C\u65F6\u95F4",
26229
+ "template.durationHint": "\u652F\u6301 30s\u30015min\u30011h\u30012d",
26230
+ "template.advanced": "\u66F4\u591A\u6267\u884C\u8BBE\u7F6E",
26231
+ "template.category": "\u5206\u7C7B",
26232
+ "template.batchId": "\u6279\u6B21 ID",
26233
+ "template.importance": "\u91CD\u8981\u7A0B\u5EA6\uFF081-5\uFF09",
26234
+ "template.urgency": "\u7D27\u6025\u7A0B\u5EA6\uFF081-5\uFF09",
26235
+ "template.maxInstances": "\u81EA\u52A8\u8C03\u5EA6\u4E0A\u9650",
26236
+ "template.maxInstancesHint": "\u4EC5\u9650\u5236\u81EA\u52A8\u8C03\u5EA6\uFF1B\u624B\u52A8\u201C\u7ACB\u5373\u8FD0\u884C\u4E00\u6B21\u201D\u59CB\u7EC8\u52A0\u5165\u961F\u5217\u3002\u6D3B\u8DC3\u4EFB\u52A1\u5305\u542B\u5F85\u6267\u884C\u3001\u8FD0\u884C\u4E2D\u548C\u7B49\u5F85\u91CD\u8BD5\u3002",
26237
+ "template.maxRetries": "\u5931\u8D25\u91CD\u8BD5\u6B21\u6570",
26238
+ "template.retryBackoff": "\u91CD\u8BD5\u7B49\u5F85",
26239
+ "template.timeout": "\u5355\u6B21\u8D85\u65F6",
26240
+ "template.optional": "\u53EF\u9009",
26241
+ "template.futureOnly": "\u4FDD\u5B58\u540E\u7684\u8BBE\u7F6E\u7528\u4E8E\u4EE5\u540E\u751F\u6210\u7684\u4EFB\u52A1\uFF1B\u7F16\u8F91\u4E0D\u4F1A\u6539\u53D8\u5DF2\u7ECF\u8FDB\u5165\u961F\u5217\u6216\u6B63\u5728\u6267\u884C\u7684\u4EFB\u52A1\u3002",
26242
+ "projects.title": "\u9879\u76EE\u5206\u7EC4",
26243
+ "projects.description": "\u9879\u76EE\u76EE\u5F55\u662F\u9694\u79BB\u952E\uFF1B\u5148\u770B\u6BCF\u4E2A\u9879\u76EE\u6709\u6CA1\u6709\u8FD0\u884C\u4E2D\u6216\u6392\u961F\u4EFB\u52A1\uFF0C\u518D\u51B3\u5B9A\u662F\u5426\u7EE7\u7EED\u6DFB\u52A0\u3002",
26244
+ "projects.all": "\u5168\u90E8\u9879\u76EE",
26245
+ "projects.legacy": "\u672A\u5206\u7EC4",
26246
+ "projects.legacyHint": "\u65E7\u7248\u672C\u4E2D\u6CA1\u6709\u9879\u76EE\u76EE\u5F55\u7684\u4EFB\u52A1\uFF1B\u53EF\u67E5\u770B\u3001\u53D6\u6D88\u6216\u5220\u9664\uFF0C\u9700\u8981\u4FEE\u6539\u65F6\u8BF7\u5728\u6B63\u786E\u9879\u76EE\u76EE\u5F55\u4E0B\u91CD\u65B0\u521B\u5EFA\u3002",
26247
+ "projects.counts": "\u8FD0\u884C {running} \xB7 \u6392\u961F {pending} \xB7 \u5F02\u5E38 {failed}",
26248
+ "task.createTitle": "\u65B0\u5EFA\u4EFB\u52A1",
26249
+ "task.editTitle": "\u7F16\u8F91\u4EFB\u52A1",
26250
+ "task.formSubtitle": "\u4EFB\u52A1\u4F1A\u7ACB\u5373\u52A0\u5165\u6301\u4E45\u961F\u5217\uFF1B\u5E76\u53D1\u5DF2\u6EE1\u65F6\u4F1A\u81EA\u52A8\u7B49\u5F85\u3002",
26251
+ "task.batchHint": "\u76F8\u540C\u975E\u7A7A\u6279\u6B21 ID \u7684\u4EFB\u52A1\u4E25\u683C\u4E32\u884C\uFF1B\u7559\u7A7A\u5219\u4E0D\u53D7\u6279\u6B21\u4E32\u884C\u9650\u5236\uFF0C\u4F46\u4ECD\u53D7\u5168\u5C40\u5E76\u53D1\u548C\u4F9D\u8D56\u7EA6\u675F\u3002",
26252
+ "task.projectExisting": "\u6B64\u9879\u76EE\u73B0\u6709 {total} \u4E2A\u4EFB\u52A1\uFF1A\u8FD0\u884C {running}\uFF0C\u6392\u961F {pending}\uFF0C\u5F02\u5E38 {failed}\u3002",
26253
+ "task.projectNew": "\u8FD9\u662F\u4E00\u4E2A\u65B0\u9879\u76EE\u5206\u7EC4\uFF1B\u521B\u5EFA\u540E\u4F1A\u51FA\u73B0\u5728\u9879\u76EE\u5217\u8868\u4E2D\u3002",
25614
26254
  "theme.label": "\u4E3B\u9898",
25615
26255
  "theme.system": "\u8DDF\u968F\u7CFB\u7EDF",
25616
26256
  "theme.light": "\u6D45\u8272",
@@ -25625,21 +26265,31 @@ var init_ui = __esm({
25625
26265
  "dialog.retryTaskBody": "\u4EFB\u52A1\u5C06\u56DE\u5230\u5F85\u6267\u884C\u72B6\u6001\uFF0C\u5E76\u91CD\u7F6E\u81EA\u52A8\u91CD\u8BD5\u9884\u7B97\u3002",
25626
26266
  "dialog.deleteTask": "\u5220\u9664\u4EFB\u52A1 #{id}\uFF1F",
25627
26267
  "dialog.deleteTaskBody": "\u4EFB\u52A1\u53CA\u5173\u8054\u6267\u884C\u8BB0\u5F55\u5C06\u6C38\u4E45\u5220\u9664\uFF0C\u6B64\u64CD\u4F5C\u65E0\u6CD5\u64A4\u9500\u3002",
25628
- "dialog.disableTemplate": "\u7981\u7528\u8FD9\u4E2A\u8C03\u5EA6\u6A21\u677F\uFF1F",
25629
- "dialog.disableTemplateBody": "\u6A21\u677F\u5C06\u505C\u6B62\u81EA\u52A8\u521B\u5EFA\u65B0\u4EFB\u52A1\uFF0C\u5DF2\u6709\u4EFB\u52A1\u4E0D\u53D7\u5F71\u54CD\u3002",
25630
- "dialog.deleteTemplate": "\u5220\u9664\u8FD9\u4E2A\u8C03\u5EA6\u6A21\u677F\uFF1F",
25631
- "dialog.deleteTemplateBody": "\u6A21\u677F\u914D\u7F6E\u5C06\u6C38\u4E45\u5220\u9664\uFF0C\u6B64\u64CD\u4F5C\u65E0\u6CD5\u64A4\u9500\u3002",
25632
- "dialog.triggerTemplate": "\u7ACB\u5373\u89E6\u53D1\u4E00\u6B21\uFF1F",
25633
- "dialog.triggerTemplateBody": "\u7CFB\u7EDF\u4F1A\u6309\u5F53\u524D\u6A21\u677F\u521B\u5EFA\u4E00\u4E2A\u65B0\u4EFB\u52A1\uFF0C\u5E76\u9075\u5B88\u6700\u5927\u5B9E\u4F8B\u9650\u5236\u3002",
26268
+ "dialog.disableTemplate": "\u7981\u7528\u8FD9\u4E2A\u5B9A\u65F6\u4EFB\u52A1\uFF1F",
26269
+ "dialog.disableTemplateBody": "\u5B83\u5C06\u505C\u6B62\u81EA\u52A8\u521B\u5EFA\u65B0\u4EFB\u52A1\uFF0C\u5DF2\u6709\u4EFB\u52A1\u4E0D\u53D7\u5F71\u54CD\u3002",
26270
+ "dialog.deleteTemplate": "\u5220\u9664\u8FD9\u4E2A\u5B9A\u65F6\u4EFB\u52A1\uFF1F",
26271
+ "dialog.deleteTemplateBody": "\u5B9A\u65F6\u4EFB\u52A1\u914D\u7F6E\u5C06\u6C38\u4E45\u5220\u9664\uFF0C\u6B64\u64CD\u4F5C\u65E0\u6CD5\u64A4\u9500\u3002",
26272
+ "dialog.triggerTemplate": "\u7ACB\u5373\u8FD0\u884C\u4E00\u6B21\uFF1F",
26273
+ "dialog.triggerTemplateBody": "\u7CFB\u7EDF\u4F1A\u6309\u5F53\u524D\u8BBE\u7F6E\u7ACB\u5373\u521B\u5EFA\u4E00\u4E2A\u4EFB\u52A1\u5E76\u52A0\u5165\u961F\u5217\uFF1B\u82E5\u5168\u5C40\u5E76\u53D1\u5DF2\u6EE1\uFF0C\u4EFB\u52A1\u4F1A\u7B49\u5F85\u6267\u884C\u3002",
25634
26274
  "dialog.clearTitle": "\u786E\u8BA4\u6E05\u7A7A\u6570\u636E\u5E93",
25635
- "dialog.clearBody": "\u8FD9\u4F1A\u5220\u9664\u5168\u90E8\u4EFB\u52A1\u3001\u6267\u884C\u8BB0\u5F55\u548C\u8C03\u5EA6\u6A21\u677F\u3002\u7CFB\u7EDF\u4F1A\u5148\u81EA\u52A8\u5907\u4EFD\u3002",
26275
+ "dialog.clearBody": "\u8FD9\u4F1A\u5220\u9664\u5168\u90E8\u4EFB\u52A1\u3001\u6267\u884C\u8BB0\u5F55\u548C\u5B9A\u65F6\u4EFB\u52A1\u3002\u7CFB\u7EDF\u4F1A\u5148\u81EA\u52A8\u5907\u4EFD\u3002",
25636
26276
  "dialog.clearInstruction": "\u8F93\u5165 CLEAR \u4EE5\u786E\u8BA4",
26277
+ "dialog.restartGateway": "\u91CD\u542F\u5E76\u5E94\u7528\u8BBE\u7F6E\uFF1F",
26278
+ "dialog.restartGatewayBody": "Gateway \u4F1A\u77ED\u6682\u79BB\u7EBF\uFF0C\u901A\u5E38\u6570\u79D2\u540E\u7531 PM2 \u81EA\u52A8\u6062\u590D\u3002",
26279
+ "dialog.restartGatewayRunningBody": "\u5F53\u524D\u6709 {count} \u4E2A\u4EFB\u52A1\u5728\u8FD0\u884C\u3002\u7CFB\u7EDF\u4F1A\u5148\u7B49\u5F85\u5B83\u4EEC\u5B8C\u6210\uFF1B\u8D85\u65F6\u540E\uFF0C\u672A\u5B8C\u6210\u4EFB\u52A1\u4F1A\u88AB\u5B89\u5168\u505C\u6B62\u5E76\u653E\u56DE\u961F\u5217\uFF0C\u7136\u540E Gateway \u7531 PM2 \u81EA\u52A8\u6062\u590D\u3002",
25637
26280
  "feedback.retryFailed": "\u91CD\u8BD5\u5931\u8D25",
25638
26281
  "feedback.cancelFailed": "\u53D6\u6D88\u5931\u8D25",
25639
26282
  "feedback.deleteFailed": "\u5220\u9664\u5931\u8D25",
25640
26283
  "feedback.requestFailed": "\u8BF7\u6C42\u5931\u8D25",
25641
26284
  "feedback.triggered": "\u5DF2\u521B\u5EFA\u4EFB\u52A1 #{id}",
25642
- "feedback.configSaved": "\u8BBE\u7F6E\u5DF2\u4FDD\u5B58\uFF0C\u91CD\u542F Gateway \u540E\u751F\u6548",
26285
+ "feedback.taskCreated": "\u4EFB\u52A1 #{id} \u5DF2\u52A0\u5165\u961F\u5217",
26286
+ "feedback.taskUpdated": "\u4EFB\u52A1 #{id} \u5DF2\u66F4\u65B0",
26287
+ "feedback.templateCreated": "\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u521B\u5EFA",
26288
+ "feedback.templateUpdated": "\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u66F4\u65B0",
26289
+ "feedback.configSaved": "\u8BBE\u7F6E\u5DF2\u4FDD\u5B58",
26290
+ "feedback.sessionCommandCopied": "\u4F1A\u8BDD\u547D\u4EE4\u5DF2\u590D\u5236\uFF0C\u53EF\u76F4\u63A5\u7C98\u8D34\u5230\u7EC8\u7AEF\u8FD0\u884C",
26291
+ "feedback.restarting": "Gateway \u6B63\u5728\u91CD\u542F\uFF0C\u8BF7\u7A0D\u5019\u2026",
26292
+ "feedback.restartTimeout": "Gateway \u5C1A\u672A\u6062\u590D\uFF0C\u8BF7\u7A0D\u540E\u5237\u65B0\u6216\u68C0\u67E5 PM2 \u72B6\u6001",
25643
26293
  "feedback.databaseCleared": "\u6570\u636E\u5E93\u5DF2\u6E05\u7A7A\uFF0C\u5907\u4EFD\u4F4D\u4E8E\uFF1A{path}",
25644
26294
  "feedback.copyFailed": "\u590D\u5236\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u9009\u62E9\u5185\u5BB9",
25645
26295
  "a11y.skip": "\u8DF3\u5230\u4E3B\u8981\u5185\u5BB9",
@@ -25652,13 +26302,13 @@ var init_ui = __esm({
25652
26302
  "app.local": "Running locally",
25653
26303
  "app.footer": "Local-first \xB7 Your data stays on this device",
25654
26304
  "nav.tasks": "Tasks",
25655
- "nav.templates": "Schedules",
26305
+ "nav.templates": "Scheduled tasks",
25656
26306
  "nav.runs": "Runs",
25657
26307
  "nav.system": "System",
25658
26308
  "page.tasks.title": "Task queue",
25659
26309
  "page.tasks.description": "Track priority, execution state, and retries, then act on tasks that need attention.",
25660
- "page.templates.title": "Schedule templates",
25661
- "page.templates.description": "Manage cron, delayed, and recurring work so repeated tasks run on time.",
26310
+ "page.templates.title": "Scheduled tasks",
26311
+ "page.templates.description": "Create and edit cron, fixed-interval, and one-time tasks, including their model, agent, and prompt.",
25662
26312
  "page.runs.title": "Execution history",
25663
26313
  "page.runs.description": "Inspect the status, duration, heartbeat, and output of every agent run.",
25664
26314
  "page.system.title": "System settings",
@@ -25668,29 +26318,43 @@ var init_ui = __esm({
25668
26318
  "action.retry": "Retry",
25669
26319
  "action.cancel": "Cancel",
25670
26320
  "action.delete": "Delete",
26321
+ "action.edit": "Edit",
25671
26322
  "action.enable": "Enable",
25672
26323
  "action.disable": "Disable",
25673
26324
  "action.trigger": "Run now",
26325
+ "action.continueSession": "Continue session",
25674
26326
  "action.logs": "View log",
25675
26327
  "action.hideLogs": "Hide log",
25676
26328
  "action.save": "Save settings",
26329
+ "action.saveAndRestart": "Save and restart",
25677
26330
  "action.copy": "Copy JSON",
25678
26331
  "action.close": "Close",
25679
26332
  "action.confirm": "Confirm",
25680
26333
  "action.clearDatabase": "Clear database",
26334
+ "action.createTask": "New task",
26335
+ "action.saveTask": "Add to queue",
26336
+ "action.updateTask": "Save changes",
26337
+ "action.createTemplate": "New scheduled task",
26338
+ "action.saveTemplate": "Save scheduled task",
25681
26339
  "status.pending": "Pending",
25682
26340
  "status.running": "Running",
25683
26341
  "status.done": "Done",
25684
- "status.failed": "Failed",
25685
- "status.dead_letter": "Dead letter",
26342
+ "status.failed": "Waiting to retry",
26343
+ "status.dead_letter": "Stopped",
26344
+ "status.deadLetterHint": "The system will not run this task automatically again. Its retries may be exhausted or a dependency cannot continue; inspect the error, then retry manually if appropriate.",
26345
+ "status.deadLetterAction": "Inspect the error, then retry manually",
25686
26346
  "status.cancelled": "Cancelled",
26347
+ "status.executionStillActive": "The execution process is still stopping and temporarily occupies a slot",
25687
26348
  "status.unknown": "Unknown",
26349
+ "runStatus.running": "Running",
26350
+ "runStatus.done": "Succeeded",
26351
+ "runStatus.failed": "Failed",
25688
26352
  "stats.total": "Total tasks",
25689
26353
  "stats.pending": "Pending",
25690
26354
  "stats.running": "Running",
25691
26355
  "stats.done": "Completed",
25692
- "stats.failedDead": "Failed & dead",
25693
- "stats.templates": "Templates",
26356
+ "stats.failedDead": "Waiting to retry & stopped",
26357
+ "stats.templates": "Scheduled tasks",
25694
26358
  "stats.enabled": "Enabled",
25695
26359
  "stats.disabled": "Disabled",
25696
26360
  "stats.records": "Total runs",
@@ -25698,7 +26362,7 @@ var init_ui = __esm({
25698
26362
  "stats.pageFailed": "Failed here",
25699
26363
  "stats.pageRunning": "Running here",
25700
26364
  "filter.all": "All",
25701
- "filter.searchTasks": "Search tasks, agents, or prompts on this page",
26365
+ "filter.searchTasks": "Search tasks, projects, batches, agents, or prompts on this page",
25702
26366
  "filter.noResults": "No tasks match this search",
25703
26367
  "table.id": "ID",
25704
26368
  "table.task": "Task",
@@ -25722,14 +26386,14 @@ var init_ui = __esm({
25722
26386
  "pagination.next": "Next",
25723
26387
  "pagination.summary": "Page {page} of {pages} \xB7 {total} items",
25724
26388
  "empty.tasks": "Your queue is empty",
25725
- "empty.tasksHint": "Create the first task with the OpenCode plugin or supertask add.",
25726
- "empty.templates": "No schedule templates yet",
25727
- "empty.templatesHint": "Create one with: supertask template add",
26389
+ "empty.tasksHint": "Select \u201CNew task\u201D, or use the OpenCode plugin or supertask add.",
26390
+ "empty.templates": "No scheduled tasks yet",
26391
+ "empty.templatesHint": "Select \u201CNew scheduled task\u201D to create one.",
25728
26392
  "empty.runs": "No execution history yet",
25729
26393
  "empty.running": "No tasks are running right now",
25730
26394
  "schedule.cron": "Cron",
25731
- "schedule.recurring": "Recurring",
25732
- "schedule.delayed": "Delayed",
26395
+ "schedule.recurring": "Fixed interval",
26396
+ "schedule.delayed": "One-time",
25733
26397
  "schedule.unknown": "Unknown",
25734
26398
  "schedule.enabled": "Enabled",
25735
26399
  "schedule.disabled": "Disabled",
@@ -25738,14 +26402,14 @@ var init_ui = __esm({
25738
26402
  "schedule.hours": "{count} hr",
25739
26403
  "schedule.days": "{count} days",
25740
26404
  "schedule.overdue": "Overdue",
25741
- "system.worker": "Worker",
25742
- "system.scheduler": "Scheduler",
25743
- "system.watchdog": "Watchdog",
26405
+ "system.worker": "Task execution",
26406
+ "system.scheduler": "Scheduled task service",
26407
+ "system.watchdog": "Runtime monitor",
25744
26408
  "system.maxConcurrency": "Max concurrency",
25745
26409
  "system.pollInterval": "Poll interval",
25746
26410
  "system.heartbeatInterval": "Heartbeat interval",
25747
26411
  "system.taskTimeout": "Task timeout",
25748
- "system.schedulerEnabled": "Enable scheduler",
26412
+ "system.schedulerEnabled": "Enable scheduled tasks",
25749
26413
  "system.checkInterval": "Check interval",
25750
26414
  "system.heartbeatTimeout": "Heartbeat timeout",
25751
26415
  "system.cleanupInterval": "Cleanup interval",
@@ -25755,8 +26419,12 @@ var init_ui = __esm({
25755
26419
  "system.minutes": "minutes",
25756
26420
  "system.hours": "hours",
25757
26421
  "system.days": "days",
25758
- "system.activeTemplates": "Active templates",
25759
- "system.saveHint": "Restart Gateway to apply saved settings",
26422
+ "system.activeTemplates": "Enabled scheduled tasks",
26423
+ "system.saveHint": "Restart Gateway to apply saved settings.",
26424
+ "system.configApplied": "The settings shown here are active.",
26425
+ "system.configPending": "Settings are saved, but this Gateway is still using the values from its last start.",
26426
+ "system.configForeground": "This page is not connected to Gateway runtime settings. Restart Gateway after saving.",
26427
+ "system.configRestartManually": "Settings are saved but not active. Return to the terminal that launched Gateway, press Ctrl-C, then run supertask gateway again.",
25760
26428
  "system.runningTasks": "Active tasks ({running} / {limit} concurrent)",
25761
26429
  "system.taskStats": "Task overview",
25762
26430
  "system.configFile": "Configuration file",
@@ -25766,6 +26434,45 @@ var init_ui = __esm({
25766
26434
  "system.noDefault": "No, using defaults",
25767
26435
  "system.danger": "Danger zone",
25768
26436
  "system.dangerDescription": "A verified backup is created first, then tasks, runs, and templates are cleared transactionally. Active work blocks this operation.",
26437
+ "template.createTitle": "New scheduled task",
26438
+ "template.editTitle": "Edit scheduled task",
26439
+ "template.formSubtitle": "Choose when to run and which project, model, and prompt OpenCode should use.",
26440
+ "template.name": "Name",
26441
+ "template.cwd": "Project directory",
26442
+ "template.cwdHint": "OpenCode runs the task in this directory.",
26443
+ "template.agent": "Agent",
26444
+ "template.model": "Model",
26445
+ "template.prompt": "Prompt",
26446
+ "template.scheduleType": "Schedule",
26447
+ "template.cronExpr": "Cron expression",
26448
+ "template.cronHint": "Example: 0 9 * * * (daily at 09:00)",
26449
+ "template.interval": "Interval",
26450
+ "template.runAt": "Run at",
26451
+ "template.durationHint": "Supports 30s, 5min, 1h, 2d",
26452
+ "template.advanced": "More execution settings",
26453
+ "template.category": "Category",
26454
+ "template.batchId": "Batch ID",
26455
+ "template.importance": "Importance (1-5)",
26456
+ "template.urgency": "Urgency (1-5)",
26457
+ "template.maxInstances": "Automatic scheduling limit",
26458
+ "template.maxInstancesHint": "Limits automatic scheduling only. \u201CRun now\u201D always queues a task. Active instances include pending, running, and retry-waiting tasks.",
26459
+ "template.maxRetries": "Failure retries",
26460
+ "template.retryBackoff": "Retry delay",
26461
+ "template.timeout": "Run timeout",
26462
+ "template.optional": "Optional",
26463
+ "template.futureOnly": "Saved settings apply to tasks created in the future. Editing does not change queued or running tasks.",
26464
+ "projects.title": "Projects",
26465
+ "projects.description": "The project directory is the isolation key. Check active and queued work before adding more.",
26466
+ "projects.all": "All projects",
26467
+ "projects.legacy": "Ungrouped",
26468
+ "projects.legacyHint": "Legacy tasks without a project directory. View, cancel, or delete them; recreate under the correct project to make changes.",
26469
+ "projects.counts": "Running {running} \xB7 queued {pending} \xB7 issues {failed}",
26470
+ "task.createTitle": "New task",
26471
+ "task.editTitle": "Edit task",
26472
+ "task.formSubtitle": "The task enters the durable queue immediately and waits automatically when concurrency is full.",
26473
+ "task.batchHint": "Tasks with the same non-empty batch ID run serially. Blank removes the batch constraint; global concurrency and dependencies still apply.",
26474
+ "task.projectExisting": "This project has {total} tasks: {running} running, {pending} queued, and {failed} with issues.",
26475
+ "task.projectNew": "This is a new project group. It appears in the project list after creation.",
25769
26476
  "theme.label": "Theme",
25770
26477
  "theme.system": "System",
25771
26478
  "theme.light": "Light",
@@ -25783,18 +26490,28 @@ var init_ui = __esm({
25783
26490
  "dialog.disableTemplate": "Disable this schedule?",
25784
26491
  "dialog.disableTemplateBody": "It will stop creating new tasks automatically. Existing tasks are unchanged.",
25785
26492
  "dialog.deleteTemplate": "Delete this schedule?",
25786
- "dialog.deleteTemplateBody": "This template configuration will be permanently deleted.",
26493
+ "dialog.deleteTemplateBody": "This scheduled task configuration will be permanently deleted.",
25787
26494
  "dialog.triggerTemplate": "Run this schedule now?",
25788
- "dialog.triggerTemplateBody": "A new task is created from the template, subject to its instance limit.",
26495
+ "dialog.triggerTemplateBody": "A task is queued immediately using the current settings. If global concurrency is full, it waits to run.",
25789
26496
  "dialog.clearTitle": "Confirm database clear",
25790
- "dialog.clearBody": "This deletes every task, run, and schedule template after creating a backup.",
26497
+ "dialog.clearBody": "This deletes every task, run, and scheduled task after creating a backup.",
25791
26498
  "dialog.clearInstruction": "Type CLEAR to confirm",
26499
+ "dialog.restartGateway": "Restart and apply settings?",
26500
+ "dialog.restartGatewayBody": "Gateway will be briefly unavailable and should recover through PM2 within a few seconds.",
26501
+ "dialog.restartGatewayRunningBody": "{count} tasks are running. The system waits for them first; after the grace period, unfinished tasks are stopped safely and returned to the queue before PM2 restores Gateway.",
25792
26502
  "feedback.retryFailed": "Retry failed",
25793
26503
  "feedback.cancelFailed": "Cancellation failed",
25794
26504
  "feedback.deleteFailed": "Delete failed",
25795
26505
  "feedback.requestFailed": "Request failed",
25796
26506
  "feedback.triggered": "Task #{id} created",
25797
- "feedback.configSaved": "Settings saved. Restart Gateway to apply them.",
26507
+ "feedback.taskCreated": "Task #{id} added to the queue",
26508
+ "feedback.taskUpdated": "Task #{id} updated",
26509
+ "feedback.templateCreated": "Scheduled task created",
26510
+ "feedback.templateUpdated": "Scheduled task updated",
26511
+ "feedback.configSaved": "Settings saved",
26512
+ "feedback.sessionCommandCopied": "Session command copied. Paste it into a terminal to continue.",
26513
+ "feedback.restarting": "Gateway is restarting\u2026",
26514
+ "feedback.restartTimeout": "Gateway has not recovered yet. Refresh later or check PM2 status.",
25798
26515
  "feedback.databaseCleared": "Database cleared. Backup: {path}",
25799
26516
  "feedback.copyFailed": "Copy failed. Select the content manually.",
25800
26517
  "a11y.skip": "Skip to main content",
@@ -25829,8 +26546,8 @@ var init_ui = __esm({
25829
26546
  radial-gradient(circle at 10% -10%,var(--bg-glow),transparent 34rem),var(--bg);
25830
26547
  font-family:Inter,ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
25831
26548
  font-size:14px; line-height:1.5; -webkit-font-smoothing:antialiased; }
25832
- button,input,select { font:inherit; }
25833
- button,a,select,input { -webkit-tap-highlight-color:transparent; }
26549
+ button,input,select,textarea { font:inherit; }
26550
+ button,a,select,input,textarea { -webkit-tap-highlight-color:transparent; }
25834
26551
  a { color:inherit; }
25835
26552
  code,.mono,.m { font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace; }
25836
26553
  .skip-link { position:fixed; left:16px; top:-60px; z-index:200; padding:10px 14px; border-radius:8px;
@@ -25911,6 +26628,16 @@ var init_ui = __esm({
25911
26628
  .panel-head { min-height:52px; display:flex; align-items:center; justify-content:space-between; gap:12px; padding:0 18px;
25912
26629
  border-bottom:1px solid var(--border); }
25913
26630
  .panel-head h2,.panel-head h3 { margin:0; font-size:14px; letter-spacing:-.01em; }
26631
+ .panel-head p { margin:3px 0 0; color:var(--text-2); font-size:11px; }
26632
+ .project-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(230px,1fr)); gap:10px; padding:14px; }
26633
+ .project-card { min-width:0; padding:12px; border:1px solid var(--border); border-radius:10px; color:var(--text); background:var(--surface-2); text-decoration:none; }
26634
+ .project-card:hover { border-color:var(--border-strong); background:var(--surface-3); }
26635
+ .project-card.active { border-color:color-mix(in srgb,var(--primary) 45%,var(--border)); background:var(--primary-soft); }
26636
+ .project-card-head { display:flex; align-items:center; justify-content:space-between; gap:10px; }
26637
+ .project-card-head strong { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
26638
+ .project-card-head span { color:var(--text-3); font-size:11px; }
26639
+ .project-path { margin-top:4px; overflow:hidden; color:var(--text-3); font-family:"SFMono-Regular",Consolas,monospace; font-size:10px; text-overflow:ellipsis; white-space:nowrap; }
26640
+ .project-counts { margin-top:8px; color:var(--text-2); font-size:11px; }
25914
26641
  .table-wrap { width:100%; overflow-x:auto; }
25915
26642
  table { width:100%; border-collapse:separate; border-spacing:0; font-size:13px; }
25916
26643
  th { height:42px; padding:0 13px; color:var(--text-3); background:var(--surface-2); border-bottom:1px solid var(--border);
@@ -25921,6 +26648,7 @@ var init_ui = __esm({
25921
26648
  tbody tr:hover { background:color-mix(in srgb,var(--primary-soft) 30%,transparent); }
25922
26649
  .task-name { font-weight:680; color:var(--text); }
25923
26650
  .task-prompt { max-width:520px; margin-top:3px; color:var(--text-2); font-size:12px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
26651
+ .task-context { margin-top:6px; }
25924
26652
  .muted,.mu { color:var(--text-2); }
25925
26653
  .faint { color:var(--text-3); }
25926
26654
  .small,.sm { font-size:12px; }
@@ -26004,6 +26732,18 @@ var init_ui = __esm({
26004
26732
  .dialog-head p { margin:3px 0 0; color:var(--text-2); font-size:11px; }
26005
26733
  .dialog-body { max-height:70vh; overflow:auto; padding:18px; }
26006
26734
  .dialog-actions { display:flex; align-items:center; justify-content:flex-end; gap:8px; padding:14px 18px; border-top:1px solid var(--border); }
26735
+ .template-dialog { width:min(880px,calc(100% - 32px)); }
26736
+ .template-form-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:15px; }
26737
+ .form-field { display:flex; min-width:0; flex-direction:column; gap:6px; color:var(--text-2); font-size:12px; font-weight:650; }
26738
+ .form-field-wide { grid-column:1 / -1; }
26739
+ .form-field input,.form-field select,.form-field textarea { width:100%; min-height:39px; padding:8px 10px; border:1px solid var(--border); border-radius:9px;
26740
+ outline:none; color:var(--text); background:var(--surface-2); font-weight:450; }
26741
+ .form-field textarea { resize:vertical; line-height:1.5; }
26742
+ .form-field input:focus,.form-field select:focus,.form-field textarea:focus { border-color:var(--primary); box-shadow:var(--focus); background:var(--surface); }
26743
+ .form-field small { color:var(--text-3); font-size:10px; font-weight:450; }
26744
+ .advanced-fields { margin-top:18px; padding-top:14px; border-top:1px solid var(--border); }
26745
+ .advanced-fields summary { margin-bottom:14px; color:var(--text-2); cursor:pointer; font-size:12px; font-weight:700; }
26746
+ .form-note { margin:16px 0 0; color:var(--text-3); font-size:11px; }
26007
26747
  .json-view { min-height:160px; margin:0; padding:15px; overflow:auto; border:1px solid var(--border); border-radius:10px; color:var(--text-2);
26008
26748
  background:var(--surface-2); font-size:12px; white-space:pre-wrap; overflow-wrap:anywhere; }
26009
26749
  .confirm-copy { color:var(--text-2); margin:0; }
@@ -26069,6 +26809,9 @@ var init_ui = __esm({
26069
26809
  .responsive-table td[data-primary]::before { display:none; }
26070
26810
  .responsive-table .task-prompt { max-width:100%; }
26071
26811
  .responsive-table .actions { justify-content:flex-start; }
26812
+ .project-grid { grid-template-columns:1fr; }
26813
+ .template-form-grid { grid-template-columns:1fr; }
26814
+ .form-field-wide { grid-column:auto; }
26072
26815
  }
26073
26816
  @media (max-width:520px) {
26074
26817
  .stats-grid,.stats-grid.three { grid-template-columns:1fr 1fr; }
@@ -26096,10 +26839,45 @@ var init_ui = __esm({
26096
26839
  var web_exports = {};
26097
26840
  __export(web_exports, {
26098
26841
  dashboardApp: () => dashboardApp,
26099
- default: () => web_default
26842
+ default: () => web_default,
26843
+ isSafeDashboardRestartTarget: () => isSafeDashboardRestartTarget,
26844
+ resolveDashboardConfigState: () => resolveDashboardConfigState,
26845
+ setDashboardRuntimeConfig: () => setDashboardRuntimeConfig
26100
26846
  });
26101
- import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync4, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "fs";
26102
- import { dirname as dirname8 } from "path";
26847
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync5, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "fs";
26848
+ import { basename as basename3, dirname as dirname8 } from "path";
26849
+ function setDashboardRuntimeConfig(config) {
26850
+ runtimeConfig = config === null ? null : structuredClone(config);
26851
+ }
26852
+ function isValidSessionId(value) {
26853
+ return typeof value === "string" && SESSION_ID_PATTERN.test(value);
26854
+ }
26855
+ function maskSessionId(value) {
26856
+ return isValidSessionId(value) ? `${value.slice(4, 7)}***${value.slice(-3)}` : "\u2014";
26857
+ }
26858
+ function sessionCommand(value) {
26859
+ if (!isValidSessionId(value)) throw new Error("session unavailable");
26860
+ return `opencode --session ${value}`;
26861
+ }
26862
+ function configsEqual(left, right) {
26863
+ return JSON.stringify(left) === JSON.stringify(right);
26864
+ }
26865
+ function resolveDashboardConfigState(runtimeAvailable, restartRequired, managedRestart) {
26866
+ if (!runtimeAvailable) return "foreground";
26867
+ if (!restartRequired) return "applied";
26868
+ return managedRestart ? "pending" : "manual";
26869
+ }
26870
+ function isSafeDashboardRestartTarget(diagnostic, currentPid) {
26871
+ return diagnostic.pm2Installed && diagnostic.processFound && diagnostic.status === "online" && diagnostic.pid === currentPid && diagnostic.ready && diagnostic.scopeMatches;
26872
+ }
26873
+ function canRestartCurrentGateway() {
26874
+ if (runtimeConfig === null) return false;
26875
+ try {
26876
+ return isSafeDashboardRestartTarget(getGatewayDiagnostic(), process.pid);
26877
+ } catch {
26878
+ return false;
26879
+ }
26880
+ }
26103
26881
  function parsePositiveInteger2(value) {
26104
26882
  if (!/^\d+$/.test(value)) return null;
26105
26883
  const parsed = Number(value);
@@ -26108,6 +26886,134 @@ function parsePositiveInteger2(value) {
26108
26886
  function parseTaskStatus2(value) {
26109
26887
  return TASK_STATUSES2.has(value) ? value : null;
26110
26888
  }
26889
+ function parseTaskPayload(value) {
26890
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
26891
+ throw new Error("\u8BF7\u6C42\u5185\u5BB9\u5FC5\u987B\u662F\u5BF9\u8C61");
26892
+ }
26893
+ const input = value;
26894
+ const requiredString = (name) => {
26895
+ const field = input[name];
26896
+ if (typeof field !== "string" || !field.trim()) throw new Error(`${name} \u4E0D\u80FD\u4E3A\u7A7A`);
26897
+ return field.trim();
26898
+ };
26899
+ const optionalString = (name, fallback = null) => {
26900
+ const field = input[name];
26901
+ if (field === void 0 || field === null || field === "") return fallback;
26902
+ if (typeof field !== "string") throw new Error(`${name} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`);
26903
+ return field.trim() || fallback;
26904
+ };
26905
+ const integer2 = (name, fallback) => {
26906
+ const field = input[name];
26907
+ if (field === void 0 || field === null || field === "") return fallback;
26908
+ if (typeof field !== "number" || !Number.isSafeInteger(field)) {
26909
+ throw new Error(`${name} \u5FC5\u987B\u662F\u6574\u6570`);
26910
+ }
26911
+ return field;
26912
+ };
26913
+ const duration = (name, fallback, allowZero = false) => {
26914
+ const raw2 = optionalString(name, fallback);
26915
+ if (raw2 === null) return null;
26916
+ if (allowZero && raw2 === "0") return 0;
26917
+ if (Number.isFinite(Number(raw2))) {
26918
+ throw new Error(`${name} \u5FC5\u987B\u5E26\u65F6\u95F4\u5355\u4F4D\uFF0C\u8BF7\u4F7F\u7528 30s\u30015min\u30011h \u6216 2d`);
26919
+ }
26920
+ const milliseconds = parseDuration(raw2);
26921
+ if (milliseconds === null || !Number.isSafeInteger(milliseconds)) {
26922
+ throw new Error(`${name} \u683C\u5F0F\u65E0\u6548\uFF0C\u8BF7\u4F7F\u7528 30s\u30015min\u30011h \u6216 2d`);
26923
+ }
26924
+ return milliseconds;
26925
+ };
26926
+ return {
26927
+ name: requiredString("name"),
26928
+ cwd: requiredString("cwd"),
26929
+ agent: requiredString("agent"),
26930
+ model: optionalString("model", "default"),
26931
+ prompt: requiredString("prompt"),
26932
+ category: optionalString("category", "general"),
26933
+ batchId: optionalString("batchId"),
26934
+ importance: integer2("importance", 3),
26935
+ urgency: integer2("urgency", 3),
26936
+ maxRetries: integer2("maxRetries", 3),
26937
+ retryBackoffMs: duration("retryBackoff", "30s", true),
26938
+ timeoutMs: duration("timeout", null)
26939
+ };
26940
+ }
26941
+ function editableTaskPayload(input) {
26942
+ return {
26943
+ name: input.name,
26944
+ agent: input.agent,
26945
+ model: input.model ?? "default",
26946
+ prompt: input.prompt,
26947
+ category: input.category ?? "general",
26948
+ batchId: input.batchId ?? null,
26949
+ importance: input.importance ?? 3,
26950
+ urgency: input.urgency ?? 3,
26951
+ maxRetries: input.maxRetries ?? 3,
26952
+ retryBackoffMs: input.retryBackoffMs ?? 3e4,
26953
+ timeoutMs: input.timeoutMs ?? null
26954
+ };
26955
+ }
26956
+ function parseTemplatePayload(value) {
26957
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
26958
+ throw new Error("\u8BF7\u6C42\u5185\u5BB9\u5FC5\u987B\u662F\u5BF9\u8C61");
26959
+ }
26960
+ const input = value;
26961
+ const requiredString = (name) => {
26962
+ const field = input[name];
26963
+ if (typeof field !== "string" || !field.trim()) throw new Error(`${name} \u4E0D\u80FD\u4E3A\u7A7A`);
26964
+ return field.trim();
26965
+ };
26966
+ const optionalString = (name, fallback = null) => {
26967
+ const field = input[name];
26968
+ if (field === void 0 || field === null || field === "") return fallback;
26969
+ if (typeof field !== "string") throw new Error(`${name} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`);
26970
+ return field.trim() || fallback;
26971
+ };
26972
+ const integer2 = (name, fallback) => {
26973
+ const field = input[name];
26974
+ if (field === void 0 || field === null || field === "") return fallback;
26975
+ if (typeof field !== "number" || !Number.isSafeInteger(field)) {
26976
+ throw new Error(`${name} \u5FC5\u987B\u662F\u6574\u6570`);
26977
+ }
26978
+ return field;
26979
+ };
26980
+ const duration = (name, fallback, allowZero = false) => {
26981
+ const raw2 = optionalString(name, fallback);
26982
+ if (raw2 === null) return null;
26983
+ if (allowZero && raw2 === "0") return 0;
26984
+ if (Number.isFinite(Number(raw2))) {
26985
+ throw new Error(`${name} \u5FC5\u987B\u5E26\u65F6\u95F4\u5355\u4F4D\uFF0C\u8BF7\u4F7F\u7528 30s\u30015min\u30011h \u6216 2d`);
26986
+ }
26987
+ const milliseconds = parseDuration(raw2);
26988
+ if (milliseconds === null || !Number.isSafeInteger(milliseconds)) {
26989
+ throw new Error(`${name} \u683C\u5F0F\u65E0\u6548\uFF0C\u8BF7\u4F7F\u7528 30s\u30015min\u30011h \u6216 2d`);
26990
+ }
26991
+ return milliseconds;
26992
+ };
26993
+ const scheduleType = requiredString("scheduleType");
26994
+ if (!["cron", "delayed", "recurring"].includes(scheduleType)) {
26995
+ throw new Error("scheduleType \u5FC5\u987B\u662F cron\u3001delayed \u6216 recurring");
26996
+ }
26997
+ return {
26998
+ name: requiredString("name"),
26999
+ agent: requiredString("agent"),
27000
+ model: optionalString("model", "default"),
27001
+ prompt: requiredString("prompt"),
27002
+ cwd: requiredString("cwd"),
27003
+ category: optionalString("category", "general"),
27004
+ importance: integer2("importance", 3),
27005
+ urgency: integer2("urgency", 3),
27006
+ batchId: optionalString("batchId"),
27007
+ scheduleType,
27008
+ cronExpr: scheduleType === "cron" ? requiredString("cronExpr") : null,
27009
+ intervalMs: scheduleType === "recurring" ? duration("interval", null) : null,
27010
+ runAt: scheduleType === "delayed" ? integer2("runAt", null) : null,
27011
+ maxInstances: integer2("maxInstances", 1),
27012
+ maxRetries: integer2("maxRetries", 3),
27013
+ retryBackoffMs: duration("retryBackoff", "30s", true),
27014
+ timeoutMs: duration("timeout", null)
27015
+ };
27016
+ }
26111
27017
  function safeStatus(value) {
26112
27018
  return value && TASK_STATUSES2.has(value) ? value : "unknown";
26113
27019
  }
@@ -26142,9 +27048,9 @@ function esc(value) {
26142
27048
  }
26143
27049
  function readCurrentConfig() {
26144
27050
  const configPath = getConfigPath();
26145
- if (!existsSync7(configPath)) return {};
27051
+ if (!existsSync8(configPath)) return {};
26146
27052
  try {
26147
- return JSON.parse(readFileSync4(configPath, "utf-8"));
27053
+ return JSON.parse(readFileSync5(configPath, "utf-8"));
26148
27054
  } catch {
26149
27055
  return {};
26150
27056
  }
@@ -26152,7 +27058,7 @@ function readCurrentConfig() {
26152
27058
  function writeConfig(cfg) {
26153
27059
  const configPath = getConfigPath();
26154
27060
  const dir = dirname8(configPath);
26155
- if (!existsSync7(dir)) mkdirSync5(dir, { recursive: true });
27061
+ if (!existsSync8(dir)) mkdirSync5(dir, { recursive: true });
26156
27062
  const tempPath = `${configPath}.${process.pid}.tmp`;
26157
27063
  writeFileSync3(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
26158
27064
  renameSync2(tempPath, configPath);
@@ -26187,13 +27093,14 @@ function pagination(locale, basePath, page, pages, total, suffix = "") {
26187
27093
  const next = page < pages ? `<a class="btn" href="${basePath}?page=${page + 1}${suffix}">${t(locale, "pagination.next")}${icon("chevronRight")}</a>` : "";
26188
27094
  return `<div class="pagination">${previous}<span class="summary">${t(locale, "pagination.summary", { page, pages, total })}</span>${next}</div>`;
26189
27095
  }
26190
- var app, TASK_STATUSES2, dashboardApp, web_default;
27096
+ var app, LEGACY_PROJECT_FILTER, TASK_STATUSES2, SESSION_ID_PATTERN, runtimeConfig, restartScheduled, dashboardApp, web_default;
26191
27097
  var init_web = __esm({
26192
27098
  "src/web/index.tsx"() {
26193
27099
  "use strict";
26194
27100
  init_dist();
26195
27101
  init_drizzle_orm();
26196
27102
  init_db2();
27103
+ init_duration();
26197
27104
  init_database_maintenance_service();
26198
27105
  init_task_run_service();
26199
27106
  init_task_service();
@@ -26201,8 +27108,10 @@ var init_web = __esm({
26201
27108
  init_config();
26202
27109
  init_health();
26203
27110
  init_job_templates();
27111
+ init_pm2();
26204
27112
  init_ui();
26205
27113
  app = new Hono2();
27114
+ LEGACY_PROJECT_FILTER = "__supertask_legacy__";
26206
27115
  TASK_STATUSES2 = /* @__PURE__ */ new Set([
26207
27116
  "pending",
26208
27117
  "running",
@@ -26211,6 +27120,9 @@ var init_web = __esm({
26211
27120
  "dead_letter",
26212
27121
  "cancelled"
26213
27122
  ]);
27123
+ SESSION_ID_PATTERN = /^ses_[A-Za-z0-9_]+$/;
27124
+ runtimeConfig = null;
27125
+ restartScheduled = false;
26214
27126
  app.use("*", async (c, next) => {
26215
27127
  await next();
26216
27128
  c.header("X-Content-Type-Options", "nosniff");
@@ -26247,24 +27159,52 @@ var init_web = __esm({
26247
27159
  const page = parsePositiveInteger2(c.req.query("page") || "1");
26248
27160
  if (page === null) return c.text("invalid page", 400);
26249
27161
  const statusFilter = c.req.query("status") || "";
27162
+ const requestedCwd = c.req.query("cwd") || "";
27163
+ const legacyProjectFilter = requestedCwd === LEGACY_PROJECT_FILTER;
27164
+ const cwdFilter = legacyProjectFilter ? "" : requestedCwd;
27165
+ const projectFilter = legacyProjectFilter ? LEGACY_PROJECT_FILTER : cwdFilter;
26250
27166
  const parsedStatus = statusFilter ? parseTaskStatus2(statusFilter) : null;
26251
27167
  if (statusFilter && !parsedStatus) return c.text("invalid status", 400);
26252
27168
  const limit = 50;
26253
27169
  const offset = (page - 1) * limit;
26254
- const [tasks4, statsData] = await Promise.all([
26255
- TaskService.list({ limit, offset, ...parsedStatus ? { status: parsedStatus } : {} }),
26256
- TaskService.stats({})
27170
+ const [tasks4, statsData, globalStats, projects, globalRunning, legacyStats, legacyRunning] = await Promise.all([
27171
+ TaskService.list({
27172
+ limit,
27173
+ offset,
27174
+ ...parsedStatus === "running" ? { activeExecution: true } : parsedStatus ? { status: parsedStatus } : {},
27175
+ ...legacyProjectFilter ? { legacyCwd: true } : cwdFilter ? { cwd: cwdFilter } : {}
27176
+ }),
27177
+ TaskService.stats(legacyProjectFilter ? { legacyCwd: true } : cwdFilter ? { cwd: cwdFilter } : {}),
27178
+ TaskService.stats(),
27179
+ TaskService.projectSummaries(1e3),
27180
+ TaskService.countRunning(),
27181
+ TaskService.stats({ legacyCwd: true }),
27182
+ TaskService.countRunning({ legacyCwd: true })
26257
27183
  ]);
26258
27184
  const latestRuns = await TaskRunService.getLatestByTaskIds(tasks4.map((task) => task.id));
27185
+ const selectedRunning = legacyProjectFilter ? legacyRunning : cwdFilter ? projects.find((project) => project.cwd === cwdFilter)?.running ?? 0 : globalRunning;
26259
27186
  const counts = {
26260
27187
  pending: statsData.pending || 0,
26261
- running: statsData.running || 0,
27188
+ running: selectedRunning,
26262
27189
  done: statsData.done || 0,
26263
27190
  failed: (statsData.failed || 0) + (statsData.dead_letter || 0),
26264
27191
  total: statsData.total || 0
26265
27192
  };
26266
- const filteredTotal = parsedStatus ? Number(statsData[parsedStatus] ?? 0) : counts.total;
27193
+ const filteredTotal = parsedStatus === "running" ? selectedRunning : parsedStatus ? Number(statsData[parsedStatus] ?? 0) : counts.total;
26267
27194
  const totalPages = Math.max(1, Math.ceil(filteredTotal / limit));
27195
+ if (page > totalPages) {
27196
+ const params = new URLSearchParams({ page: String(totalPages) });
27197
+ if (statusFilter) params.set("status", statusFilter);
27198
+ if (projectFilter) params.set("cwd", projectFilter);
27199
+ return c.redirect(`/?${params.toString()}`);
27200
+ }
27201
+ const taskListUrl = (status, cwd) => {
27202
+ const params = new URLSearchParams();
27203
+ if (status) params.set("status", status);
27204
+ if (cwd) params.set("cwd", cwd);
27205
+ const query = params.toString();
27206
+ return query ? `/?${query}` : "/";
27207
+ };
26268
27208
  const filterItems = [
26269
27209
  { status: "", label: t(locale, "filter.all") },
26270
27210
  { status: "pending", label: statusText(locale, "pending") },
@@ -26275,22 +27215,27 @@ var init_web = __esm({
26275
27215
  { status: "cancelled", label: statusText(locale, "cancelled") }
26276
27216
  ];
26277
27217
  const filters = filterItems.map(({ status, label }) => {
26278
- const href = status ? `/?status=${status}` : "/";
26279
- return `<a href="${href}" class="filter-chip ${statusFilter === status ? "active" : ""}">${label}</a>`;
27218
+ const href = taskListUrl(status, projectFilter);
27219
+ return `<a href="${esc(href)}" class="filter-chip ${statusFilter === status ? "active" : ""}">${label}</a>`;
26280
27220
  }).join("");
26281
27221
  const rows = tasks4.map((task) => {
26282
27222
  const status = safeStatus(task.status);
26283
- const executionActive = latestRuns.get(task.id)?.status === "running";
26284
- const searchable = esc(`${task.name} ${task.agent} ${task.prompt}`);
27223
+ const latestRun = latestRuns.get(task.id);
27224
+ const executionActive = latestRun?.status === "running";
27225
+ const batchId = task.batchId?.trim() || null;
27226
+ const searchable = esc(`${task.name} ${task.agent} ${task.prompt} ${task.cwd ?? ""} ${task.batchId ?? ""} ${task.category ?? ""}`);
26285
27227
  return `<tr data-task-row data-search="${searchable}">
26286
27228
  <td class="faint" data-label="${t(locale, "table.id")}">#${task.id}</td>
26287
- <td data-primary data-label="${t(locale, "table.task")}"><div class="task-name">${esc(task.name)}</div><div class="task-prompt" title="${esc(task.prompt)}">${esc(task.prompt.substring(0, 160))}</div></td>
27229
+ <td data-primary data-label="${t(locale, "table.task")}"><div class="task-name">${esc(task.name)}</div><div class="task-prompt" title="${esc(task.prompt)}">${esc(task.prompt.substring(0, 160))}</div>
27230
+ <div class="actions task-context"><span class="tag" title="${esc(task.cwd ?? "")}">${esc(task.cwd ? basename3(task.cwd) || task.cwd : t(locale, "projects.legacy"))}</span>${batchId ? `<span class="tag" title="${esc(t(locale, "template.batchId"))}">${esc(batchId)}</span>` : ""}</div></td>
26288
27231
  <td data-label="${t(locale, "table.agent")}"><span class="tag">${esc(task.agent)}</span></td>
26289
- <td data-label="${t(locale, "table.status")}"><span class="badge b-${status}">${statusText(locale, status)}</span></td>
26290
- <td data-label="${t(locale, "table.duration")}" class="small ${task.status === "running" ? "" : "muted"}">${formatDuration(task.startedAt, task.finishedAt)}</td>
27232
+ <td data-label="${t(locale, "table.status")}"><span class="badge b-${status}" ${status === "dead_letter" ? `title="${esc(t(locale, "status.deadLetterHint"))}"` : ""}>${statusText(locale, status)}</span>${status === "dead_letter" ? `<div class="muted small" style="margin-top:5px">${t(locale, "status.deadLetterAction")}</div>` : ""}${executionActive && status !== "running" ? `<div class="muted small" style="margin-top:5px">${t(locale, "status.executionStillActive")}</div>` : ""}</td>
27233
+ <td data-label="${t(locale, "table.duration")}" class="small ${executionActive || task.status === "running" ? "" : "muted"}">${formatDuration(task.startedAt, task.finishedAt)}</td>
26291
27234
  <td data-label="${t(locale, "table.retries")}" class="muted small">${(task.retryCount ?? 0) > 0 ? task.retryCount : "\u2014"}</td>
26292
27235
  <td data-label="${t(locale, "table.actions")}"><div class="actions">
27236
+ ${task.cwd?.trim() && ["pending", "failed", "dead_letter"].includes(task.status ?? "") ? `<button type="button" class="btn" onclick="openTaskEditor(${task.id})">${t(locale, "action.edit")}</button>` : ""}
26293
27237
  <button type="button" class="btn" onclick="showDetail(${task.id})">${t(locale, "action.details")}</button>
27238
+ ${isValidSessionId(latestRun?.sessionId) ? `<button type="button" class="btn" onclick="copySessionCommand(${latestRun.id})">${icon("copy")}${t(locale, "action.continueSession")}</button>` : ""}
26294
27239
  ${task.status === "failed" || task.status === "dead_letter" ? `<button type="button" class="btn btn-warning" onclick="retryTask(${task.id})">${t(locale, "action.retry")}</button>` : ""}
26295
27240
  ${["pending", "running", "failed"].includes(task.status ?? "") ? `<button type="button" class="btn btn-warning" onclick="cancelTask(${task.id})">${t(locale, "action.cancel")}</button>` : ""}
26296
27241
  ${task.status === "running" || executionActive ? "" : `<button type="button" class="btn btn-danger" onclick="deleteTask(${task.id})">${t(locale, "action.delete")}</button>`}
@@ -26302,7 +27247,32 @@ var init_web = __esm({
26302
27247
  <tbody>${rows}</tbody>
26303
27248
  </table></div>
26304
27249
  <div id="search-empty" hidden>${emptyState(t(locale, "filter.noResults"), "")}</div>`;
26305
- const suffix = statusFilter ? `&status=${statusFilter}` : "";
27250
+ const paginationParts = [];
27251
+ if (statusFilter) paginationParts.push(`status=${encodeURIComponent(statusFilter)}`);
27252
+ if (projectFilter) paginationParts.push(`cwd=${encodeURIComponent(projectFilter)}`);
27253
+ const suffix = paginationParts.length > 0 ? `&${paginationParts.join("&")}` : "";
27254
+ const projectCards = [
27255
+ `<a class="project-card ${projectFilter ? "" : "active"}" href="${esc(taskListUrl(statusFilter, ""))}">
27256
+ <div class="project-card-head"><strong>${t(locale, "projects.all")}</strong><span>${globalStats.total || 0}</span></div>
27257
+ <div class="project-counts">${t(locale, "projects.counts", { running: globalRunning, pending: globalStats.pending || 0, failed: (globalStats.failed || 0) + (globalStats.dead_letter || 0) })}</div>
27258
+ </a>`,
27259
+ ...legacyStats.total > 0 ? [`<a class="project-card ${legacyProjectFilter ? "active" : ""}" href="${esc(taskListUrl(statusFilter, LEGACY_PROJECT_FILTER))}">
27260
+ <div class="project-card-head"><strong>${t(locale, "projects.legacy")}</strong><span>${legacyStats.total}</span></div>
27261
+ <div class="project-path">${t(locale, "projects.legacyHint")}</div>
27262
+ <div class="project-counts">${t(locale, "projects.counts", { running: legacyRunning, pending: legacyStats.pending || 0, failed: (legacyStats.failed || 0) + (legacyStats.dead_letter || 0) })}</div>
27263
+ </a>`] : [],
27264
+ ...projects.map((project) => `<a class="project-card ${cwdFilter === project.cwd ? "active" : ""}" href="${esc(taskListUrl(statusFilter, project.cwd))}" title="${esc(project.cwd)}">
27265
+ <div class="project-card-head"><strong>${esc(basename3(project.cwd) || project.cwd)}</strong><span>${project.total}</span></div>
27266
+ <div class="project-path">${esc(project.cwd)}</div>
27267
+ <div class="project-counts">${t(locale, "projects.counts", { running: project.running, pending: project.pending, failed: project.failed })}</div>
27268
+ </a>`)
27269
+ ].join("");
27270
+ const projectData = JSON.stringify(Object.fromEntries(projects.map((project) => [project.cwd, {
27271
+ total: project.total,
27272
+ pending: project.pending,
27273
+ running: project.running,
27274
+ failed: project.failed
27275
+ }]))).replace(/</g, "\\u003c");
26306
27276
  const body = `
26307
27277
  <div class="stats-grid">
26308
27278
  ${statCard(counts.pending, t(locale, "stats.pending"), "tone-neutral", icon("clock"))}
@@ -26310,19 +27280,61 @@ var init_web = __esm({
26310
27280
  ${statCard(counts.done, t(locale, "stats.done"), "tone-green", icon("check"), "reveal-delay-1")}
26311
27281
  ${statCard(counts.failed, t(locale, "stats.failedDead"), "tone-red", icon("alert"), "reveal-delay-2")}
26312
27282
  </div>
27283
+ <section class="panel project-panel reveal reveal-delay-1">
27284
+ <div class="panel-head"><div><h2>${t(locale, "projects.title")}</h2><p>${t(locale, "projects.description")}</p></div><button type="button" class="btn btn-primary" onclick="openTaskCreator()">${t(locale, "action.createTask")}</button></div>
27285
+ <div class="project-grid">${projectCards}</div>
27286
+ </section>
26313
27287
  <div class="toolbar reveal reveal-delay-1">
26314
27288
  <div class="filters">${filters}</div>
26315
27289
  <label class="search-box">${icon("search")}<input type="search" oninput="filterTasks(this.value)" placeholder="${t(locale, "filter.searchTasks")}" aria-label="${t(locale, "filter.searchTasks")}"></label>
26316
27290
  </div>
26317
27291
  <section class="panel reveal reveal-delay-2">${table}</section>
26318
- ${pagination(locale, "/", page, totalPages, filteredTotal, suffix)}`;
27292
+ ${pagination(locale, "/", page, totalPages, filteredTotal, suffix)}
27293
+ <script type="application/json" id="task-project-data">${projectData}</script>
27294
+ <dialog id="task-dialog" class="template-dialog">
27295
+ <form id="task-form" data-default-cwd="${esc(cwdFilter)}" onsubmit="saveTask(event)">
27296
+ <input id="task-id" type="hidden">
27297
+ <div class="dialog-head"><div><h2 id="task-dialog-title">${t(locale, "task.createTitle")}</h2><p>${t(locale, "task.formSubtitle")}</p></div><button type="button" class="icon-button" onclick="document.getElementById('task-dialog').close()" aria-label="${t(locale, "action.close")}">${icon("close")}</button></div>
27298
+ <div class="dialog-body">
27299
+ <div class="template-form-grid">
27300
+ <label class="form-field"><span>${t(locale, "template.name")}</span><input id="task-name" required maxlength="200" autocomplete="off"></label>
27301
+ <label class="form-field"><span>${t(locale, "template.cwd")}</span><input id="task-cwd" required autocomplete="off" list="task-cwd-options" placeholder="/path/to/project" oninput="updateTaskProjectStatus()"><small>${t(locale, "template.cwdHint")}</small></label>
27302
+ <datalist id="task-cwd-options">${projects.map((project) => `<option value="${esc(project.cwd)}"></option>`).join("")}</datalist>
27303
+ <label class="form-field"><span>${t(locale, "template.agent")}</span><input id="task-agent" required autocomplete="off" value="build" placeholder="build"></label>
27304
+ <label class="form-field"><span>${t(locale, "template.model")}</span><input id="task-model" required autocomplete="off" value="default" placeholder="default"></label>
27305
+ <label class="form-field form-field-wide"><span>${t(locale, "template.prompt")}</span><textarea id="task-prompt" rows="6" required></textarea></label>
27306
+ </div>
27307
+ <p id="task-project-status" class="form-note"></p>
27308
+ <details class="advanced-fields">
27309
+ <summary>${t(locale, "template.advanced")}</summary>
27310
+ <div class="template-form-grid">
27311
+ <label class="form-field"><span>${t(locale, "template.category")}</span><input id="task-category" autocomplete="off" value="general"></label>
27312
+ <label class="form-field"><span>${t(locale, "template.batchId")}</span><input id="task-batch" autocomplete="off"><small>${t(locale, "task.batchHint")}</small></label>
27313
+ <label class="form-field"><span>${t(locale, "template.importance")}</span><input id="task-importance" type="number" min="1" max="5" step="1" value="3" required></label>
27314
+ <label class="form-field"><span>${t(locale, "template.urgency")}</span><input id="task-urgency" type="number" min="1" max="5" step="1" value="3" required></label>
27315
+ <label class="form-field"><span>${t(locale, "template.maxRetries")}</span><input id="task-max-retries" type="number" min="0" max="1000" step="1" value="3" required></label>
27316
+ <label class="form-field"><span>${t(locale, "template.retryBackoff")}</span><input id="task-retry-backoff" autocomplete="off" value="30s"><small>${t(locale, "template.durationHint")}</small></label>
27317
+ <label class="form-field"><span>${t(locale, "template.timeout")}</span><input id="task-timeout" autocomplete="off" placeholder="30min"><small>${t(locale, "template.optional")}</small></label>
27318
+ </div>
27319
+ </details>
27320
+ </div>
27321
+ <div class="dialog-actions"><button type="button" class="btn" onclick="document.getElementById('task-dialog').close()">${t(locale, "action.cancel")}</button><button id="task-save" type="submit" class="btn btn-primary">${t(locale, "action.saveTask")}</button></div>
27322
+ </form>
27323
+ </dialog>`;
26319
27324
  return c.html(renderLayout({ locale, activeTab: "tasks", body }));
26320
27325
  });
26321
27326
  app.get("/templates", async (c) => {
26322
27327
  const locale = resolveLocale(c);
26323
- const templates = await TaskTemplateService.list(100);
26324
- const enabled = templates.filter((template) => template.enabled).length;
26325
- const disabled = templates.length - enabled;
27328
+ const page = parsePositiveInteger2(c.req.query("page") || "1");
27329
+ if (page === null) return c.text("invalid page", 400);
27330
+ const limit = 50;
27331
+ const offset = (page - 1) * limit;
27332
+ const [templates, templateStats] = await Promise.all([
27333
+ TaskTemplateService.list(limit, offset),
27334
+ TaskTemplateService.stats()
27335
+ ]);
27336
+ const totalPages = Math.max(1, Math.ceil(templateStats.total / limit));
27337
+ if (page > totalPages) return c.redirect(`/templates?page=${totalPages}`);
26326
27338
  const rows = templates.map((template) => {
26327
27339
  const scheduleType = ["cron", "recurring", "delayed"].includes(template.scheduleType) ? template.scheduleType : "unknown";
26328
27340
  const typeLabel = scheduleType === "cron" ? t(locale, "schedule.cron") : scheduleType === "recurring" ? t(locale, "schedule.recurring") : scheduleType === "delayed" ? t(locale, "schedule.delayed") : t(locale, "schedule.unknown");
@@ -26340,21 +27352,56 @@ var init_web = __esm({
26340
27352
  <td data-label="${t(locale, "table.status")}"><span class="badge ${template.enabled ? "b-done" : "b-cancelled"}">${t(locale, template.enabled ? "schedule.enabled" : "schedule.disabled")}</span></td>
26341
27353
  <td data-label="${t(locale, "table.lastRun")}" class="small muted">${formatRelative(template.lastRunAt, locale)}</td>
26342
27354
  <td data-label="${t(locale, "table.nextRun")}" class="small">${formatFuture(template.nextRunAt, locale)}</td>
26343
- <td data-label="${t(locale, "table.actions")}"><div class="actions"><button type="button" class="btn" onclick="showTemplateDetail(${template.id})">${t(locale, "action.details")}</button>
27355
+ <td data-label="${t(locale, "table.actions")}"><div class="actions"><button type="button" class="btn" onclick="openTemplateEditor(${template.id})">${t(locale, "action.edit")}</button><button type="button" class="btn" onclick="showTemplateDetail(${template.id})">${t(locale, "action.details")}</button>
26344
27356
  <button type="button" class="btn btn-primary" onclick="triggerTmpl(${template.id})">${t(locale, "action.trigger")}</button>${toggle}
26345
27357
  <button type="button" class="btn btn-danger" onclick="deleteTmpl(${template.id})">${t(locale, "action.delete")}</button></div></td>
26346
27358
  </tr>`;
26347
27359
  }).join("");
26348
27360
  const body = `
26349
27361
  <div class="stats-grid three">
26350
- ${statCard(templates.length, t(locale, "stats.templates"), "tone-purple", icon("templates"))}
26351
- ${statCard(enabled, t(locale, "stats.enabled"), "tone-green", icon("check"), "reveal-delay-1")}
26352
- ${statCard(disabled, t(locale, "stats.disabled"), "tone-neutral", icon("clock"), "reveal-delay-2")}
27362
+ ${statCard(templateStats.total, t(locale, "stats.templates"), "tone-purple", icon("templates"))}
27363
+ ${statCard(templateStats.enabled, t(locale, "stats.enabled"), "tone-green", icon("check"), "reveal-delay-1")}
27364
+ ${statCard(templateStats.disabled, t(locale, "stats.disabled"), "tone-neutral", icon("clock"), "reveal-delay-2")}
26353
27365
  </div>
26354
27366
  <section class="panel reveal reveal-delay-2">
26355
- <div class="panel-head"><h2>${t(locale, "page.templates.title")}</h2></div>
26356
- ${templates.length === 0 ? emptyState(t(locale, "empty.templates"), t(locale, "empty.templatesHint"), "supertask template add") : `<div class="table-wrap"><table class="responsive-table"><thead><tr><th>ID</th><th>${t(locale, "table.name")}</th><th>${t(locale, "table.type")}</th><th>${t(locale, "table.rule")}</th><th>${t(locale, "table.status")}</th><th>${t(locale, "table.lastRun")}</th><th>${t(locale, "table.nextRun")}</th><th>${t(locale, "table.actions")}</th></tr></thead><tbody>${rows}</tbody></table></div>`}
26357
- </section>`;
27367
+ <div class="panel-head"><h2>${t(locale, "page.templates.title")}</h2><button type="button" class="btn btn-primary" onclick="openTemplateCreator()">${t(locale, "action.createTemplate")}</button></div>
27368
+ ${templates.length === 0 ? emptyState(t(locale, "empty.templates"), t(locale, "empty.templatesHint")) : `<div class="table-wrap"><table class="responsive-table"><thead><tr><th>ID</th><th>${t(locale, "table.name")}</th><th>${t(locale, "table.type")}</th><th>${t(locale, "table.rule")}</th><th>${t(locale, "table.status")}</th><th>${t(locale, "table.lastRun")}</th><th>${t(locale, "table.nextRun")}</th><th>${t(locale, "table.actions")}</th></tr></thead><tbody>${rows}</tbody></table></div>`}
27369
+ </section>
27370
+ <dialog id="template-dialog" class="template-dialog">
27371
+ <form id="template-form" onsubmit="saveTemplate(event)">
27372
+ <input id="template-id" type="hidden">
27373
+ <div class="dialog-head"><div><h2 id="template-dialog-title">${t(locale, "template.createTitle")}</h2><p>${t(locale, "template.formSubtitle")}</p></div><button type="button" class="icon-button" onclick="document.getElementById('template-dialog').close()" aria-label="${t(locale, "action.close")}">${icon("close")}</button></div>
27374
+ <div class="dialog-body">
27375
+ <div class="template-form-grid">
27376
+ <label class="form-field"><span>${t(locale, "template.name")}</span><input id="template-name" required maxlength="200" autocomplete="off"></label>
27377
+ <label class="form-field"><span>${t(locale, "template.cwd")}</span><input id="template-cwd" required autocomplete="off" placeholder="/path/to/project"><small>${t(locale, "template.cwdHint")}</small></label>
27378
+ <label class="form-field"><span>${t(locale, "template.agent")}</span><input id="template-agent" required autocomplete="off" placeholder="build"></label>
27379
+ <label class="form-field"><span>${t(locale, "template.model")}</span><input id="template-model" required autocomplete="off" value="default" placeholder="default"></label>
27380
+ <label class="form-field form-field-wide"><span>${t(locale, "template.prompt")}</span><textarea id="template-prompt" rows="6" required></textarea></label>
27381
+ <label class="form-field"><span>${t(locale, "template.scheduleType")}</span><select id="template-schedule-type" onchange="updateTemplateScheduleFields()"><option value="recurring">${t(locale, "schedule.recurring")}</option><option value="delayed">${t(locale, "schedule.delayed")}</option><option value="cron">${t(locale, "schedule.cron")}</option></select></label>
27382
+ <label id="template-cron-field" class="form-field" hidden><span>${t(locale, "template.cronExpr")}</span><input id="template-cron" autocomplete="off" placeholder="0 9 * * *"><small>${t(locale, "template.cronHint")}</small></label>
27383
+ <label id="template-interval-field" class="form-field"><span>${t(locale, "template.interval")}</span><input id="template-interval" autocomplete="off" value="1h" placeholder="30s / 5min / 1h"><small>${t(locale, "template.durationHint")}</small></label>
27384
+ <label id="template-run-at-field" class="form-field" hidden><span>${t(locale, "template.runAt")}</span><input id="template-run-at" type="datetime-local" step="0.001"></label>
27385
+ </div>
27386
+ <details class="advanced-fields">
27387
+ <summary>${t(locale, "template.advanced")}</summary>
27388
+ <div class="template-form-grid">
27389
+ <label class="form-field"><span>${t(locale, "template.category")}</span><input id="template-category" autocomplete="off" value="general"></label>
27390
+ <label class="form-field"><span>${t(locale, "template.batchId")}</span><input id="template-batch" autocomplete="off"><small>${t(locale, "template.optional")}</small></label>
27391
+ <label class="form-field"><span>${t(locale, "template.importance")}</span><input id="template-importance" type="number" min="1" max="5" step="1" value="3" required></label>
27392
+ <label class="form-field"><span>${t(locale, "template.urgency")}</span><input id="template-urgency" type="number" min="1" max="5" step="1" value="3" required></label>
27393
+ <label class="form-field"><span>${t(locale, "template.maxInstances")}</span><input id="template-max-instances" type="number" min="1" max="1000" step="1" value="1" required><small>${t(locale, "template.maxInstancesHint")}</small></label>
27394
+ <label class="form-field"><span>${t(locale, "template.maxRetries")}</span><input id="template-max-retries" type="number" min="0" max="1000" step="1" value="3" required></label>
27395
+ <label class="form-field"><span>${t(locale, "template.retryBackoff")}</span><input id="template-retry-backoff" autocomplete="off" value="30s"><small>${t(locale, "template.durationHint")}</small></label>
27396
+ <label class="form-field"><span>${t(locale, "template.timeout")}</span><input id="template-timeout" autocomplete="off" placeholder="30min"><small>${t(locale, "template.optional")}</small></label>
27397
+ </div>
27398
+ </details>
27399
+ <p class="form-note">${t(locale, "template.futureOnly")}</p>
27400
+ </div>
27401
+ <div class="dialog-actions"><button type="button" class="btn" onclick="document.getElementById('template-dialog').close()">${t(locale, "action.cancel")}</button><button id="template-save" type="submit" class="btn btn-primary">${t(locale, "action.saveTemplate")}</button></div>
27402
+ </form>
27403
+ </dialog>
27404
+ ${pagination(locale, "/templates", page, totalPages, templateStats.total)}`;
26358
27405
  return c.html(renderLayout({ locale, activeTab: "templates", body }));
26359
27406
  });
26360
27407
  app.get("/runs", async (c) => {
@@ -26382,9 +27429,11 @@ var init_web = __esm({
26382
27429
  const totalResult = await db.select({ count: sql`count(*)` }).from(taskRuns4);
26383
27430
  const total = Number(totalResult[0]?.count ?? 0);
26384
27431
  const totalPages = Math.max(1, Math.ceil(total / limit));
27432
+ if (page > totalPages) return c.redirect(`/runs?page=${totalPages}`);
26385
27433
  const logs = [];
26386
27434
  const rows = runs.map((run) => {
26387
27435
  const status = safeStatus(run.status);
27436
+ const resumable = isValidSessionId(run.sessionId);
26388
27437
  if (run.log) {
26389
27438
  logs.push(`<section id="log-${run.id}" class="panel log-panel" hidden><div class="panel-head"><h3>Run #${run.id} \xB7 ${esc(run.taskName)}</h3></div><div class="log-box">${esc(run.log)}</div></section>`);
26390
27439
  }
@@ -26392,10 +27441,12 @@ var init_web = __esm({
26392
27441
  <td class="faint" data-label="${t(locale, "table.run")}">#${run.id}</td>
26393
27442
  <td data-primary data-label="${t(locale, "table.task")}"><div class="task-name">${esc(run.taskName)} <span class="faint">#${run.taskId}</span></div>${run.model ? `<div style="margin-top:4px"><span class="tag">${esc(run.model)}</span></div>` : ""}</td>
26394
27443
  <td data-label="${t(locale, "table.agent")}"><span class="tag">${esc(run.taskAgent)}</span></td>
26395
- <td data-label="${t(locale, "table.status")}"><span class="badge b-${status}">${statusText(locale, status)}</span></td>
27444
+ <td data-label="${t(locale, "table.session")}" class="m small">${esc(maskSessionId(run.sessionId))}</td>
27445
+ <td data-label="${t(locale, "table.status")}"><span class="badge b-${status}">${runStatusText(locale, run.status ?? "unknown")}</span></td>
26396
27446
  <td data-label="${t(locale, "table.duration")}" class="small">${formatDuration(run.startedAt, run.finishedAt)}</td>
26397
27447
  <td data-label="${t(locale, "table.heartbeat")}" class="small muted">${formatRelative(run.heartbeatAt, locale)}</td>
26398
27448
  <td data-label="${t(locale, "table.actions")}"><div class="actions"><button type="button" class="btn" onclick="showRunDetail(${run.id})">${t(locale, "action.details")}</button>
27449
+ ${resumable ? `<button type="button" class="btn" onclick="copySessionCommand(${run.id})">${icon("copy")}${t(locale, "action.continueSession")}</button>` : ""}
26399
27450
  ${run.log ? `<button type="button" class="btn" aria-expanded="false" onclick="toggleLog(${run.id},this)">${t(locale, "action.logs")}</button>` : ""}</div></td>
26400
27451
  </tr>`;
26401
27452
  }).join("");
@@ -26406,22 +27457,37 @@ var init_web = __esm({
26406
27457
  ${statCard(runs.filter((run) => run.status === "failed").length, t(locale, "stats.pageFailed"), "tone-red", icon("alert"), "reveal-delay-1")}
26407
27458
  ${statCard(runs.filter((run) => run.status === "running").length, t(locale, "stats.pageRunning"), "tone-blue", icon("activity"), "reveal-delay-2")}
26408
27459
  </div>
26409
- <section class="panel reveal reveal-delay-2">${runs.length === 0 ? emptyState(t(locale, "empty.runs"), "") : `<div class="table-wrap"><table class="responsive-table"><thead><tr><th>${t(locale, "table.run")}</th><th>${t(locale, "table.task")}</th><th>${t(locale, "table.agent")}</th><th>${t(locale, "table.status")}</th><th>${t(locale, "table.duration")}</th><th>${t(locale, "table.heartbeat")}</th><th>${t(locale, "table.actions")}</th></tr></thead><tbody>${rows}</tbody></table></div>`}</section>
27460
+ <section class="panel reveal reveal-delay-2">${runs.length === 0 ? emptyState(t(locale, "empty.runs"), "") : `<div class="table-wrap"><table class="responsive-table"><thead><tr><th>${t(locale, "table.run")}</th><th>${t(locale, "table.task")}</th><th>${t(locale, "table.agent")}</th><th>${t(locale, "table.session")}</th><th>${t(locale, "table.status")}</th><th>${t(locale, "table.duration")}</th><th>${t(locale, "table.heartbeat")}</th><th>${t(locale, "table.actions")}</th></tr></thead><tbody>${rows}</tbody></table></div>`}</section>
26410
27461
  ${logs.join("")}${pagination(locale, "/runs", page, totalPages, total)}`;
26411
27462
  return c.html(renderLayout({ locale, activeTab: "runs", body }));
26412
27463
  });
26413
27464
  app.get("/system", async (c) => {
26414
27465
  const locale = resolveLocale(c);
26415
27466
  const config = loadConfig();
27467
+ const activeConfig = runtimeConfig ?? config;
27468
+ const restartRequired = runtimeConfig !== null && !configsEqual(config, runtimeConfig);
27469
+ const managedRestart = canRestartCurrentGateway();
27470
+ const configState = resolveDashboardConfigState(
27471
+ runtimeConfig !== null,
27472
+ restartRequired,
27473
+ managedRestart
27474
+ );
27475
+ const configStateKey = {
27476
+ foreground: "system.configForeground",
27477
+ applied: "system.configApplied",
27478
+ pending: "system.configPending",
27479
+ manual: "system.configRestartManually"
27480
+ }[configState];
27481
+ const configStateText = t(locale, configStateKey);
26416
27482
  const configPath = getConfigPath();
26417
- const [stats, runningRuns, templates] = await Promise.all([
27483
+ const [stats, runningRuns, templateStats] = await Promise.all([
26418
27484
  TaskService.stats({}),
26419
27485
  TaskRunService.getAllRunningRuns(),
26420
- TaskTemplateService.list(100)
27486
+ TaskTemplateService.stats()
26421
27487
  ]);
26422
- const configExists = existsSync7(configPath);
27488
+ const configExists = existsSync8(configPath);
26423
27489
  const runRows = runningRuns.map((run) => {
26424
- const session = run.sessionId ? `${run.sessionId.slice(4, 7)}***${run.sessionId.slice(-3)}` : "\u2014";
27490
+ const session = maskSessionId(run.sessionId);
26425
27491
  return `<tr><td class="faint" data-label="${t(locale, "table.run")}">#${run.id}</td><td data-primary data-label="${t(locale, "table.task")}">#${run.taskId}</td><td data-label="${t(locale, "table.session")}" class="m small">${esc(session)}</td>
26426
27492
  <td data-label="${t(locale, "table.model")}" class="small">${esc(run.model) || "\u2014"}</td><td data-label="${t(locale, "table.startedAt")}" class="small">${formatDateTime(run.startedAt, locale)}</td>
26427
27493
  <td data-label="${t(locale, "table.heartbeat")}" class="small muted">${formatRelative(run.heartbeatAt, locale)}</td><td data-label="${t(locale, "table.pid")}" class="m small">W:${run.workerPid ?? "\u2014"} C:${run.childPid ?? "\u2014"}</td>
@@ -26437,10 +27503,10 @@ var init_web = __esm({
26437
27503
  <div class="field"><label for="hi">${t(locale, "system.heartbeatInterval")}</label>${unitInput("hi", config.worker.heartbeatIntervalMs / 1e3, 5, t(locale, "system.seconds"))}</div>
26438
27504
  <div class="field"><label for="to">${t(locale, "system.taskTimeout")}</label>${unitInput("to", config.worker.taskTimeoutMs / 6e4, 1, t(locale, "system.minutes"))}</div>
26439
27505
  </section>
26440
- <section class="card settings-card reveal-delay-1"><h2 class="settings-title"><span>${icon("templates")}${t(locale, "system.scheduler")}</span><span class="badge ${config.scheduler.enabled ? "b-done" : "b-cancelled"}">${t(locale, config.scheduler.enabled ? "schedule.enabled" : "schedule.disabled")}</span></h2>
27506
+ <section class="card settings-card reveal-delay-1"><h2 class="settings-title"><span>${icon("templates")}${t(locale, "system.scheduler")}</span><span class="badge ${activeConfig.scheduler.enabled ? "b-done" : "b-cancelled"}">${t(locale, activeConfig.scheduler.enabled ? "schedule.enabled" : "schedule.disabled")}</span></h2>
26441
27507
  <div class="switch-field"><label for="se">${t(locale, "system.schedulerEnabled")}</label><label class="switch"><input id="se" type="checkbox" name="se" ${config.scheduler.enabled ? "checked" : ""}><span></span></label></div>
26442
27508
  <div class="field"><label for="si">${t(locale, "system.checkInterval")}</label>${unitInput("si", config.scheduler.checkIntervalMs, 100, t(locale, "system.milliseconds"))}</div>
26443
- <div class="info-row"><span class="info-key">${t(locale, "system.activeTemplates")}</span><span class="info-value">${templates.filter((template) => template.enabled).length} / ${templates.length}</span></div>
27509
+ <div class="info-row"><span class="info-key">${t(locale, "system.activeTemplates")}</span><span class="info-value">${templateStats.enabled} / ${templateStats.total}</span></div>
26444
27510
  </section>
26445
27511
  <section class="card settings-card reveal-delay-2"><h2 class="settings-title"><span>${icon("system")}${t(locale, "system.watchdog")}</span></h2>
26446
27512
  <div class="field"><label for="wt">${t(locale, "system.heartbeatTimeout")}</label>${unitInput("wt", config.watchdog.heartbeatTimeoutMs / 1e3, 10, t(locale, "system.seconds"))}</div>
@@ -26449,10 +27515,10 @@ var init_web = __esm({
26449
27515
  <div class="field"><label for="rd">${t(locale, "system.retentionDays")}</label>${unitInput("rd", config.watchdog.retentionDays, 1, t(locale, "system.days"))}</div>
26450
27516
  </section>
26451
27517
  </div>
26452
- <div class="save-row"><span class="muted small">${t(locale, "system.saveHint")}</span><button type="submit" class="btn btn-primary">${t(locale, "action.save")}</button></div>
27518
+ <div class="save-row"><span class="muted small">${configStateText}</span><div class="actions"><button type="submit" class="btn">${t(locale, "action.save")}</button>${managedRestart ? `<button type="button" class="btn btn-primary" onclick="saveConfig(true,${runningRuns.length})">${t(locale, "action.saveAndRestart")}</button>` : ""}</div></div>
26453
27519
  </form>
26454
27520
  <section class="panel reveal">
26455
- <div class="panel-head"><h2>${t(locale, "system.runningTasks", { running: runningRuns.length, limit: config.worker.maxConcurrency })}</h2></div>
27521
+ <div class="panel-head"><h2>${t(locale, "system.runningTasks", { running: runningRuns.length, limit: activeConfig.worker.maxConcurrency })}</h2></div>
26456
27522
  ${runningRuns.length === 0 ? emptyState(t(locale, "empty.running"), "") : `<div class="table-wrap"><table class="responsive-table"><thead><tr><th>${t(locale, "table.run")}</th><th>${t(locale, "table.task")}</th><th>${t(locale, "table.session")}</th><th>${t(locale, "table.model")}</th><th>${t(locale, "table.startedAt")}</th><th>${t(locale, "table.heartbeat")}</th><th>${t(locale, "table.pid")}</th><th>${t(locale, "table.duration")}</th></tr></thead><tbody>${runRows}</tbody></table></div>`}
26457
27523
  </section>
26458
27524
  <section class="panel reveal reveal-delay-1"><div class="panel-head"><h2>${t(locale, "system.taskStats")}</h2></div><div class="overview-grid">
@@ -26469,6 +27535,30 @@ var init_web = __esm({
26469
27535
  <button type="button" class="btn btn-danger" onclick="clearDatabase()">${icon("database")}${t(locale, "action.clearDatabase")}</button></section>`;
26470
27536
  return c.html(renderLayout({ locale, activeTab: "system", body }));
26471
27537
  });
27538
+ app.post("/api/tasks", async (c) => {
27539
+ try {
27540
+ const task = await TaskService.add(parseTaskPayload(await c.req.json()));
27541
+ return c.json({ success: true, task }, 201);
27542
+ } catch (error) {
27543
+ return c.json({
27544
+ error: error instanceof Error ? error.message : String(error)
27545
+ }, 400);
27546
+ }
27547
+ });
27548
+ app.put("/api/tasks/:id", async (c) => {
27549
+ const id = parsePositiveInteger2(c.req.param("id"));
27550
+ if (id === null) return c.json({ error: "invalid id" }, 400);
27551
+ try {
27552
+ const input = parseTaskPayload(await c.req.json());
27553
+ const task = await TaskService.update(id, editableTaskPayload(input));
27554
+ if (task) return c.json({ success: true, task });
27555
+ return await TaskService.getById(id) ? c.json({ error: "task status does not allow editing" }, 409) : c.json({ error: "not found" }, 404);
27556
+ } catch (error) {
27557
+ return c.json({
27558
+ error: error instanceof Error ? error.message : String(error)
27559
+ }, 400);
27560
+ }
27561
+ });
26472
27562
  app.get("/api/tasks/:id", async (c) => {
26473
27563
  const id = parsePositiveInteger2(c.req.param("id"));
26474
27564
  if (id === null) return c.json({ error: "invalid id" }, 400);
@@ -26484,6 +27574,24 @@ var init_web = __esm({
26484
27574
  if (!run) return c.json({ error: "not found" }, 404);
26485
27575
  return c.json(run);
26486
27576
  });
27577
+ app.get("/api/runs/:id/session-command", async (c) => {
27578
+ const id = parsePositiveInteger2(c.req.param("id"));
27579
+ if (id === null) return c.json({ error: "invalid id" }, 400);
27580
+ const run = await TaskRunService.getById(id);
27581
+ if (!run) return c.json({ error: "not found" }, 404);
27582
+ if (!isValidSessionId(run.sessionId)) return c.json({ error: "session unavailable" }, 409);
27583
+ return c.json({ command: sessionCommand(run.sessionId) });
27584
+ });
27585
+ app.get("/api/gateway/status", (c) => {
27586
+ const savedConfig = loadConfig();
27587
+ const managed = canRestartCurrentGateway();
27588
+ return c.json({
27589
+ pid: process.pid,
27590
+ managed,
27591
+ ready: managed && getGatewayHealth().status === "ok",
27592
+ restartRequired: runtimeConfig === null || !configsEqual(savedConfig, runtimeConfig)
27593
+ });
27594
+ });
26487
27595
  app.get("/api/templates/:id", async (c) => {
26488
27596
  const id = parsePositiveInteger2(c.req.param("id"));
26489
27597
  if (id === null) return c.json({ error: "invalid id" }, 400);
@@ -26491,6 +27599,30 @@ var init_web = __esm({
26491
27599
  if (!template) return c.json({ error: "not found" }, 404);
26492
27600
  return c.json(template);
26493
27601
  });
27602
+ app.post("/api/templates", async (c) => {
27603
+ try {
27604
+ const input = parseTemplatePayload(await c.req.json());
27605
+ const template = await TaskTemplateService.create(input);
27606
+ return c.json({ success: true, template }, 201);
27607
+ } catch (error) {
27608
+ return c.json({
27609
+ error: error instanceof Error ? error.message : String(error)
27610
+ }, 400);
27611
+ }
27612
+ });
27613
+ app.put("/api/templates/:id", async (c) => {
27614
+ const id = parsePositiveInteger2(c.req.param("id"));
27615
+ if (id === null) return c.json({ error: "invalid id" }, 400);
27616
+ try {
27617
+ const input = parseTemplatePayload(await c.req.json());
27618
+ const template = await TaskTemplateService.update(id, input);
27619
+ return template ? c.json({ success: true, template }) : c.json({ error: "not found" }, 404);
27620
+ } catch (error) {
27621
+ return c.json({
27622
+ error: error instanceof Error ? error.message : String(error)
27623
+ }, 400);
27624
+ }
27625
+ });
26494
27626
  app.post("/api/tasks/:id/retry", async (c) => {
26495
27627
  const id = parsePositiveInteger2(c.req.param("id"));
26496
27628
  if (id === null) return c.json({ error: "invalid id" }, 400);
@@ -26539,22 +27671,31 @@ var init_web = __esm({
26539
27671
  app.post("/api/templates/:id/trigger", async (c) => {
26540
27672
  const id = parsePositiveInteger2(c.req.param("id"));
26541
27673
  if (id === null) return c.json({ error: "invalid id" }, 400);
26542
- const template = await TaskTemplateService.getById(id);
26543
- if (!template) return c.json({ error: "not found" }, 404);
26544
27674
  const task = await triggerTaskFromTemplate(id);
26545
- if (!task) return c.json({ error: "maxInstances reached" }, 409);
26546
- return c.json({ success: true, taskId: task.id });
27675
+ return task ? c.json({ success: true, taskId: task.id }) : c.json({ error: "not found" }, 404);
26547
27676
  });
26548
27677
  app.put("/api/config", async (c) => {
26549
27678
  try {
26550
- const body = await c.req.json();
27679
+ const rawBody = await c.req.json();
27680
+ if (!rawBody || typeof rawBody !== "object" || Array.isArray(rawBody)) {
27681
+ throw new Error("\u8BF7\u6C42\u5185\u5BB9\u5FC5\u987B\u662F\u5BF9\u8C61");
27682
+ }
27683
+ const body = rawBody;
27684
+ const section = (name) => {
27685
+ const value = body[name];
27686
+ if (value === void 0) return {};
27687
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
27688
+ throw new Error(`${name} \u5FC5\u987B\u662F\u5BF9\u8C61`);
27689
+ }
27690
+ return value;
27691
+ };
26551
27692
  const current = readCurrentConfig();
26552
27693
  const currentWorker = current.worker ?? {};
26553
27694
  const currentScheduler = current.scheduler ?? {};
26554
27695
  const currentWatchdog = current.watchdog ?? {};
26555
- const bodyWorker = body.worker ?? {};
26556
- const bodyScheduler = body.scheduler ?? {};
26557
- const bodyWatchdog = body.watchdog ?? {};
27696
+ const bodyWorker = section("worker");
27697
+ const bodyScheduler = section("scheduler");
27698
+ const bodyWatchdog = section("watchdog");
26558
27699
  const merged = {
26559
27700
  ...current,
26560
27701
  ...body,
@@ -26563,8 +27704,13 @@ var init_web = __esm({
26563
27704
  scheduler: { ...currentScheduler, ...bodyScheduler },
26564
27705
  watchdog: { ...currentWatchdog, ...bodyWatchdog }
26565
27706
  };
26566
- writeConfig(validateConfig(merged));
26567
- return c.json({ success: true });
27707
+ const savedConfig = validateConfig(merged);
27708
+ writeConfig(savedConfig);
27709
+ return c.json({
27710
+ success: true,
27711
+ restartRequired: runtimeConfig === null || !configsEqual(savedConfig, runtimeConfig),
27712
+ managed: canRestartCurrentGateway()
27713
+ });
26568
27714
  } catch (error) {
26569
27715
  return c.json({
26570
27716
  success: false,
@@ -26572,9 +27718,27 @@ var init_web = __esm({
26572
27718
  }, 400);
26573
27719
  }
26574
27720
  });
27721
+ app.post("/api/gateway/restart", async (c) => {
27722
+ const rawBody = await c.req.json().catch(() => null);
27723
+ const confirmation = rawBody && typeof rawBody === "object" && !Array.isArray(rawBody) ? rawBody.confirmation : void 0;
27724
+ if (confirmation !== "RESTART") {
27725
+ return c.json({ error: "confirmation must be RESTART" }, 400);
27726
+ }
27727
+ if (restartScheduled) return c.json({ error: "restart already scheduled" }, 409);
27728
+ if (!canRestartCurrentGateway()) {
27729
+ return c.json({ error: "\u5F53\u524D Gateway \u4E0D\u662F\u7531\u5339\u914D\u8FD0\u884C\u4F5C\u7528\u57DF\u7684 PM2 \u8FDB\u7A0B\u6258\u7BA1\uFF0C\u65E0\u6CD5\u4ECE\u7F51\u9875\u5B89\u5168\u91CD\u542F" }, 409);
27730
+ }
27731
+ restartScheduled = true;
27732
+ const previousPid = process.pid;
27733
+ setTimeout(() => {
27734
+ process.kill(previousPid, "SIGTERM");
27735
+ }, 500);
27736
+ return c.json({ success: true, previousPid }, 202);
27737
+ });
26575
27738
  app.post("/api/database/clear", async (c) => {
26576
- const body = await c.req.json().catch(() => ({}));
26577
- if (body.confirmation !== "CLEAR") {
27739
+ const rawBody = await c.req.json().catch(() => null);
27740
+ const confirmation = rawBody && typeof rawBody === "object" && !Array.isArray(rawBody) ? rawBody.confirmation : void 0;
27741
+ if (confirmation !== "CLEAR") {
26578
27742
  return c.json({ success: false, error: "confirmation must be CLEAR" }, 400);
26579
27743
  }
26580
27744
  try {
@@ -26824,8 +27988,9 @@ async function main() {
26824
27988
  await scheduler.start();
26825
27989
  if (shuttingDown) return;
26826
27990
  if (cfg.dashboard.enabled) {
26827
- const { dashboardApp: dashboardApp2 } = await Promise.resolve().then(() => (init_web(), web_exports));
27991
+ const { dashboardApp: dashboardApp2, setDashboardRuntimeConfig: setDashboardRuntimeConfig2 } = await Promise.resolve().then(() => (init_web(), web_exports));
26828
27992
  if (shuttingDown) return;
27993
+ setDashboardRuntimeConfig2(cfg);
26829
27994
  dashboardServer = Bun.serve({
26830
27995
  hostname: "127.0.0.1",
26831
27996
  port: cfg.dashboard.port,
@@ -26879,185 +28044,6 @@ var init_gateway = __esm({
26879
28044
  }
26880
28045
  });
26881
28046
 
26882
- // src/core/semver.ts
26883
- function parseSemanticVersion(version2) {
26884
- const match2 = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec(version2);
26885
- if (!match2) return null;
26886
- const prerelease = match2[4]?.split(".") ?? [];
26887
- if (prerelease.some((identifier) => /^\d+$/.test(identifier) && identifier.length > 1 && identifier.startsWith("0"))) {
26888
- return null;
26889
- }
26890
- return {
26891
- major: Number(match2[1]),
26892
- minor: Number(match2[2]),
26893
- patch: Number(match2[3]),
26894
- prerelease
26895
- };
26896
- }
26897
- function compareSemanticVersions(left, right) {
26898
- const a = parseSemanticVersion(left);
26899
- const b = parseSemanticVersion(right);
26900
- if (!a || !b) return null;
26901
- for (const key of ["major", "minor", "patch"]) {
26902
- if (a[key] !== b[key]) return a[key] - b[key];
26903
- }
26904
- if (a.prerelease.length === 0 || b.prerelease.length === 0) {
26905
- if (a.prerelease.length === b.prerelease.length) return 0;
26906
- return a.prerelease.length === 0 ? 1 : -1;
26907
- }
26908
- const identifiers = Math.max(a.prerelease.length, b.prerelease.length);
26909
- for (let index2 = 0; index2 < identifiers; index2 += 1) {
26910
- const leftIdentifier = a.prerelease[index2];
26911
- const rightIdentifier = b.prerelease[index2];
26912
- if (leftIdentifier === void 0 || rightIdentifier === void 0) {
26913
- return leftIdentifier === rightIdentifier ? 0 : leftIdentifier === void 0 ? -1 : 1;
26914
- }
26915
- if (leftIdentifier === rightIdentifier) continue;
26916
- const leftNumeric = /^\d+$/.test(leftIdentifier);
26917
- const rightNumeric = /^\d+$/.test(rightIdentifier);
26918
- if (leftNumeric && rightNumeric) return Number(leftIdentifier) - Number(rightIdentifier);
26919
- if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1;
26920
- return leftIdentifier < rightIdentifier ? -1 : 1;
26921
- }
26922
- return 0;
26923
- }
26924
- function isSemanticVersion(version2) {
26925
- return parseSemanticVersion(version2) !== null;
26926
- }
26927
- var init_semver = __esm({
26928
- "src/core/semver.ts"() {
26929
- "use strict";
26930
- }
26931
- });
26932
-
26933
- // src/daemon/update.ts
26934
- var update_exports = {};
26935
- __export(update_exports, {
26936
- installLatestPlugin: () => installLatestPlugin,
26937
- installPluginVersion: () => installPluginVersion,
26938
- resolveInstalledPlugin: () => resolveInstalledPlugin
26939
- });
26940
- import { spawnSync as spawnSync3 } from "child_process";
26941
- import { existsSync as existsSync8, readFileSync as readFileSync5, readdirSync } from "fs";
26942
- import { homedir as homedir4 } from "os";
26943
- import { join as join6 } from "path";
26944
- function pluginAt(packageDir) {
26945
- const packageJson = join6(packageDir, "package.json");
26946
- const gatewayEntry = join6(packageDir, "dist/gateway/index.js");
26947
- if (!existsSync8(packageJson) || !existsSync8(gatewayEntry)) return null;
26948
- try {
26949
- const pkg = JSON.parse(readFileSync5(packageJson, "utf8"));
26950
- if (pkg.name !== PACKAGE_NAME || typeof pkg.version !== "string") return null;
26951
- return { packageDir, gatewayEntry, version: pkg.version };
26952
- } catch {
26953
- return null;
26954
- }
26955
- }
26956
- function compareVersions(left, right) {
26957
- return compareSemanticVersions(left, right) ?? left.localeCompare(right);
26958
- }
26959
- function cacheRoot() {
26960
- return process.env.SUPERTASK_OPENCODE_CACHE_DIR ?? join6(homedir4(), ".cache/opencode/packages");
26961
- }
26962
- function installedPlugins() {
26963
- const root = cacheRoot();
26964
- const packageDirs = existsSync8(root) ? readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && (entry.name === PACKAGE_NAME || entry.name.startsWith(`${PACKAGE_NAME}@`))).map((entry) => join6(root, entry.name, "node_modules", PACKAGE_NAME)) : [];
26965
- return packageDirs.map(pluginAt).filter((plugin) => plugin !== null);
26966
- }
26967
- function resolveInstalledPlugin() {
26968
- const override = process.env.SUPERTASK_PLUGIN_PACKAGE_DIR;
26969
- if (override) {
26970
- const plugin = pluginAt(override);
26971
- if (!plugin) throw new Error(`[supertask] \u5B89\u88C5\u5305\u65E0\u6548\u6216\u7F3A\u5C11 Gateway \u6784\u5EFA\u4EA7\u7269: ${override}`);
26972
- return plugin;
26973
- }
26974
- const installed = installedPlugins().sort((left, right) => compareVersions(right.version, left.version));
26975
- if (!installed[0]) {
26976
- throw new Error(`[supertask] OpenCode \u63D2\u4EF6\u7F13\u5B58\u4E2D\u627E\u4E0D\u5230\u53EF\u8FD0\u884C\u7684 ${PACKAGE_NAME}`);
26977
- }
26978
- return installed[0];
26979
- }
26980
- function opencodeBin() {
26981
- return process.env.SUPERTASK_OPENCODE_BIN ?? "opencode";
26982
- }
26983
- function npmBin() {
26984
- return process.env.SUPERTASK_NPM_BIN ?? "npm";
26985
- }
26986
- function latestVersion() {
26987
- const result = spawnSync3(npmBin(), [
26988
- "view",
26989
- PACKAGE_NAME,
26990
- "dist-tags.latest",
26991
- "--json"
26992
- ], {
26993
- encoding: "utf8",
26994
- env: process.env,
26995
- timeout: 3e4
26996
- });
26997
- const output = `${result.stdout ?? ""}`.trim();
26998
- if (result.error) {
26999
- throw new Error(`[supertask] \u67E5\u8BE2 npm latest \u5931\u8D25: ${result.error.message}`);
27000
- }
27001
- if (result.status !== 0) {
27002
- const detail = `${result.stderr ?? ""}`.trim();
27003
- throw new Error(`[supertask] \u67E5\u8BE2 npm latest \u5931\u8D25: ${detail || `\u9000\u51FA\u7801 ${result.status}`}`);
27004
- }
27005
- let response;
27006
- try {
27007
- response = JSON.parse(output);
27008
- } catch {
27009
- throw new Error(`[supertask] npm latest \u8FD4\u56DE\u65E0\u6CD5\u89E3\u6790: ${output || "(empty)"}`);
27010
- }
27011
- const versions = typeof response === "string" ? [response] : Array.isArray(response) && response.every((value) => typeof value === "string") ? response : [];
27012
- const uniqueVersions = [...new Set(versions)];
27013
- if (uniqueVersions.length !== 1 || !isSemanticVersion(uniqueVersions[0])) {
27014
- throw new Error(`[supertask] npm latest \u7248\u672C\u65E0\u6548: ${String(response)}`);
27015
- }
27016
- return uniqueVersions[0];
27017
- }
27018
- function resolveInstalledVersion(expectedVersion) {
27019
- const override = process.env.SUPERTASK_PLUGIN_PACKAGE_DIR;
27020
- const installed = override ? [pluginAt(override)].filter((plugin) => plugin !== null) : installedPlugins();
27021
- const matched = installed.find((plugin) => plugin.version === expectedVersion);
27022
- if (matched) return matched;
27023
- const actual = installed.length > 0 ? installed.map((plugin) => plugin.version).join(", ") : "\u672A\u627E\u5230\u53EF\u8FD0\u884C\u7F13\u5B58";
27024
- throw new Error(`[supertask] OpenCode \u63D2\u4EF6\u7F13\u5B58\u7248\u672C\u4E0D\u5339\u914D\uFF1A\u671F\u671B ${expectedVersion}\uFF0C\u5B9E\u9645 ${actual}`);
27025
- }
27026
- function installPluginVersion(version2) {
27027
- if (!isSemanticVersion(version2)) {
27028
- throw new Error(`[supertask] OpenCode \u63D2\u4EF6\u7248\u672C\u65E0\u6548: ${version2}`);
27029
- }
27030
- const result = spawnSync3(opencodeBin(), [
27031
- "plugin",
27032
- `${PACKAGE_NAME}@${version2}`,
27033
- "--global",
27034
- "--force"
27035
- ], {
27036
- encoding: "utf8",
27037
- env: process.env,
27038
- timeout: 12e4
27039
- });
27040
- const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
27041
- if (result.error) {
27042
- throw new Error(`[supertask] OpenCode \u63D2\u4EF6\u66F4\u65B0\u5931\u8D25: ${result.error.message}`);
27043
- }
27044
- if (result.status !== 0) {
27045
- throw new Error(`[supertask] OpenCode \u63D2\u4EF6\u66F4\u65B0\u5931\u8D25: ${output || `\u9000\u51FA\u7801 ${result.status}`}`);
27046
- }
27047
- return resolveInstalledVersion(version2);
27048
- }
27049
- function installLatestPlugin() {
27050
- return installPluginVersion(latestVersion());
27051
- }
27052
- var PACKAGE_NAME;
27053
- var init_update2 = __esm({
27054
- "src/daemon/update.ts"() {
27055
- "use strict";
27056
- init_semver();
27057
- PACKAGE_NAME = "opencode-supertask";
27058
- }
27059
- });
27060
-
27061
28047
  // node_modules/commander/esm.mjs
27062
28048
  var import_index = __toESM(require_commander(), 1);
27063
28049
  var {
@@ -27081,37 +28067,7 @@ init_task_run_service();
27081
28067
  init_task_template_service();
27082
28068
  init_database_maintenance_service();
27083
28069
  init_db2();
27084
-
27085
- // src/core/duration.ts
27086
- var DURATION_REGEX = /^(\d+(?:\.\d+)?)\s*(ms|s|sec|seconds?|min|minutes?|m|h|hours?|d|days?|w|weeks?)$/i;
27087
- var ISO8601_REGEX = /^P(?:([.\d]+)D)?(?:T(?:([.\d]+)H)?(?:([.\d]+)M)?(?:([.\d]+)S)?)?$/i;
27088
- function parseDuration(input) {
27089
- const trimmed = input.trim();
27090
- const simple = DURATION_REGEX.exec(trimmed);
27091
- if (simple) {
27092
- const value = parseFloat(simple[1]);
27093
- const unit = simple[2].toLowerCase();
27094
- if (unit === "ms") return value;
27095
- if (unit === "s" || unit === "sec" || unit === "second" || unit === "seconds") return value * 1e3;
27096
- if (unit === "min" || unit === "minute" || unit === "minutes" || unit === "m") return value * 6e4;
27097
- if (unit === "h" || unit === "hour" || unit === "hours") return value * 36e5;
27098
- if (unit === "d" || unit === "day" || unit === "days") return value * 864e5;
27099
- if (unit === "w" || unit === "week" || unit === "weeks") return value * 6048e5;
27100
- }
27101
- const iso = ISO8601_REGEX.exec(trimmed);
27102
- if (iso) {
27103
- const days = parseFloat(iso[1] ?? "0");
27104
- const hours = parseFloat(iso[2] ?? "0");
27105
- const minutes = parseFloat(iso[3] ?? "0");
27106
- const seconds = parseFloat(iso[4] ?? "0");
27107
- return (days * 86400 + hours * 3600 + minutes * 60 + seconds) * 1e3;
27108
- }
27109
- const asNumber = Number(trimmed);
27110
- if (!isNaN(asNumber) && asNumber > 0) return asNumber;
27111
- return null;
27112
- }
27113
-
27114
- // src/cli/index.ts
28070
+ init_duration();
27115
28071
  init_pm2();
27116
28072
  init_config();
27117
28073
  import { spawnSync as spawnSync4 } from "child_process";
@@ -27245,6 +28201,7 @@ function parseTaskStatus(value) {
27245
28201
  }
27246
28202
 
27247
28203
  // src/cli/index.ts
28204
+ init_update2();
27248
28205
  async function withDb(fn, formatError = (error) => JSON.stringify({
27249
28206
  error: error instanceof Error ? error.message : String(error)
27250
28207
  })) {
@@ -27295,6 +28252,44 @@ program2.command("add").description("\u521B\u5EFA\u65B0\u4EFB\u52A1").requiredOp
27295
28252
  });
27296
28253
  console.log(JSON.stringify({ id: task.id, status: "created" }, null, 2));
27297
28254
  }));
28255
+ program2.command("edit").description("\u4FEE\u6539\u5F53\u524D\u9879\u76EE\u4E2D\u5C1A\u672A\u5B8C\u6210\u7684\u4EFB\u52A1").requiredOption("--id <id>", "\u4EFB\u52A1 ID").option("-n, --name <name>", "\u4EFB\u52A1\u540D\u79F0").option("-a, --agent <agent>", "Agent \u540D\u79F0").option("-m, --model <model>", "\u6A21\u578B").option("-p, --prompt <prompt>", "\u63D0\u793A\u8BCD").option("-c, --category <category>", "\u5206\u7C7B").option("-i, --importance <number>", "\u91CD\u8981\u7A0B\u5EA6 (1-5)").option("-u, --urgency <number>", "\u7D27\u6025\u7A0B\u5EA6 (1-5)").option("-b, --batch <batchId>", "\u6279\u6B21 ID").option("--clear-batch", "\u6E05\u7A7A\u6279\u6B21 ID").option("--max-retries <number>", "\u9996\u6B21\u6267\u884C\u4E4B\u5916\u5141\u8BB8\u7684\u91CD\u8BD5\u6B21\u6570").option("--retry-backoff <duration>", "\u91CD\u8BD5\u9000\u907F\u57FA\u7840\u95F4\u9694\uFF0C\u5982 30s / 5min").option("--timeout <duration>", "\u4EFB\u52A1\u786C\u8D85\u65F6\uFF0C\u5982 30min / 2h").option("--clear-timeout", "\u6E05\u7A7A\u4EFB\u52A1\u7EA7\u8D85\u65F6\uFF0C\u6539\u7528 Gateway \u9ED8\u8BA4\u503C").action(async (options) => withDb(async () => {
28256
+ if (options.batch !== void 0 && options.clearBatch) {
28257
+ throw new Error("batch \u548C clear-batch \u4E0D\u80FD\u540C\u65F6\u4F7F\u7528");
28258
+ }
28259
+ if (options.timeout !== void 0 && options.clearTimeout) {
28260
+ throw new Error("timeout \u548C clear-timeout \u4E0D\u80FD\u540C\u65F6\u4F7F\u7528");
28261
+ }
28262
+ const update = {};
28263
+ for (const field of ["name", "agent", "model", "prompt", "category"]) {
28264
+ if (options[field] !== void 0) update[field] = options[field];
28265
+ }
28266
+ if (options.importance !== void 0) {
28267
+ update.importance = parseBoundedInteger(options.importance, "importance", 1, 5);
28268
+ }
28269
+ if (options.urgency !== void 0) {
28270
+ update.urgency = parseBoundedInteger(options.urgency, "urgency", 1, 5);
28271
+ }
28272
+ if (options.maxRetries !== void 0) {
28273
+ update.maxRetries = parseBoundedInteger(options.maxRetries, "max-retries", 0, 1e3);
28274
+ }
28275
+ if (options.batch !== void 0 || options.clearBatch) {
28276
+ update.batchId = options.clearBatch ? null : options.batch;
28277
+ }
28278
+ if (options.retryBackoff !== void 0) {
28279
+ const retryBackoffMs = parseDuration(options.retryBackoff);
28280
+ if (retryBackoffMs === null) throw new Error("retry-backoff \u683C\u5F0F\u65E0\u6548");
28281
+ update.retryBackoffMs = retryBackoffMs;
28282
+ }
28283
+ if (options.timeout !== void 0 || options.clearTimeout) {
28284
+ const timeoutMs = options.clearTimeout ? null : parseDuration(options.timeout);
28285
+ if (timeoutMs === null && !options.clearTimeout) throw new Error("timeout \u683C\u5F0F\u65E0\u6548");
28286
+ update.timeoutMs = timeoutMs;
28287
+ }
28288
+ const id = parsePositiveInteger(options.id, "id");
28289
+ const task = await TaskService.update(id, update, { cwd: process.cwd() });
28290
+ if (!task) throw new Error(`\u4EFB\u52A1 #${id} \u4E0D\u5B58\u5728\u4E8E\u5F53\u524D\u9879\u76EE\uFF0C\u6216\u5176\u72B6\u6001\u4E0D\u5141\u8BB8\u7F16\u8F91`);
28291
+ console.log(JSON.stringify({ id: task.id, status: task.status, updated: true }, null, 2));
28292
+ }));
27298
28293
  program2.command("next").description("\u83B7\u53D6\u4E0B\u4E00\u4E2A\u5F85\u6267\u884C\u7684\u4EFB\u52A1").action(async () => withDb(async () => {
27299
28294
  const task = await TaskService.next({ cwd: process.cwd() });
27300
28295
  if (task) {
@@ -27379,7 +28374,7 @@ program2.command("delete").description("\u5220\u9664\u4EFB\u52A1").requiredOptio
27379
28374
  console.log(JSON.stringify({ deleted, id }));
27380
28375
  }));
27381
28376
  program2.command("template").description("\u7BA1\u7406\u4EFB\u52A1\u8C03\u5EA6\u6A21\u677F").addCommand(
27382
- new Command("add").description("\u521B\u5EFA\u8C03\u5EA6\u6A21\u677F").requiredOption("-n, --name <name>", "\u6A21\u677F\u540D\u79F0").requiredOption("-a, --agent <agent>", "Agent \u540D\u79F0").requiredOption("-p, --prompt <prompt>", "\u63D0\u793A\u8BCD").requiredOption("-t, --type <type>", "\u8C03\u5EA6\u7C7B\u578B\uFF1Acron/delayed/recurring").option("--cron <expr>", "cron \u8868\u8FBE\u5F0F\uFF08cron \u7C7B\u578B\u5FC5\u586B\uFF09").option("--delay <duration>", "\u5EF6\u8FDF\u65F6\u95F4\uFF08delayed \u7C7B\u578B\u5FC5\u586B\uFF09\uFF0C\u5982 30s / 5min / 1h / 2d").option("--interval <duration>", "\u5FAA\u73AF\u95F4\u9694\uFF08recurring \u7C7B\u578B\u5FC5\u586B\uFF09\uFF0C\u5982 1h / 30min / 5s").option("-m, --model <model>", "\u6A21\u578B").option("-c, --category <category>", "\u5206\u7C7B", "general").option("-i, --importance <number>", "\u91CD\u8981\u7A0B\u5EA6 1-5", "3").option("-u, --urgency <number>", "\u7D27\u6025\u7A0B\u5EA6 1-5", "3").option("-b, --batch <batchId>", "\u6A21\u677F\u751F\u6210\u4EFB\u52A1\u7684\u6279\u6B21 ID").option("--max-instances <number>", "\u6700\u5927\u5E76\u53D1\u5B9E\u4F8B\u6570", "1").option("--max-retries <number>", "\u6700\u5927\u91CD\u8BD5\u6B21\u6570", "3").option("--retry-backoff <duration>", "\u9000\u907F\u57FA\u7840\u95F4\u9694\uFF0C\u5982 30s / 5min", "30s").option("--timeout <duration>", "\u6BCF\u6B21\u4EFB\u52A1\u786C\u8D85\u65F6\uFF0C\u5982 30min / 2h").action(async (options) => withDb(async () => {
28377
+ new Command("add").description("\u521B\u5EFA\u8C03\u5EA6\u6A21\u677F").requiredOption("-n, --name <name>", "\u6A21\u677F\u540D\u79F0").requiredOption("-a, --agent <agent>", "Agent \u540D\u79F0").requiredOption("-p, --prompt <prompt>", "\u63D0\u793A\u8BCD").requiredOption("-t, --type <type>", "\u8C03\u5EA6\u7C7B\u578B\uFF1Acron/delayed/recurring").option("--cron <expr>", "cron \u8868\u8FBE\u5F0F\uFF08cron \u7C7B\u578B\u5FC5\u586B\uFF09").option("--delay <duration>", "\u5EF6\u8FDF\u65F6\u95F4\uFF08delayed \u7C7B\u578B\u5FC5\u586B\uFF09\uFF0C\u5982 30s / 5min / 1h / 2d").option("--interval <duration>", "\u5FAA\u73AF\u95F4\u9694\uFF08recurring \u7C7B\u578B\u5FC5\u586B\uFF09\uFF0C\u5982 1h / 30min / 5s").option("-m, --model <model>", "\u6A21\u578B").option("-c, --category <category>", "\u5206\u7C7B", "general").option("-i, --importance <number>", "\u91CD\u8981\u7A0B\u5EA6 1-5", "3").option("-u, --urgency <number>", "\u7D27\u6025\u7A0B\u5EA6 1-5", "3").option("-b, --batch <batchId>", "\u6A21\u677F\u751F\u6210\u4EFB\u52A1\u7684\u6279\u6B21 ID").option("--max-instances <number>", "\u81EA\u52A8\u8C03\u5EA6\u6D3B\u8DC3\u5B9E\u4F8B\u4E0A\u9650\uFF08\u624B\u52A8\u89E6\u53D1\u4E0D\u53D7\u9650\uFF09", "1").option("--max-retries <number>", "\u6700\u5927\u91CD\u8BD5\u6B21\u6570", "3").option("--retry-backoff <duration>", "\u9000\u907F\u57FA\u7840\u95F4\u9694\uFF0C\u5982 30s / 5min", "30s").option("--timeout <duration>", "\u6BCF\u6B21\u4EFB\u52A1\u786C\u8D85\u65F6\uFF0C\u5982 30min / 2h").action(async (options) => withDb(async () => {
27383
28378
  let intervalMs = null;
27384
28379
  let runAt = null;
27385
28380
  const retryBackoffMs = parseDuration(options.retryBackoff);
@@ -27540,6 +28535,7 @@ program2.command("doctor").description("\u68C0\u67E5 OpenCode\u3001\u6570\u636E\
27540
28535
  config.watchdog.heartbeatTimeoutMs
27541
28536
  );
27542
28537
  const gateway = getGatewayDiagnostic();
28538
+ const packageVersion = getPackageVersion();
27543
28539
  const opencodeBin2 = process.env.SUPERTASK_OPENCODE_BIN ?? "opencode";
27544
28540
  const opencodeResult = spawnSync4(opencodeBin2, ["--version"], {
27545
28541
  encoding: "utf8",
@@ -27551,6 +28547,7 @@ program2.command("doctor").description("\u68C0\u67E5 OpenCode\u3001\u6570\u636E\
27551
28547
  version: opencodeResult.status === 0 ? opencodeResult.stdout.trim() : null,
27552
28548
  error: opencodeResult.status === 0 ? null : opencodeResult.error?.message || opencodeResult.stderr.trim() || `\u9000\u51FA\u7801 ${opencodeResult.status}`
27553
28549
  };
28550
+ const plugin = getOpenCodePluginDiagnostic();
27554
28551
  let dashboard = {
27555
28552
  enabled: config.dashboard.enabled,
27556
28553
  ok: !config.dashboard.enabled,
@@ -27570,6 +28567,26 @@ program2.command("doctor").description("\u68C0\u67E5 OpenCode\u3001\u6570\u636E\
27570
28567
  }
27571
28568
  }
27572
28569
  const warnings = [];
28570
+ const gatewayEntryPinned = gateway.gatewayEntry === null || !/[\\/]opencode-supertask@(latest|next)[\\/]/.test(gateway.gatewayEntry);
28571
+ const gatewayVersionMatchesPackage = gateway.gatewayPackageVersion !== null && gateway.runningVersion === gateway.gatewayPackageVersion;
28572
+ const configuredVersionsMatch = plugin.ok && plugin.version === gateway.gatewayPackageVersion;
28573
+ if (!plugin.ok && plugin.error) {
28574
+ warnings.push(plugin.error);
28575
+ }
28576
+ if (plugin.version !== null && plugin.version !== packageVersion) {
28577
+ warnings.push(`\u5F53\u524D CLI v${packageVersion} \u4E0E OpenCode \u63D2\u4EF6 v${plugin.version} \u4E0D\u4E00\u81F4\uFF1B\u8BF7\u7528\u539F\u5305\u7BA1\u7406\u5668\u5168\u5C40\u5B89\u88C5 opencode-supertask@${plugin.version}\uFF08npm install -g \u6216 bun add -g\uFF09`);
28578
+ }
28579
+ if (!gatewayEntryPinned) {
28580
+ warnings.push(`PM2 Gateway \u4ECD\u4ECE\u6D6E\u52A8\u7F13\u5B58\u8DEF\u5F84\u542F\u52A8\uFF1A${gateway.gatewayEntry}`);
28581
+ }
28582
+ if (gateway.processFound && gateway.gatewayPackageVersion === null) {
28583
+ warnings.push(`\u65E0\u6CD5\u4ECE PM2 Gateway \u5165\u53E3\u786E\u8BA4 opencode-supertask \u5305\u7248\u672C\uFF1A${gateway.gatewayEntry ?? "unknown"}`);
28584
+ } else if (gateway.processFound && !gatewayVersionMatchesPackage) {
28585
+ warnings.push(`Gateway ready \u9501\u7248\u672C ${gateway.runningVersion ?? "unknown"} \u4E0E\u5165\u53E3\u5305\u7248\u672C ${gateway.gatewayPackageVersion ?? "unknown"} \u4E0D\u4E00\u81F4`);
28586
+ }
28587
+ if (plugin.version !== null && gateway.gatewayPackageVersion !== null && plugin.version !== gateway.gatewayPackageVersion) {
28588
+ warnings.push(`OpenCode \u63D2\u4EF6 v${plugin.version} \u4E0E PM2 Gateway v${gateway.gatewayPackageVersion} \u4E0D\u4E00\u81F4\uFF1B\u6267\u884C supertask upgrade`);
28589
+ }
27573
28590
  if (gateway.pm2Installed && !gateway.logRotationInstalled) {
27574
28591
  warnings.push("\u672A\u68C0\u6D4B\u5230 pm2-logrotate\uFF1B\u957F\u671F\u8FD0\u884C\u524D\u5EFA\u8BAE\u5B89\u88C5\u5E76\u9650\u5236\u65E5\u5FD7\u4FDD\u7559\u91CF");
27575
28592
  }
@@ -27587,12 +28604,13 @@ program2.command("doctor").description("\u68C0\u67E5 OpenCode\u3001\u6570\u636E\
27587
28604
  `\u65E7\u7248\u9694\u79BB run #${run.runId}\uFF1A${owner}${cancel}\u786E\u8BA4\u6CA1\u6709\u9057\u7559 OpenCode \u8FDB\u7A0B\u540E\u6267\u884C supertask run abandon --id ${run.runId} --confirm ABANDON`
27588
28605
  );
27589
28606
  }
27590
- const ok = opencode.ok && database.ok && legacyQuarantinedRuns.length === 0 && gateway.pm2Installed && gateway.status === "online" && gateway.ready && gateway.scopeMatches && gateway.logRotationInstalled && gateway.startupConfigured !== false && dashboard.ok;
28607
+ const ok = opencode.ok && plugin.ok && database.ok && legacyQuarantinedRuns.length === 0 && gateway.pm2Installed && gateway.status === "online" && gateway.ready && gatewayEntryPinned && gatewayVersionMatchesPackage && configuredVersionsMatch && gateway.scopeMatches && gateway.logRotationInstalled && gateway.startupConfigured !== false && dashboard.ok;
27591
28608
  const report = {
27592
28609
  ok,
27593
- packageVersion: getPackageVersion(),
28610
+ packageVersion,
27594
28611
  configPath: getConfigPath(),
27595
28612
  opencode,
28613
+ plugin,
27596
28614
  database,
27597
28615
  legacyQuarantinedRuns,
27598
28616
  gateway,
@@ -27606,8 +28624,9 @@ program2.command("doctor").description("\u68C0\u67E5 OpenCode\u3001\u6570\u636E\
27606
28624
  const mark = (value) => value ? "\u2713" : "\u2717";
27607
28625
  console.log(`SuperTask doctor: ${ok ? "\u6B63\u5E38" : "\u5F02\u5E38"}`);
27608
28626
  console.log(`${mark(opencode.ok)} OpenCode ${opencode.version ?? opencode.error ?? "\u4E0D\u53EF\u7528"}`);
28627
+ console.log(`${mark(plugin.ok)} OpenCode \u63D2\u4EF6 ${plugin.spec || plugin.error || "\u672A\u914D\u7F6E"}${plugin.cachedVersion ? `\uFF08\u7F13\u5B58 v${plugin.cachedVersion}\uFF09` : ""}`);
27609
28628
  console.log(`${mark(database.ok)} \u6570\u636E\u5E93 ${database.path}\uFF08\u4EFB\u52A1 ${database.counts.tasks}\uFF0C\u8FD0\u884C\u4E2D ${database.runningTasks}\uFF09`);
27610
- console.log(`${mark(gateway.status === "online" && gateway.ready)} Gateway ${gateway.status ?? "missing"}${gateway.pid ? `\uFF0CPID ${gateway.pid}` : ""}${gateway.runningVersion ? `\uFF0Cv${gateway.runningVersion}` : ""}`);
28629
+ console.log(`${mark(gateway.status === "online" && gateway.ready && gatewayEntryPinned && gatewayVersionMatchesPackage)} Gateway ${gateway.status ?? "missing"}${gateway.pid ? `\uFF0CPID ${gateway.pid}` : ""}${gateway.runningVersion ? `\uFF0Cv${gateway.runningVersion}` : ""}${gateway.gatewayEntry ? `\uFF0C${gateway.gatewayEntry}` : ""}`);
27611
28630
  console.log(`${mark(dashboard.ok)} Dashboard ${dashboard.enabled ? dashboard.url : "\u5DF2\u7981\u7528"}`);
27612
28631
  for (const warning of warnings) console.log(`! ${warning}`);
27613
28632
  }
@@ -27663,6 +28682,9 @@ program2.command("upgrade").description("Update OpenCode plugin cache and restar
27663
28682
  console.log(`
27664
28683
  SuperTask upgraded: ${result.before ?? "unknown"} \u2192 ${result.after}`);
27665
28684
  console.log("Gateway restarted. Please restart opencode to load the new plugin.");
28685
+ if (getPackageVersion() !== installed.version) {
28686
+ console.log(`Global CLI remains v${getPackageVersion()}. Update it with your original package manager to opencode-supertask@${installed.version} (npm install -g or bun add -g).`);
28687
+ }
27666
28688
  } catch (err) {
27667
28689
  let detail = err instanceof Error ? err.message : String(err);
27668
28690
  try {