opencode-supertask 0.1.31 → 0.1.32-rc.2

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}`);
@@ -20989,6 +21232,26 @@ function isMacLaunchAgentConfigured(expectedPm2Home, expectedRuntime) {
20989
21232
  return false;
20990
21233
  }
20991
21234
  }
21235
+ function bootstrapMacLaunchAgent(domain, path) {
21236
+ const retryDeadline = Date.now() + 2e3;
21237
+ const sleeper = new Int32Array(new SharedArrayBuffer(4));
21238
+ let result = spawnSync2(launchctlBin(), ["bootstrap", domain, path], {
21239
+ encoding: "utf8",
21240
+ timeout: pm2CommandTimeoutMs(),
21241
+ killSignal: "SIGKILL"
21242
+ });
21243
+ while (result.status !== 0 && Date.now() < retryDeadline) {
21244
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
21245
+ if (result.status !== 5 && !output.includes("Bootstrap failed: 5:")) break;
21246
+ Atomics.wait(sleeper, 0, 0, 100);
21247
+ result = spawnSync2(launchctlBin(), ["bootstrap", domain, path], {
21248
+ encoding: "utf8",
21249
+ timeout: pm2CommandTimeoutMs(),
21250
+ killSignal: "SIGKILL"
21251
+ });
21252
+ }
21253
+ return result;
21254
+ }
20992
21255
  function installMacLaunchAgent(expectedRuntime = currentGatewayRuntime()) {
20993
21256
  if (typeof process.getuid !== "function") {
20994
21257
  throw new Error("[supertask] \u5F53\u524D\u8FD0\u884C\u65F6\u65E0\u6CD5\u83B7\u53D6 macOS \u7528\u6237 ID");
@@ -21056,11 +21319,7 @@ function installMacLaunchAgent(expectedRuntime = currentGatewayRuntime()) {
21056
21319
  timeout: pm2CommandTimeoutMs(),
21057
21320
  killSignal: "SIGKILL"
21058
21321
  });
21059
- const loaded = spawnSync2(launchctlBin(), ["bootstrap", domain, path], {
21060
- encoding: "utf8",
21061
- timeout: pm2CommandTimeoutMs(),
21062
- killSignal: "SIGKILL"
21063
- });
21322
+ const loaded = bootstrapMacLaunchAgent(domain, path);
21064
21323
  const configuredVerifyTimeout = Number(process.env.SUPERTASK_LAUNCH_AGENT_VERIFY_TIMEOUT_MS ?? 2e3);
21065
21324
  const verifyTimeoutMs = Number.isFinite(configuredVerifyTimeout) && configuredVerifyTimeout > 0 ? configuredVerifyTimeout : 2e3;
21066
21325
  const verifyDeadline = Date.now() + verifyTimeoutMs;
@@ -21086,11 +21345,7 @@ function installMacLaunchAgent(expectedRuntime = currentGatewayRuntime()) {
21086
21345
  }
21087
21346
  let rollbackFailure = "";
21088
21347
  if (wasLoaded && previousPlist != null) {
21089
- const restored = spawnSync2(launchctlBin(), ["bootstrap", domain, path], {
21090
- encoding: "utf8",
21091
- timeout: pm2CommandTimeoutMs(),
21092
- killSignal: "SIGKILL"
21093
- });
21348
+ const restored = bootstrapMacLaunchAgent(domain, path);
21094
21349
  if (restored.status !== 0) {
21095
21350
  rollbackFailure = `\uFF1B\u65E7 LaunchAgent \u6062\u590D\u5931\u8D25: ${`${restored.stdout ?? ""}${restored.stderr ?? ""}`.trim() || `\u9000\u51FA\u7801 ${restored.status}`}`;
21096
21351
  }
@@ -21257,7 +21512,7 @@ function gatewayRuntimeFromProcess(processInfo) {
21257
21512
  if (typeof savedBunPath !== "string" || typeof savedCwd !== "string") return null;
21258
21513
  try {
21259
21514
  accessSync(savedBunPath, constants.X_OK);
21260
- if (!statSync2(savedCwd).isDirectory()) return null;
21515
+ if (!statSync3(savedCwd).isDirectory()) return null;
21261
21516
  if (spawnSync2(savedBunPath, ["--version"], {
21262
21517
  stdio: "ignore",
21263
21518
  env: savedEnv,
@@ -21464,7 +21719,7 @@ function pm2StartGateway(runtime = currentGatewayRuntime()) {
21464
21719
  try {
21465
21720
  accessSync(runtime.bunPath, constants.X_OK);
21466
21721
  accessSync(runtime.gatewayEntry, constants.R_OK);
21467
- if (!statSync2(runtime.cwd).isDirectory()) throw new Error("cwd is not a directory");
21722
+ if (!statSync3(runtime.cwd).isDirectory()) throw new Error("cwd is not a directory");
21468
21723
  } catch (error) {
21469
21724
  throw new Error(`[supertask] Gateway \u76EE\u6807\u8FD0\u884C\u65F6\u4E0D\u53EF\u7528: ${error instanceof Error ? error.message : String(error)}`);
21470
21725
  }
@@ -21854,98 +22109,369 @@ var init_pm2 = __esm({
21854
22109
  }
21855
22110
  });
21856
22111
 
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
- }
22112
+ // src/core/semver.ts
22113
+ function parseSemanticVersion(version2) {
22114
+ 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);
22115
+ if (!match2) return null;
22116
+ const prerelease = match2[4]?.split(".") ?? [];
22117
+ if (prerelease.some((identifier) => /^\d+$/.test(identifier) && identifier.length > 1 && identifier.startsWith("0"))) {
22118
+ return null;
22119
+ }
22120
+ return {
22121
+ major: Number(match2[1]),
22122
+ minor: Number(match2[2]),
22123
+ patch: Number(match2[3]),
22124
+ prerelease
21887
22125
  };
21888
22126
  }
21889
- function markGatewayActivity(component) {
21890
- if (state) state.lastActivityAt[component] = Date.now();
22127
+ function compareSemanticVersions(left, right) {
22128
+ const a = parseSemanticVersion(left);
22129
+ const b = parseSemanticVersion(right);
22130
+ if (!a || !b) return null;
22131
+ for (const key of ["major", "minor", "patch"]) {
22132
+ if (a[key] !== b[key]) return a[key] - b[key];
22133
+ }
22134
+ if (a.prerelease.length === 0 || b.prerelease.length === 0) {
22135
+ if (a.prerelease.length === b.prerelease.length) return 0;
22136
+ return a.prerelease.length === 0 ? 1 : -1;
22137
+ }
22138
+ const identifiers = Math.max(a.prerelease.length, b.prerelease.length);
22139
+ for (let index2 = 0; index2 < identifiers; index2 += 1) {
22140
+ const leftIdentifier = a.prerelease[index2];
22141
+ const rightIdentifier = b.prerelease[index2];
22142
+ if (leftIdentifier === void 0 || rightIdentifier === void 0) {
22143
+ return leftIdentifier === rightIdentifier ? 0 : leftIdentifier === void 0 ? -1 : 1;
22144
+ }
22145
+ if (leftIdentifier === rightIdentifier) continue;
22146
+ const leftNumeric = /^\d+$/.test(leftIdentifier);
22147
+ const rightNumeric = /^\d+$/.test(rightIdentifier);
22148
+ if (leftNumeric && rightNumeric) return Number(leftIdentifier) - Number(rightIdentifier);
22149
+ if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1;
22150
+ return leftIdentifier < rightIdentifier ? -1 : 1;
22151
+ }
22152
+ return 0;
21891
22153
  }
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;
22154
+ function isSemanticVersion(version2) {
22155
+ return parseSemanticVersion(version2) !== null;
21898
22156
  }
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
- };
22157
+ var init_semver = __esm({
22158
+ "src/core/semver.ts"() {
22159
+ "use strict";
22160
+ }
22161
+ });
22162
+
22163
+ // src/daemon/update.ts
22164
+ var update_exports = {};
22165
+ __export(update_exports, {
22166
+ getOpenCodePluginDiagnostic: () => getOpenCodePluginDiagnostic,
22167
+ installLatestPlugin: () => installLatestPlugin,
22168
+ installPluginVersion: () => installPluginVersion,
22169
+ resolveConfiguredPluginSpec: () => resolveConfiguredPluginSpec,
22170
+ resolveInstalledPlugin: () => resolveInstalledPlugin,
22171
+ resolveInstalledPluginVersion: () => resolveInstalledPluginVersion
22172
+ });
22173
+ import { spawnSync as spawnSync3 } from "child_process";
22174
+ import { existsSync as existsSync6, readFileSync as readFileSync4, readdirSync } from "fs";
22175
+ import { homedir as homedir4 } from "os";
22176
+ import { join as join5 } from "path";
22177
+ function pluginAt(packageDir) {
22178
+ const packageJson = join5(packageDir, "package.json");
22179
+ const gatewayEntry = join5(packageDir, "dist/gateway/index.js");
22180
+ if (!existsSync6(packageJson) || !existsSync6(gatewayEntry)) return null;
22181
+ try {
22182
+ const pkg = JSON.parse(readFileSync4(packageJson, "utf8"));
22183
+ if (pkg.name !== PACKAGE_NAME || typeof pkg.version !== "string") return null;
22184
+ return { packageDir, gatewayEntry, version: pkg.version };
22185
+ } catch {
22186
+ return null;
22187
+ }
21908
22188
  }
21909
- function resetGatewayHealth() {
21910
- state = null;
22189
+ function compareVersions(left, right) {
22190
+ return compareSemanticVersions(left, right) ?? left.localeCompare(right);
21911
22191
  }
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;
21916
- 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
21925
- };
22192
+ function cacheRoot() {
22193
+ return process.env.SUPERTASK_OPENCODE_CACHE_DIR ?? join5(homedir4(), ".cache/opencode/packages");
21926
22194
  }
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,
22195
+ function installedPlugins() {
22196
+ const root = cacheRoot();
22197
+ 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)) : [];
22198
+ return packageDirs.map(pluginAt).filter((plugin) => plugin !== null);
22199
+ }
22200
+ function resolveInstalledPlugin() {
22201
+ const override = process.env.SUPERTASK_PLUGIN_PACKAGE_DIR;
22202
+ if (override) {
22203
+ const plugin = pluginAt(override);
22204
+ if (!plugin) throw new Error(`[supertask] \u5B89\u88C5\u5305\u65E0\u6548\u6216\u7F3A\u5C11 Gateway \u6784\u5EFA\u4EA7\u7269: ${override}`);
22205
+ return plugin;
22206
+ }
22207
+ const installed = installedPlugins().sort((left, right) => compareVersions(right.version, left.version));
22208
+ if (!installed[0]) {
22209
+ throw new Error(`[supertask] OpenCode \u63D2\u4EF6\u7F13\u5B58\u4E2D\u627E\u4E0D\u5230\u53EF\u8FD0\u884C\u7684 ${PACKAGE_NAME}`);
22210
+ }
22211
+ return installed[0];
22212
+ }
22213
+ function opencodeBin() {
22214
+ return process.env.SUPERTASK_OPENCODE_BIN ?? "opencode";
22215
+ }
22216
+ function npmBin() {
22217
+ return process.env.SUPERTASK_NPM_BIN ?? "npm";
22218
+ }
22219
+ function latestVersion() {
22220
+ const result = spawnSync3(npmBin(), [
22221
+ "view",
22222
+ PACKAGE_NAME,
22223
+ "dist-tags.latest",
22224
+ "--json"
22225
+ ], {
22226
+ encoding: "utf8",
22227
+ env: process.env,
22228
+ timeout: 3e4
22229
+ });
22230
+ const output = `${result.stdout ?? ""}`.trim();
22231
+ if (result.error) {
22232
+ throw new Error(`[supertask] \u67E5\u8BE2 npm latest \u5931\u8D25: ${result.error.message}`);
22233
+ }
22234
+ if (result.status !== 0) {
22235
+ const detail = `${result.stderr ?? ""}`.trim();
22236
+ throw new Error(`[supertask] \u67E5\u8BE2 npm latest \u5931\u8D25: ${detail || `\u9000\u51FA\u7801 ${result.status}`}`);
22237
+ }
22238
+ let response;
22239
+ try {
22240
+ response = JSON.parse(output);
22241
+ } catch {
22242
+ throw new Error(`[supertask] npm latest \u8FD4\u56DE\u65E0\u6CD5\u89E3\u6790: ${output || "(empty)"}`);
22243
+ }
22244
+ const versions = typeof response === "string" ? [response] : Array.isArray(response) && response.every((value) => typeof value === "string") ? response : [];
22245
+ const uniqueVersions = [...new Set(versions)];
22246
+ if (uniqueVersions.length !== 1 || !isSemanticVersion(uniqueVersions[0])) {
22247
+ throw new Error(`[supertask] npm latest \u7248\u672C\u65E0\u6548: ${String(response)}`);
22248
+ }
22249
+ return uniqueVersions[0];
22250
+ }
22251
+ function resolveInstalledPluginVersion(expectedVersion) {
22252
+ const override = process.env.SUPERTASK_PLUGIN_PACKAGE_DIR;
22253
+ const installed = override ? [pluginAt(override)].filter((plugin) => plugin !== null) : installedPlugins();
22254
+ const matched = installed.find((plugin) => plugin.version === expectedVersion);
22255
+ if (matched) return matched;
22256
+ const actual = installed.length > 0 ? installed.map((plugin) => plugin.version).join(", ") : "\u672A\u627E\u5230\u53EF\u8FD0\u884C\u7F13\u5B58";
22257
+ throw new Error(`[supertask] OpenCode \u63D2\u4EF6\u7F13\u5B58\u7248\u672C\u4E0D\u5339\u914D\uFF1A\u671F\u671B ${expectedVersion}\uFF0C\u5B9E\u9645 ${actual}`);
22258
+ }
22259
+ function resolveConfiguredPluginSpec(value) {
22260
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
22261
+ throw new Error("[supertask] OpenCode \u6700\u7EC8\u914D\u7F6E\u4E0D\u662F\u5BF9\u8C61");
22262
+ }
22263
+ const plugins = value.plugin;
22264
+ if (!Array.isArray(plugins)) {
22265
+ throw new Error(`[supertask] OpenCode \u6700\u7EC8\u914D\u7F6E\u672A\u542F\u7528 ${PACKAGE_NAME}`);
22266
+ }
22267
+ const matches = plugins.flatMap((entry) => {
22268
+ if (typeof entry === "string") return [entry];
22269
+ if (Array.isArray(entry) && typeof entry[0] === "string") return [entry[0]];
22270
+ return [];
22271
+ }).filter((spec2) => spec2 === PACKAGE_NAME || spec2.startsWith(`${PACKAGE_NAME}@`));
22272
+ if (matches.length === 0) {
22273
+ throw new Error(`[supertask] OpenCode \u6700\u7EC8\u914D\u7F6E\u672A\u542F\u7528 ${PACKAGE_NAME}`);
22274
+ }
22275
+ if (matches.length !== 1) {
22276
+ throw new Error(`[supertask] OpenCode \u6700\u7EC8\u914D\u7F6E\u5305\u542B\u591A\u4E2A ${PACKAGE_NAME} \u58F0\u660E: ${matches.join(", ")}`);
22277
+ }
22278
+ const spec = matches[0];
22279
+ const version2 = spec.startsWith(`${PACKAGE_NAME}@`) ? spec.slice(PACKAGE_NAME.length + 1) : null;
22280
+ return {
22281
+ spec,
22282
+ version: version2 !== null && isSemanticVersion(version2) ? version2 : null,
22283
+ exact: version2 !== null && isSemanticVersion(version2)
22284
+ };
22285
+ }
22286
+ function getOpenCodePluginDiagnostic() {
22287
+ const result = spawnSync3(opencodeBin(), ["debug", "config", "--pure"], {
22288
+ encoding: "utf8",
22289
+ env: process.env,
22290
+ timeout: 3e4
22291
+ });
22292
+ const failed = (message) => ({
22293
+ ok: false,
22294
+ spec: "",
22295
+ version: null,
22296
+ exact: false,
22297
+ cachedVersion: null,
22298
+ packageDir: null,
22299
+ error: message
22300
+ });
22301
+ if (result.error) {
22302
+ return failed(`[supertask] \u65E0\u6CD5\u8BFB\u53D6 OpenCode \u6700\u7EC8\u914D\u7F6E: ${result.error.message}`);
22303
+ }
22304
+ if (result.status !== 0) {
22305
+ const detail = `${result.stderr ?? ""}`.trim();
22306
+ return failed(`[supertask] \u65E0\u6CD5\u8BFB\u53D6 OpenCode \u6700\u7EC8\u914D\u7F6E: ${detail || `\u9000\u51FA\u7801 ${result.status}`}`);
22307
+ }
22308
+ let config;
22309
+ try {
22310
+ config = JSON.parse(`${result.stdout ?? ""}`);
22311
+ } catch {
22312
+ return failed("[supertask] OpenCode \u6700\u7EC8\u914D\u7F6E\u4E0D\u662F\u6709\u6548 JSON");
22313
+ }
22314
+ let configured;
22315
+ try {
22316
+ configured = resolveConfiguredPluginSpec(config);
22317
+ } catch (error) {
22318
+ return failed(error instanceof Error ? error.message : String(error));
22319
+ }
22320
+ if (!configured.exact || configured.version === null) {
22321
+ return {
22322
+ ok: false,
22323
+ ...configured,
22324
+ cachedVersion: null,
22325
+ packageDir: null,
22326
+ error: `[supertask] OpenCode \u63D2\u4EF6\u5FC5\u987B\u56FA\u5B9A\u7CBE\u786E\u7248\u672C\uFF0C\u4E0D\u80FD\u4F7F\u7528 ${configured.spec}`
22327
+ };
22328
+ }
22329
+ try {
22330
+ const installed = resolveInstalledPluginVersion(configured.version);
22331
+ return {
22332
+ ok: true,
22333
+ ...configured,
22334
+ cachedVersion: installed.version,
22335
+ packageDir: installed.packageDir,
22336
+ error: null
22337
+ };
22338
+ } catch (error) {
22339
+ return {
22340
+ ok: false,
22341
+ ...configured,
22342
+ cachedVersion: null,
22343
+ packageDir: null,
22344
+ error: error instanceof Error ? error.message : String(error)
22345
+ };
22346
+ }
22347
+ }
22348
+ function installPluginVersion(version2) {
22349
+ if (!isSemanticVersion(version2)) {
22350
+ throw new Error(`[supertask] OpenCode \u63D2\u4EF6\u7248\u672C\u65E0\u6548: ${version2}`);
22351
+ }
22352
+ const result = spawnSync3(opencodeBin(), [
22353
+ "plugin",
22354
+ `${PACKAGE_NAME}@${version2}`,
22355
+ "--global",
22356
+ "--force"
22357
+ ], {
22358
+ encoding: "utf8",
22359
+ env: process.env,
22360
+ timeout: 12e4
22361
+ });
22362
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
22363
+ if (result.error) {
22364
+ throw new Error(`[supertask] OpenCode \u63D2\u4EF6\u66F4\u65B0\u5931\u8D25: ${result.error.message}`);
22365
+ }
22366
+ if (result.status !== 0) {
22367
+ throw new Error(`[supertask] OpenCode \u63D2\u4EF6\u66F4\u65B0\u5931\u8D25: ${output || `\u9000\u51FA\u7801 ${result.status}`}`);
22368
+ }
22369
+ return resolveInstalledPluginVersion(version2);
22370
+ }
22371
+ function installLatestPlugin() {
22372
+ return installPluginVersion(latestVersion());
22373
+ }
22374
+ var PACKAGE_NAME;
22375
+ var init_update2 = __esm({
22376
+ "src/daemon/update.ts"() {
22377
+ "use strict";
22378
+ init_semver();
22379
+ PACKAGE_NAME = "opencode-supertask";
22380
+ }
22381
+ });
22382
+
22383
+ // src/gateway/health.ts
22384
+ function initializeGatewayHealth(config) {
22385
+ const now = Date.now();
22386
+ state = {
22387
+ startedAt: now,
22388
+ config,
22389
+ lastActivityAt: {
22390
+ worker: now,
22391
+ scheduler: now,
22392
+ watchdog: now,
22393
+ watchdogCleanup: now
22394
+ },
22395
+ lastSuccessAt: {
22396
+ worker: now,
22397
+ scheduler: now,
22398
+ watchdog: now,
22399
+ watchdogCleanup: now
22400
+ },
22401
+ consecutiveFailures: {
22402
+ worker: 0,
22403
+ scheduler: 0,
22404
+ watchdog: 0,
22405
+ watchdogCleanup: 0
22406
+ },
22407
+ lastError: {
22408
+ worker: null,
22409
+ scheduler: null,
22410
+ watchdog: null,
22411
+ watchdogCleanup: null
22412
+ }
22413
+ };
22414
+ }
22415
+ function markGatewayActivity(component) {
22416
+ if (state) state.lastActivityAt[component] = Date.now();
22417
+ }
22418
+ function markGatewaySuccess(component) {
22419
+ if (!state) return;
22420
+ const now = Date.now();
22421
+ state.lastActivityAt[component] = now;
22422
+ state.lastSuccessAt[component] = now;
22423
+ state.consecutiveFailures[component] = 0;
22424
+ }
22425
+ function markGatewayFailure(component, error) {
22426
+ if (!state) return;
22427
+ const now = Date.now();
22428
+ state.lastActivityAt[component] = now;
22429
+ state.consecutiveFailures[component] += 1;
22430
+ state.lastError[component] = {
22431
+ at: now,
22432
+ message: error instanceof Error ? error.message : String(error)
22433
+ };
22434
+ }
22435
+ function resetGatewayHealth() {
22436
+ state = null;
22437
+ }
22438
+ function componentStatus(component, enabled, maxAgeMs, now) {
22439
+ const lastActivityAt = state?.lastActivityAt[component] ?? null;
22440
+ const ageMs = lastActivityAt == null ? null : Math.max(0, now - lastActivityAt);
22441
+ const consecutiveFailures = state?.consecutiveFailures[component] ?? 0;
22442
+ return {
22443
+ enabled,
22444
+ healthy: !enabled || ageMs != null && ageMs <= maxAgeMs && consecutiveFailures === 0,
22445
+ lastActivityAt,
22446
+ lastSuccessAt: state?.lastSuccessAt[component] ?? null,
22447
+ consecutiveFailures,
22448
+ lastError: state?.lastError[component] ?? null,
22449
+ ageMs,
22450
+ maxAgeMs
22451
+ };
22452
+ }
22453
+ function getGatewayHealth(now = Date.now()) {
22454
+ const worker = componentStatus(
22455
+ "worker",
22456
+ true,
22457
+ Math.max((state?.config.workerPollIntervalMs ?? 1e3) * 5, 5e3),
22458
+ now
22459
+ );
22460
+ const scheduler = componentStatus(
22461
+ "scheduler",
22462
+ state?.config.schedulerEnabled ?? false,
22463
+ Math.max((state?.config.schedulerCheckIntervalMs ?? 1e3) * 5, 5e3),
22464
+ now
22465
+ );
22466
+ const watchdog = componentStatus(
22467
+ "watchdog",
22468
+ true,
22469
+ Math.max((state?.config.watchdogCheckIntervalMs ?? 6e4) * 3, 5e3),
22470
+ now
22471
+ );
22472
+ const watchdogCleanup = componentStatus(
22473
+ "watchdogCleanup",
22474
+ true,
21949
22475
  Math.max((state?.config.watchdogCleanupIntervalMs ?? 864e5) * 2, 6e4),
21950
22476
  now
21951
22477
  );
@@ -21988,8 +22514,8 @@ var init_health = __esm({
21988
22514
  // src/worker/index.ts
21989
22515
  import { spawn } from "child_process";
21990
22516
  import { fileURLToPath as fileURLToPath5 } from "url";
21991
- import { existsSync as existsSync6 } from "fs";
21992
- import { dirname as dirname7, join as join5 } from "path";
22517
+ import { existsSync as existsSync7 } from "fs";
22518
+ import { dirname as dirname7, join as join6 } from "path";
21993
22519
  import { randomUUID as randomUUID2 } from "crypto";
21994
22520
  function assertWorkerProcessIsolationSupported(platform = process.platform) {
21995
22521
  if (platform === "win32") {
@@ -22007,6 +22533,8 @@ var init_worker = __esm({
22007
22533
  init_health();
22008
22534
  init_process_control();
22009
22535
  init_launch_protocol();
22536
+ init_task_working_directory();
22537
+ init_task_batch();
22010
22538
  DEFAULT_MAX_OUTPUT_CHARS = 64 * 1024;
22011
22539
  FORBIDDEN_AGENT = "supertask-runner";
22012
22540
  WorkerEngine = class {
@@ -22112,7 +22640,8 @@ var init_worker = __esm({
22112
22640
  if (!task) break;
22113
22641
  if (this.stopped) break;
22114
22642
  if (!await TaskService.start(task.id)) continue;
22115
- if (task.batchId) this.activeBatchIds.add(task.batchId);
22643
+ const batchId = normalizeTaskBatchId(task.batchId);
22644
+ if (batchId) this.activeBatchIds.add(batchId);
22116
22645
  if (this.stopped) {
22117
22646
  await TaskService.resetRunningToPending([task.id]);
22118
22647
  this.releaseBatch(task);
@@ -22143,6 +22672,14 @@ var init_worker = __esm({
22143
22672
  this.releaseBatch(task);
22144
22673
  continue;
22145
22674
  }
22675
+ try {
22676
+ validateTaskWorkingDirectory(task.cwd);
22677
+ } catch (error) {
22678
+ const message = error instanceof Error ? error.message : String(error);
22679
+ await TaskService.failRun(task.id, run.id, message, { setDeadLetter: true });
22680
+ this.releaseBatch(task);
22681
+ continue;
22682
+ }
22146
22683
  await this.spawnTask(task, run.id, launchIdentity);
22147
22684
  } catch (err) {
22148
22685
  this.releaseBatch(task);
@@ -22169,10 +22706,10 @@ var init_worker = __esm({
22169
22706
  const extension = import.meta.url.endsWith(".ts") ? "ts" : "js";
22170
22707
  const moduleDir = dirname7(fileURLToPath5(import.meta.url));
22171
22708
  const candidates = [
22172
- join5(moduleDir, `launcher.${extension}`),
22173
- join5(moduleDir, `../worker/launcher.${extension}`)
22709
+ join6(moduleDir, `launcher.${extension}`),
22710
+ join6(moduleDir, `../worker/launcher.${extension}`)
22174
22711
  ];
22175
- const entry = candidates.find((candidate) => existsSync6(candidate));
22712
+ const entry = candidates.find((candidate) => existsSync7(candidate));
22176
22713
  if (!entry) throw new Error(`Worker launcher \u4E0D\u5B58\u5728\uFF1A${candidates.join(", ")}`);
22177
22714
  return entry;
22178
22715
  }
@@ -22228,6 +22765,11 @@ var init_worker = __esm({
22228
22765
  child.on("message", (message) => {
22229
22766
  if (isMatchingDrainProof(message, entry.launchIdentity)) {
22230
22767
  entry.guardianDrained = true;
22768
+ try {
22769
+ child.send(drainProofAckForIdentity(entry.launchIdentity));
22770
+ } catch (error) {
22771
+ this.logError("drain proof acknowledgment failed", error, task.id);
22772
+ }
22231
22773
  }
22232
22774
  });
22233
22775
  let spawnError = null;
@@ -22507,7 +23049,8 @@ ${output}` : ""}` : output;
22507
23049
  );
22508
23050
  }
22509
23051
  releaseBatch(task) {
22510
- if (task.batchId) this.activeBatchIds.delete(task.batchId);
23052
+ const batchId = normalizeTaskBatchId(task.batchId);
23053
+ if (batchId) this.activeBatchIds.delete(batchId);
22511
23054
  }
22512
23055
  resolveModel(taskModel) {
22513
23056
  if (!taskModel || taskModel === "default") return null;
@@ -22828,8 +23371,11 @@ var init_watchdog = __esm({
22828
23371
  });
22829
23372
 
22830
23373
  // src/gateway/scheduler/job-templates.ts
22831
- async function cloneTaskFromTemplate(templateId) {
22832
- return createTaskFromTemplate(templateId, { advanceSchedule: true });
23374
+ async function cloneTaskFromTemplate(templateId, expectedNextRunAt) {
23375
+ return createTaskFromTemplate(templateId, {
23376
+ advanceSchedule: true,
23377
+ expectedNextRunAt
23378
+ });
22833
23379
  }
22834
23380
  async function triggerTaskFromTemplate(templateId) {
22835
23381
  return createTaskFromTemplate(templateId, {
@@ -22842,37 +23388,40 @@ function createTaskFromTemplate(templateId, options) {
22842
23388
  return db.transaction((tx) => {
22843
23389
  const tmpl = tx.select().from(taskTemplates3).where(eq(taskTemplates3.id, templateId)).limit(1).get();
22844
23390
  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();
23391
+ if (options.advanceSchedule && options.expectedNextRunAt !== void 0 && tmpl.nextRunAt !== options.expectedNextRunAt) return null;
23392
+ if (options.advanceSchedule) {
23393
+ const activeTasks = tx.select({ count: sql`count(*)` }).from(schema_exports.tasks).where(and(
23394
+ eq(schema_exports.tasks.templateId, templateId),
23395
+ sql`${schema_exports.tasks.status} IN ('pending', 'running')`
23396
+ )).get();
23397
+ const retryableTasks = tx.select({ count: sql`count(*)` }).from(schema_exports.tasks).where(and(
23398
+ eq(schema_exports.tasks.templateId, templateId),
23399
+ eq(schema_exports.tasks.status, "failed"),
23400
+ sql`${schema_exports.tasks.retryCount} <= ${schema_exports.tasks.maxRetries}`
23401
+ )).get();
23402
+ 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(
23403
+ eq(schema_exports.taskRuns.status, "running"),
23404
+ eq(schema_exports.tasks.templateId, templateId),
23405
+ sql`NOT (
23406
+ ${schema_exports.tasks.status} IN ('pending', 'running')
23407
+ OR (
23408
+ ${schema_exports.tasks.status} = 'failed'
23409
+ AND ${schema_exports.tasks.retryCount} <= ${schema_exports.tasks.maxRetries}
23410
+ )
23411
+ )`
23412
+ )).get();
23413
+ const activeCount = Number(activeTasks?.count ?? 0) + Number(retryableTasks?.count ?? 0) + Number(detachedRuns?.count ?? 0);
23414
+ if (activeCount >= (tmpl.maxInstances ?? 1)) {
23415
+ if (tmpl.scheduleType !== "delayed") {
23416
+ const nextRunAt2 = TaskTemplateService.calculateNextRunAt(
23417
+ tmpl.scheduleType,
23418
+ tmpl,
23419
+ nowMs
23420
+ );
23421
+ tx.update(taskTemplates3).set({ nextRunAt: nextRunAt2, updatedAt: nowMs }).where(and(eq(taskTemplates3.id, templateId), eq(taskTemplates3.enabled, true))).run();
23422
+ }
23423
+ return null;
22874
23424
  }
22875
- return null;
22876
23425
  }
22877
23426
  const isDelayed = tmpl.scheduleType === "delayed";
22878
23427
  const nextRunAt = isDelayed ? null : TaskTemplateService.calculateNextRunAt(
@@ -22889,7 +23438,7 @@ function createTaskFromTemplate(templateId, options) {
22889
23438
  category: tmpl.category ?? "general",
22890
23439
  importance: tmpl.importance ?? 3,
22891
23440
  urgency: tmpl.urgency ?? 3,
22892
- batchId: tmpl.batchId,
23441
+ batchId: normalizeTaskBatchId(tmpl.batchId),
22893
23442
  maxRetries: tmpl.maxRetries ?? 3,
22894
23443
  retryBackoffMs: tmpl.retryBackoffMs ?? 3e4,
22895
23444
  timeoutMs: tmpl.timeoutMs,
@@ -22950,6 +23499,7 @@ var init_job_templates = __esm({
22950
23499
  init_db2();
22951
23500
  init_drizzle_orm();
22952
23501
  init_task_template_service();
23502
+ init_task_batch();
22953
23503
  ({ taskTemplates: taskTemplates3 } = schema_exports);
22954
23504
  DUE_TEMPLATE_BATCH_SIZE = 100;
22955
23505
  }
@@ -22999,7 +23549,7 @@ var init_scheduler = __esm({
22999
23549
  const lastTemplate = dueTemplates.at(-1);
23000
23550
  for (const tmpl of dueTemplates) {
23001
23551
  try {
23002
- const task = await cloneTaskFromTemplate(tmpl.id);
23552
+ const task = await cloneTaskFromTemplate(tmpl.id, tmpl.nextRunAt);
23003
23553
  if (task) {
23004
23554
  console.log(JSON.stringify({
23005
23555
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -25287,6 +25837,14 @@ function statusText(locale, status) {
25287
25837
  const key = `status.${status}`;
25288
25838
  return key in ZH ? t(locale, key) : t(locale, "status.unknown");
25289
25839
  }
25840
+ function runStatusText(locale, status) {
25841
+ const key = `runStatus.${status}`;
25842
+ return key in ZH ? t(locale, key) : t(locale, "status.unknown");
25843
+ }
25844
+ function resolveEditedRunAt(originalEpoch, originalLocal, currentLocal) {
25845
+ if (originalEpoch !== null && originalLocal === currentLocal) return originalEpoch;
25846
+ return new Date(currentLocal).getTime();
25847
+ }
25290
25848
  function formatRelative(timestamp2, locale) {
25291
25849
  if (!timestamp2) return "\u2014";
25292
25850
  const deltaMs = timestamp2 - Date.now();
@@ -25363,13 +25921,31 @@ function clientMessages(locale) {
25363
25921
  "dialog.clearTitle",
25364
25922
  "dialog.clearBody",
25365
25923
  "dialog.clearInstruction",
25924
+ "dialog.restartGateway",
25925
+ "dialog.restartGatewayBody",
25926
+ "dialog.restartGatewayRunningBody",
25366
25927
  "feedback.retryFailed",
25367
25928
  "feedback.cancelFailed",
25368
25929
  "feedback.deleteFailed",
25369
25930
  "feedback.requestFailed",
25370
25931
  "feedback.triggered",
25932
+ "feedback.taskCreated",
25933
+ "feedback.taskUpdated",
25934
+ "task.projectExisting",
25935
+ "task.projectNew",
25936
+ "task.createTitle",
25937
+ "task.editTitle",
25938
+ "action.saveTask",
25939
+ "action.updateTask",
25371
25940
  "feedback.configSaved",
25372
25941
  "feedback.databaseCleared",
25942
+ "feedback.templateCreated",
25943
+ "feedback.templateUpdated",
25944
+ "feedback.sessionCommandCopied",
25945
+ "feedback.restarting",
25946
+ "feedback.restartTimeout",
25947
+ "template.createTitle",
25948
+ "template.editTitle",
25373
25949
  "filter.noResults"
25374
25950
  ];
25375
25951
  return Object.fromEntries(keys.map((key) => [key, t(locale, key)]));
@@ -25463,6 +26039,7 @@ function renderLayout(options) {
25463
26039
  themeMedia.addEventListener?.('change',()=>{if((localStorage.getItem('supertask-theme')||'system')==='system')applyTheme('system');});
25464
26040
  applyTheme(localStorage.getItem('supertask-theme')||'system');
25465
26041
  requestAnimationFrame(()=>document.documentElement.classList.add('ui-ready'));
26042
+ ${resolveEditedRunAt.toString()}
25466
26043
  function refreshPage(button){button.classList.add('refreshing');button.setAttribute('aria-label','${t(locale, "a11y.refreshing")}');location.reload();}
25467
26044
  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
26045
  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 +26051,22 @@ function renderLayout(options) {
25474
26051
  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
26052
  const showDetail=id=>showRecord('/api/tasks/'+id);const showRunDetail=id=>showRecord('/api/runs/'+id);const showTemplateDetail=id=>showRecord('/api/templates/'+id);
25476
26053
  async function copyDetails(){try{await navigator.clipboard.writeText(document.getElementById('detail-content').textContent);showToast(text('details.copySuccess'))}catch{showToast(text('feedback.copyFailed'),'error')}}
26054
+ 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')}}
26055
+ function taskField(name){return document.getElementById('task-'+name)}
26056
+ function taskProjects(){const node=document.getElementById('task-project-data');if(!node)return {};try{return JSON.parse(node.textContent||'{}')}catch{return {}}}
26057
+ 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')}
26058
+ 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)}
26059
+ 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')}}
26060
+ 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}}
26061
+ function templateField(name){return document.getElementById('template-'+name)}
26062
+ 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'}
26063
+ function localDateTime(milliseconds){const date=new Date(milliseconds);const local=new Date(milliseconds-date.getTimezoneOffset()*60000);return local.toISOString().slice(0,23)}
26064
+ 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}}
26065
+ 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}
26066
+ function selectedRunAt(){const input=templateField('run-at');return resolveEditedRunAt(input.dataset.originalEpoch?Number(input.dataset.originalEpoch):null,input.dataset.originalLocal||'',input.value)}
26067
+ 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)}
26068
+ 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')}}
26069
+ 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
26070
  async function enableTmpl(id){try{await readJson(await fetch('/api/templates/'+id+'/enable',{method:'POST'}));location.reload()}catch(error){showToast(error.message,'error')}}
25478
26071
  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
26072
  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 +26074,9 @@ function renderLayout(options) {
25481
26074
  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
26075
  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
26076
  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')}}
26077
+ async function confirmGatewayRestart(runningCount=0){const body=runningCount>0?text('dialog.restartGatewayRunningBody',{count:runningCount}):text('dialog.restartGatewayBody');return await ask(text('dialog.restartGateway'),body)}
26078
+ 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}}
26079
+ 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
26080
  </script>
25486
26081
  </body>
25487
26082
  </html>`;
@@ -25493,17 +26088,17 @@ var init_ui = __esm({
25493
26088
  ZH = {
25494
26089
  "app.name": "SuperTask",
25495
26090
  "app.dashboard": "\u63A7\u5236\u53F0",
25496
- "app.tagline": "\u53EF\u9760\u7684\u672C\u5730 Agent \u8C03\u5EA6\u4E2D\u5FC3",
26091
+ "app.tagline": "\u53EF\u9760\u7684\u672C\u5730 Agent \u4EFB\u52A1\u4E2D\u5FC3",
25497
26092
  "app.local": "\u672C\u5730\u8FD0\u884C",
25498
26093
  "app.footer": "Local-first \xB7 \u6570\u636E\u7559\u5728\u4F60\u7684\u8BBE\u5907\u4E0A",
25499
26094
  "nav.tasks": "\u4EFB\u52A1",
25500
- "nav.templates": "\u8C03\u5EA6",
26095
+ "nav.templates": "\u5B9A\u65F6\u4EFB\u52A1",
25501
26096
  "nav.runs": "\u6267\u884C\u8BB0\u5F55",
25502
26097
  "nav.system": "\u7CFB\u7EDF",
25503
26098
  "page.tasks.title": "\u4EFB\u52A1\u961F\u5217",
25504
26099
  "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",
26100
+ "page.templates.title": "\u5B9A\u65F6\u4EFB\u52A1",
26101
+ "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
26102
  "page.runs.title": "\u6267\u884C\u8BB0\u5F55",
25508
26103
  "page.runs.description": "\u8FFD\u8E2A\u6BCF\u6B21 Agent \u6267\u884C\u7684\u72B6\u6001\u3001\u8017\u65F6\u3001\u5FC3\u8DF3\u4E0E\u8F93\u51FA\u3002",
25509
26104
  "page.system.title": "\u7CFB\u7EDF\u8BBE\u7F6E",
@@ -25513,29 +26108,43 @@ var init_ui = __esm({
25513
26108
  "action.retry": "\u91CD\u8BD5",
25514
26109
  "action.cancel": "\u53D6\u6D88",
25515
26110
  "action.delete": "\u5220\u9664",
26111
+ "action.edit": "\u7F16\u8F91",
25516
26112
  "action.enable": "\u542F\u7528",
25517
26113
  "action.disable": "\u7981\u7528",
25518
- "action.trigger": "\u7ACB\u5373\u89E6\u53D1",
26114
+ "action.trigger": "\u7ACB\u5373\u8FD0\u884C\u4E00\u6B21",
26115
+ "action.continueSession": "\u7EE7\u7EED\u4F1A\u8BDD",
25519
26116
  "action.logs": "\u67E5\u770B\u65E5\u5FD7",
25520
26117
  "action.hideLogs": "\u6536\u8D77\u65E5\u5FD7",
25521
26118
  "action.save": "\u4FDD\u5B58\u8BBE\u7F6E",
26119
+ "action.saveAndRestart": "\u4FDD\u5B58\u5E76\u91CD\u542F",
25522
26120
  "action.copy": "\u590D\u5236 JSON",
25523
26121
  "action.close": "\u5173\u95ED",
25524
26122
  "action.confirm": "\u786E\u8BA4",
25525
26123
  "action.clearDatabase": "\u6E05\u7A7A\u6570\u636E\u5E93",
26124
+ "action.createTask": "\u65B0\u5EFA\u4EFB\u52A1",
26125
+ "action.saveTask": "\u52A0\u5165\u961F\u5217",
26126
+ "action.updateTask": "\u4FDD\u5B58\u4FEE\u6539",
26127
+ "action.createTemplate": "\u65B0\u5EFA\u5B9A\u65F6\u4EFB\u52A1",
26128
+ "action.saveTemplate": "\u4FDD\u5B58\u5B9A\u65F6\u4EFB\u52A1",
25526
26129
  "status.pending": "\u5F85\u6267\u884C",
25527
26130
  "status.running": "\u8FD0\u884C\u4E2D",
25528
26131
  "status.done": "\u5DF2\u5B8C\u6210",
25529
- "status.failed": "\u5931\u8D25",
25530
- "status.dead_letter": "\u6B7B\u4FE1",
26132
+ "status.failed": "\u7B49\u5F85\u91CD\u8BD5",
26133
+ "status.dead_letter": "\u5DF2\u505C\u6B62",
26134
+ "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",
26135
+ "status.deadLetterAction": "\u9700\u68C0\u67E5\u539F\u56E0\u540E\u624B\u52A8\u91CD\u8BD5",
25531
26136
  "status.cancelled": "\u5DF2\u53D6\u6D88",
26137
+ "status.executionStillActive": "\u6267\u884C\u8FDB\u7A0B\u4ECD\u5728\u9000\u51FA\uFF0C\u6682\u65F6\u5360\u7528\u5E76\u53D1",
25532
26138
  "status.unknown": "\u672A\u77E5",
26139
+ "runStatus.running": "\u8FD0\u884C\u4E2D",
26140
+ "runStatus.done": "\u6210\u529F",
26141
+ "runStatus.failed": "\u5931\u8D25",
25533
26142
  "stats.total": "\u603B\u4EFB\u52A1",
25534
26143
  "stats.pending": "\u5F85\u6267\u884C",
25535
26144
  "stats.running": "\u8FD0\u884C\u4E2D",
25536
26145
  "stats.done": "\u5DF2\u5B8C\u6210",
25537
- "stats.failedDead": "\u5931\u8D25\u4E0E\u6B7B\u4FE1",
25538
- "stats.templates": "\u6A21\u677F\u603B\u6570",
26146
+ "stats.failedDead": "\u7B49\u5F85\u91CD\u8BD5\u4E0E\u5DF2\u505C\u6B62",
26147
+ "stats.templates": "\u5B9A\u65F6\u4EFB\u52A1\u603B\u6570",
25539
26148
  "stats.enabled": "\u5DF2\u542F\u7528",
25540
26149
  "stats.disabled": "\u5DF2\u7981\u7528",
25541
26150
  "stats.records": "\u6267\u884C\u603B\u6570",
@@ -25543,7 +26152,7 @@ var init_ui = __esm({
25543
26152
  "stats.pageFailed": "\u672C\u9875\u5931\u8D25",
25544
26153
  "stats.pageRunning": "\u672C\u9875\u8FD0\u884C\u4E2D",
25545
26154
  "filter.all": "\u5168\u90E8",
25546
- "filter.searchTasks": "\u641C\u7D22\u5F53\u524D\u9875\u7684\u4EFB\u52A1\u3001Agent \u6216\u63D0\u793A\u8BCD",
26155
+ "filter.searchTasks": "\u641C\u7D22\u5F53\u524D\u9875\u7684\u4EFB\u52A1\u3001\u9879\u76EE\u3001\u6279\u6B21\u3001Agent \u6216\u63D0\u793A\u8BCD",
25547
26156
  "filter.noResults": "\u6CA1\u6709\u7B26\u5408\u5F53\u524D\u641C\u7D22\u6761\u4EF6\u7684\u4EFB\u52A1",
25548
26157
  "table.id": "ID",
25549
26158
  "table.task": "\u4EFB\u52A1",
@@ -25559,7 +26168,7 @@ var init_ui = __esm({
25559
26168
  "table.nextRun": "\u4E0B\u6B21\u6267\u884C",
25560
26169
  "table.run": "Run",
25561
26170
  "table.heartbeat": "\u5FC3\u8DF3",
25562
- "table.session": "Session",
26171
+ "table.session": "\u4F1A\u8BDD",
25563
26172
  "table.model": "\u6A21\u578B",
25564
26173
  "table.startedAt": "\u542F\u52A8\u65F6\u95F4",
25565
26174
  "table.pid": "PID",
@@ -25567,14 +26176,14 @@ var init_ui = __esm({
25567
26176
  "pagination.next": "\u4E0B\u4E00\u9875",
25568
26177
  "pagination.summary": "\u7B2C {page} \u9875\uFF0C\u5171 {pages} \u9875 \xB7 {total} \u6761",
25569
26178
  "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",
26179
+ "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",
26180
+ "empty.templates": "\u8FD8\u6CA1\u6709\u5B9A\u65F6\u4EFB\u52A1",
26181
+ "empty.templatesHint": "\u70B9\u51FB\u53F3\u4E0A\u89D2\u201C\u65B0\u5EFA\u5B9A\u65F6\u4EFB\u52A1\u201D\u5F00\u59CB\u521B\u5EFA\u3002",
25573
26182
  "empty.runs": "\u8FD8\u6CA1\u6709\u6267\u884C\u8BB0\u5F55",
25574
26183
  "empty.running": "\u5F53\u524D\u6CA1\u6709\u8FD0\u884C\u4E2D\u7684\u4EFB\u52A1",
25575
26184
  "schedule.cron": "Cron",
25576
- "schedule.recurring": "\u5FAA\u73AF",
25577
- "schedule.delayed": "\u5EF6\u8FDF",
26185
+ "schedule.recurring": "\u56FA\u5B9A\u95F4\u9694",
26186
+ "schedule.delayed": "\u4E00\u6B21\u6027",
25578
26187
  "schedule.unknown": "\u672A\u77E5",
25579
26188
  "schedule.enabled": "\u5DF2\u542F\u7528",
25580
26189
  "schedule.disabled": "\u5DF2\u7981\u7528",
@@ -25583,14 +26192,14 @@ var init_ui = __esm({
25583
26192
  "schedule.hours": "{count} \u5C0F\u65F6",
25584
26193
  "schedule.days": "{count} \u5929",
25585
26194
  "schedule.overdue": "\u5DF2\u5230\u671F",
25586
- "system.worker": "Worker",
25587
- "system.scheduler": "Scheduler",
25588
- "system.watchdog": "Watchdog",
26195
+ "system.worker": "\u4EFB\u52A1\u6267\u884C",
26196
+ "system.scheduler": "\u5B9A\u65F6\u4EFB\u52A1\u670D\u52A1",
26197
+ "system.watchdog": "\u8FD0\u884C\u76D1\u63A7",
25589
26198
  "system.maxConcurrency": "\u6700\u5927\u5E76\u53D1",
25590
26199
  "system.pollInterval": "\u8F6E\u8BE2\u95F4\u9694",
25591
26200
  "system.heartbeatInterval": "\u5FC3\u8DF3\u95F4\u9694",
25592
26201
  "system.taskTimeout": "\u4EFB\u52A1\u8D85\u65F6",
25593
- "system.schedulerEnabled": "\u542F\u7528\u8C03\u5EA6",
26202
+ "system.schedulerEnabled": "\u542F\u7528\u5B9A\u65F6\u4EFB\u52A1",
25594
26203
  "system.checkInterval": "\u68C0\u67E5\u95F4\u9694",
25595
26204
  "system.heartbeatTimeout": "\u5FC3\u8DF3\u8D85\u65F6",
25596
26205
  "system.cleanupInterval": "\u6E05\u7406\u95F4\u9694",
@@ -25600,8 +26209,12 @@ var init_ui = __esm({
25600
26209
  "system.minutes": "\u5206\u949F",
25601
26210
  "system.hours": "\u5C0F\u65F6",
25602
26211
  "system.days": "\u5929",
25603
- "system.activeTemplates": "\u6D3B\u8DC3\u6A21\u677F",
25604
- "system.saveHint": "\u4FDD\u5B58\u540E\u9700\u91CD\u542F Gateway \u751F\u6548",
26212
+ "system.activeTemplates": "\u5DF2\u542F\u7528\u5B9A\u65F6\u4EFB\u52A1",
26213
+ "system.saveHint": "\u8BBE\u7F6E\u4FDD\u5B58\u540E\u9700\u8981\u91CD\u542F Gateway \u624D\u80FD\u751F\u6548\u3002",
26214
+ "system.configApplied": "\u9875\u9762\u4E2D\u7684\u8BBE\u7F6E\u6B63\u5728\u751F\u6548\u3002",
26215
+ "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",
26216
+ "system.configForeground": "\u6B64\u9875\u9762\u672A\u8FDE\u63A5\u5230 Gateway \u7684\u8FD0\u884C\u914D\u7F6E\uFF1B\u4FDD\u5B58\u540E\u8BF7\u91CD\u542F Gateway\u3002",
26217
+ "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
26218
  "system.runningTasks": "\u5F53\u524D\u8FD0\u884C\u4EFB\u52A1\uFF08{running} / {limit} \u5E76\u53D1\uFF09",
25606
26219
  "system.taskStats": "\u4EFB\u52A1\u6982\u89C8",
25607
26220
  "system.configFile": "\u914D\u7F6E\u6587\u4EF6",
@@ -25610,7 +26223,46 @@ var init_ui = __esm({
25610
26223
  "system.yes": "\u662F",
25611
26224
  "system.noDefault": "\u5426\uFF0C\u5F53\u524D\u4F7F\u7528\u9ED8\u8BA4\u503C",
25612
26225
  "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",
26226
+ "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",
26227
+ "template.createTitle": "\u65B0\u5EFA\u5B9A\u65F6\u4EFB\u52A1",
26228
+ "template.editTitle": "\u7F16\u8F91\u5B9A\u65F6\u4EFB\u52A1",
26229
+ "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",
26230
+ "template.name": "\u540D\u79F0",
26231
+ "template.cwd": "\u9879\u76EE\u76EE\u5F55",
26232
+ "template.cwdHint": "OpenCode \u4F1A\u5728\u8FD9\u4E2A\u76EE\u5F55\u4E2D\u6267\u884C\u4EFB\u52A1\u3002",
26233
+ "template.agent": "Agent",
26234
+ "template.model": "\u6A21\u578B",
26235
+ "template.prompt": "\u63D0\u793A\u8BCD",
26236
+ "template.scheduleType": "\u6267\u884C\u65B9\u5F0F",
26237
+ "template.cronExpr": "Cron \u8868\u8FBE\u5F0F",
26238
+ "template.cronHint": "\u4F8B\u5982\uFF1A0 9 * * *\uFF08\u6BCF\u5929 09:00\uFF09",
26239
+ "template.interval": "\u6267\u884C\u95F4\u9694",
26240
+ "template.runAt": "\u6267\u884C\u65F6\u95F4",
26241
+ "template.durationHint": "\u652F\u6301 30s\u30015min\u30011h\u30012d",
26242
+ "template.advanced": "\u66F4\u591A\u6267\u884C\u8BBE\u7F6E",
26243
+ "template.category": "\u5206\u7C7B",
26244
+ "template.batchId": "\u6279\u6B21 ID",
26245
+ "template.importance": "\u91CD\u8981\u7A0B\u5EA6\uFF081-5\uFF09",
26246
+ "template.urgency": "\u7D27\u6025\u7A0B\u5EA6\uFF081-5\uFF09",
26247
+ "template.maxInstances": "\u81EA\u52A8\u8C03\u5EA6\u4E0A\u9650",
26248
+ "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",
26249
+ "template.maxRetries": "\u5931\u8D25\u91CD\u8BD5\u6B21\u6570",
26250
+ "template.retryBackoff": "\u91CD\u8BD5\u7B49\u5F85",
26251
+ "template.timeout": "\u5355\u6B21\u8D85\u65F6",
26252
+ "template.optional": "\u53EF\u9009",
26253
+ "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",
26254
+ "projects.title": "\u9879\u76EE\u5206\u7EC4",
26255
+ "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",
26256
+ "projects.all": "\u5168\u90E8\u9879\u76EE",
26257
+ "projects.legacy": "\u672A\u5206\u7EC4",
26258
+ "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",
26259
+ "projects.counts": "\u8FD0\u884C {running} \xB7 \u6392\u961F {pending} \xB7 \u5F02\u5E38 {failed}",
26260
+ "task.createTitle": "\u65B0\u5EFA\u4EFB\u52A1",
26261
+ "task.editTitle": "\u7F16\u8F91\u4EFB\u52A1",
26262
+ "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",
26263
+ "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",
26264
+ "task.projectExisting": "\u6B64\u9879\u76EE\u73B0\u6709 {total} \u4E2A\u4EFB\u52A1\uFF1A\u8FD0\u884C {running}\uFF0C\u6392\u961F {pending}\uFF0C\u5F02\u5E38 {failed}\u3002",
26265
+ "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
26266
  "theme.label": "\u4E3B\u9898",
25615
26267
  "theme.system": "\u8DDF\u968F\u7CFB\u7EDF",
25616
26268
  "theme.light": "\u6D45\u8272",
@@ -25625,21 +26277,31 @@ var init_ui = __esm({
25625
26277
  "dialog.retryTaskBody": "\u4EFB\u52A1\u5C06\u56DE\u5230\u5F85\u6267\u884C\u72B6\u6001\uFF0C\u5E76\u91CD\u7F6E\u81EA\u52A8\u91CD\u8BD5\u9884\u7B97\u3002",
25626
26278
  "dialog.deleteTask": "\u5220\u9664\u4EFB\u52A1 #{id}\uFF1F",
25627
26279
  "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",
26280
+ "dialog.disableTemplate": "\u7981\u7528\u8FD9\u4E2A\u5B9A\u65F6\u4EFB\u52A1\uFF1F",
26281
+ "dialog.disableTemplateBody": "\u5B83\u5C06\u505C\u6B62\u81EA\u52A8\u521B\u5EFA\u65B0\u4EFB\u52A1\uFF0C\u5DF2\u6709\u4EFB\u52A1\u4E0D\u53D7\u5F71\u54CD\u3002",
26282
+ "dialog.deleteTemplate": "\u5220\u9664\u8FD9\u4E2A\u5B9A\u65F6\u4EFB\u52A1\uFF1F",
26283
+ "dialog.deleteTemplateBody": "\u5B9A\u65F6\u4EFB\u52A1\u914D\u7F6E\u5C06\u6C38\u4E45\u5220\u9664\uFF0C\u6B64\u64CD\u4F5C\u65E0\u6CD5\u64A4\u9500\u3002",
26284
+ "dialog.triggerTemplate": "\u7ACB\u5373\u8FD0\u884C\u4E00\u6B21\uFF1F",
26285
+ "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
26286
  "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",
26287
+ "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
26288
  "dialog.clearInstruction": "\u8F93\u5165 CLEAR \u4EE5\u786E\u8BA4",
26289
+ "dialog.restartGateway": "\u91CD\u542F\u5E76\u5E94\u7528\u8BBE\u7F6E\uFF1F",
26290
+ "dialog.restartGatewayBody": "Gateway \u4F1A\u77ED\u6682\u79BB\u7EBF\uFF0C\u901A\u5E38\u6570\u79D2\u540E\u7531 PM2 \u81EA\u52A8\u6062\u590D\u3002",
26291
+ "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
26292
  "feedback.retryFailed": "\u91CD\u8BD5\u5931\u8D25",
25638
26293
  "feedback.cancelFailed": "\u53D6\u6D88\u5931\u8D25",
25639
26294
  "feedback.deleteFailed": "\u5220\u9664\u5931\u8D25",
25640
26295
  "feedback.requestFailed": "\u8BF7\u6C42\u5931\u8D25",
25641
26296
  "feedback.triggered": "\u5DF2\u521B\u5EFA\u4EFB\u52A1 #{id}",
25642
- "feedback.configSaved": "\u8BBE\u7F6E\u5DF2\u4FDD\u5B58\uFF0C\u91CD\u542F Gateway \u540E\u751F\u6548",
26297
+ "feedback.taskCreated": "\u4EFB\u52A1 #{id} \u5DF2\u52A0\u5165\u961F\u5217",
26298
+ "feedback.taskUpdated": "\u4EFB\u52A1 #{id} \u5DF2\u66F4\u65B0",
26299
+ "feedback.templateCreated": "\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u521B\u5EFA",
26300
+ "feedback.templateUpdated": "\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u66F4\u65B0",
26301
+ "feedback.configSaved": "\u8BBE\u7F6E\u5DF2\u4FDD\u5B58",
26302
+ "feedback.sessionCommandCopied": "\u4F1A\u8BDD\u547D\u4EE4\u5DF2\u590D\u5236\uFF0C\u53EF\u76F4\u63A5\u7C98\u8D34\u5230\u7EC8\u7AEF\u8FD0\u884C",
26303
+ "feedback.restarting": "Gateway \u6B63\u5728\u91CD\u542F\uFF0C\u8BF7\u7A0D\u5019\u2026",
26304
+ "feedback.restartTimeout": "Gateway \u5C1A\u672A\u6062\u590D\uFF0C\u8BF7\u7A0D\u540E\u5237\u65B0\u6216\u68C0\u67E5 PM2 \u72B6\u6001",
25643
26305
  "feedback.databaseCleared": "\u6570\u636E\u5E93\u5DF2\u6E05\u7A7A\uFF0C\u5907\u4EFD\u4F4D\u4E8E\uFF1A{path}",
25644
26306
  "feedback.copyFailed": "\u590D\u5236\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u9009\u62E9\u5185\u5BB9",
25645
26307
  "a11y.skip": "\u8DF3\u5230\u4E3B\u8981\u5185\u5BB9",
@@ -25652,13 +26314,13 @@ var init_ui = __esm({
25652
26314
  "app.local": "Running locally",
25653
26315
  "app.footer": "Local-first \xB7 Your data stays on this device",
25654
26316
  "nav.tasks": "Tasks",
25655
- "nav.templates": "Schedules",
26317
+ "nav.templates": "Scheduled tasks",
25656
26318
  "nav.runs": "Runs",
25657
26319
  "nav.system": "System",
25658
26320
  "page.tasks.title": "Task queue",
25659
26321
  "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.",
26322
+ "page.templates.title": "Scheduled tasks",
26323
+ "page.templates.description": "Create and edit cron, fixed-interval, and one-time tasks, including their model, agent, and prompt.",
25662
26324
  "page.runs.title": "Execution history",
25663
26325
  "page.runs.description": "Inspect the status, duration, heartbeat, and output of every agent run.",
25664
26326
  "page.system.title": "System settings",
@@ -25668,29 +26330,43 @@ var init_ui = __esm({
25668
26330
  "action.retry": "Retry",
25669
26331
  "action.cancel": "Cancel",
25670
26332
  "action.delete": "Delete",
26333
+ "action.edit": "Edit",
25671
26334
  "action.enable": "Enable",
25672
26335
  "action.disable": "Disable",
25673
26336
  "action.trigger": "Run now",
26337
+ "action.continueSession": "Continue session",
25674
26338
  "action.logs": "View log",
25675
26339
  "action.hideLogs": "Hide log",
25676
26340
  "action.save": "Save settings",
26341
+ "action.saveAndRestart": "Save and restart",
25677
26342
  "action.copy": "Copy JSON",
25678
26343
  "action.close": "Close",
25679
26344
  "action.confirm": "Confirm",
25680
26345
  "action.clearDatabase": "Clear database",
26346
+ "action.createTask": "New task",
26347
+ "action.saveTask": "Add to queue",
26348
+ "action.updateTask": "Save changes",
26349
+ "action.createTemplate": "New scheduled task",
26350
+ "action.saveTemplate": "Save scheduled task",
25681
26351
  "status.pending": "Pending",
25682
26352
  "status.running": "Running",
25683
26353
  "status.done": "Done",
25684
- "status.failed": "Failed",
25685
- "status.dead_letter": "Dead letter",
26354
+ "status.failed": "Waiting to retry",
26355
+ "status.dead_letter": "Stopped",
26356
+ "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.",
26357
+ "status.deadLetterAction": "Inspect the error, then retry manually",
25686
26358
  "status.cancelled": "Cancelled",
26359
+ "status.executionStillActive": "The execution process is still stopping and temporarily occupies a slot",
25687
26360
  "status.unknown": "Unknown",
26361
+ "runStatus.running": "Running",
26362
+ "runStatus.done": "Succeeded",
26363
+ "runStatus.failed": "Failed",
25688
26364
  "stats.total": "Total tasks",
25689
26365
  "stats.pending": "Pending",
25690
26366
  "stats.running": "Running",
25691
26367
  "stats.done": "Completed",
25692
- "stats.failedDead": "Failed & dead",
25693
- "stats.templates": "Templates",
26368
+ "stats.failedDead": "Waiting to retry & stopped",
26369
+ "stats.templates": "Scheduled tasks",
25694
26370
  "stats.enabled": "Enabled",
25695
26371
  "stats.disabled": "Disabled",
25696
26372
  "stats.records": "Total runs",
@@ -25698,7 +26374,7 @@ var init_ui = __esm({
25698
26374
  "stats.pageFailed": "Failed here",
25699
26375
  "stats.pageRunning": "Running here",
25700
26376
  "filter.all": "All",
25701
- "filter.searchTasks": "Search tasks, agents, or prompts on this page",
26377
+ "filter.searchTasks": "Search tasks, projects, batches, agents, or prompts on this page",
25702
26378
  "filter.noResults": "No tasks match this search",
25703
26379
  "table.id": "ID",
25704
26380
  "table.task": "Task",
@@ -25722,14 +26398,14 @@ var init_ui = __esm({
25722
26398
  "pagination.next": "Next",
25723
26399
  "pagination.summary": "Page {page} of {pages} \xB7 {total} items",
25724
26400
  "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",
26401
+ "empty.tasksHint": "Select \u201CNew task\u201D, or use the OpenCode plugin or supertask add.",
26402
+ "empty.templates": "No scheduled tasks yet",
26403
+ "empty.templatesHint": "Select \u201CNew scheduled task\u201D to create one.",
25728
26404
  "empty.runs": "No execution history yet",
25729
26405
  "empty.running": "No tasks are running right now",
25730
26406
  "schedule.cron": "Cron",
25731
- "schedule.recurring": "Recurring",
25732
- "schedule.delayed": "Delayed",
26407
+ "schedule.recurring": "Fixed interval",
26408
+ "schedule.delayed": "One-time",
25733
26409
  "schedule.unknown": "Unknown",
25734
26410
  "schedule.enabled": "Enabled",
25735
26411
  "schedule.disabled": "Disabled",
@@ -25738,14 +26414,14 @@ var init_ui = __esm({
25738
26414
  "schedule.hours": "{count} hr",
25739
26415
  "schedule.days": "{count} days",
25740
26416
  "schedule.overdue": "Overdue",
25741
- "system.worker": "Worker",
25742
- "system.scheduler": "Scheduler",
25743
- "system.watchdog": "Watchdog",
26417
+ "system.worker": "Task execution",
26418
+ "system.scheduler": "Scheduled task service",
26419
+ "system.watchdog": "Runtime monitor",
25744
26420
  "system.maxConcurrency": "Max concurrency",
25745
26421
  "system.pollInterval": "Poll interval",
25746
26422
  "system.heartbeatInterval": "Heartbeat interval",
25747
26423
  "system.taskTimeout": "Task timeout",
25748
- "system.schedulerEnabled": "Enable scheduler",
26424
+ "system.schedulerEnabled": "Enable scheduled tasks",
25749
26425
  "system.checkInterval": "Check interval",
25750
26426
  "system.heartbeatTimeout": "Heartbeat timeout",
25751
26427
  "system.cleanupInterval": "Cleanup interval",
@@ -25755,8 +26431,12 @@ var init_ui = __esm({
25755
26431
  "system.minutes": "minutes",
25756
26432
  "system.hours": "hours",
25757
26433
  "system.days": "days",
25758
- "system.activeTemplates": "Active templates",
25759
- "system.saveHint": "Restart Gateway to apply saved settings",
26434
+ "system.activeTemplates": "Enabled scheduled tasks",
26435
+ "system.saveHint": "Restart Gateway to apply saved settings.",
26436
+ "system.configApplied": "The settings shown here are active.",
26437
+ "system.configPending": "Settings are saved, but this Gateway is still using the values from its last start.",
26438
+ "system.configForeground": "This page is not connected to Gateway runtime settings. Restart Gateway after saving.",
26439
+ "system.configRestartManually": "Settings are saved but not active. Return to the terminal that launched Gateway, press Ctrl-C, then run supertask gateway again.",
25760
26440
  "system.runningTasks": "Active tasks ({running} / {limit} concurrent)",
25761
26441
  "system.taskStats": "Task overview",
25762
26442
  "system.configFile": "Configuration file",
@@ -25766,6 +26446,45 @@ var init_ui = __esm({
25766
26446
  "system.noDefault": "No, using defaults",
25767
26447
  "system.danger": "Danger zone",
25768
26448
  "system.dangerDescription": "A verified backup is created first, then tasks, runs, and templates are cleared transactionally. Active work blocks this operation.",
26449
+ "template.createTitle": "New scheduled task",
26450
+ "template.editTitle": "Edit scheduled task",
26451
+ "template.formSubtitle": "Choose when to run and which project, model, and prompt OpenCode should use.",
26452
+ "template.name": "Name",
26453
+ "template.cwd": "Project directory",
26454
+ "template.cwdHint": "OpenCode runs the task in this directory.",
26455
+ "template.agent": "Agent",
26456
+ "template.model": "Model",
26457
+ "template.prompt": "Prompt",
26458
+ "template.scheduleType": "Schedule",
26459
+ "template.cronExpr": "Cron expression",
26460
+ "template.cronHint": "Example: 0 9 * * * (daily at 09:00)",
26461
+ "template.interval": "Interval",
26462
+ "template.runAt": "Run at",
26463
+ "template.durationHint": "Supports 30s, 5min, 1h, 2d",
26464
+ "template.advanced": "More execution settings",
26465
+ "template.category": "Category",
26466
+ "template.batchId": "Batch ID",
26467
+ "template.importance": "Importance (1-5)",
26468
+ "template.urgency": "Urgency (1-5)",
26469
+ "template.maxInstances": "Automatic scheduling limit",
26470
+ "template.maxInstancesHint": "Limits automatic scheduling only. \u201CRun now\u201D always queues a task. Active instances include pending, running, and retry-waiting tasks.",
26471
+ "template.maxRetries": "Failure retries",
26472
+ "template.retryBackoff": "Retry delay",
26473
+ "template.timeout": "Run timeout",
26474
+ "template.optional": "Optional",
26475
+ "template.futureOnly": "Saved settings apply to tasks created in the future. Editing does not change queued or running tasks.",
26476
+ "projects.title": "Projects",
26477
+ "projects.description": "The project directory is the isolation key. Check active and queued work before adding more.",
26478
+ "projects.all": "All projects",
26479
+ "projects.legacy": "Ungrouped",
26480
+ "projects.legacyHint": "Legacy tasks without a project directory. View, cancel, or delete them; recreate under the correct project to make changes.",
26481
+ "projects.counts": "Running {running} \xB7 queued {pending} \xB7 issues {failed}",
26482
+ "task.createTitle": "New task",
26483
+ "task.editTitle": "Edit task",
26484
+ "task.formSubtitle": "The task enters the durable queue immediately and waits automatically when concurrency is full.",
26485
+ "task.batchHint": "Tasks with the same non-empty batch ID run serially. Blank removes the batch constraint; global concurrency and dependencies still apply.",
26486
+ "task.projectExisting": "This project has {total} tasks: {running} running, {pending} queued, and {failed} with issues.",
26487
+ "task.projectNew": "This is a new project group. It appears in the project list after creation.",
25769
26488
  "theme.label": "Theme",
25770
26489
  "theme.system": "System",
25771
26490
  "theme.light": "Light",
@@ -25783,18 +26502,28 @@ var init_ui = __esm({
25783
26502
  "dialog.disableTemplate": "Disable this schedule?",
25784
26503
  "dialog.disableTemplateBody": "It will stop creating new tasks automatically. Existing tasks are unchanged.",
25785
26504
  "dialog.deleteTemplate": "Delete this schedule?",
25786
- "dialog.deleteTemplateBody": "This template configuration will be permanently deleted.",
26505
+ "dialog.deleteTemplateBody": "This scheduled task configuration will be permanently deleted.",
25787
26506
  "dialog.triggerTemplate": "Run this schedule now?",
25788
- "dialog.triggerTemplateBody": "A new task is created from the template, subject to its instance limit.",
26507
+ "dialog.triggerTemplateBody": "A task is queued immediately using the current settings. If global concurrency is full, it waits to run.",
25789
26508
  "dialog.clearTitle": "Confirm database clear",
25790
- "dialog.clearBody": "This deletes every task, run, and schedule template after creating a backup.",
26509
+ "dialog.clearBody": "This deletes every task, run, and scheduled task after creating a backup.",
25791
26510
  "dialog.clearInstruction": "Type CLEAR to confirm",
26511
+ "dialog.restartGateway": "Restart and apply settings?",
26512
+ "dialog.restartGatewayBody": "Gateway will be briefly unavailable and should recover through PM2 within a few seconds.",
26513
+ "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
26514
  "feedback.retryFailed": "Retry failed",
25793
26515
  "feedback.cancelFailed": "Cancellation failed",
25794
26516
  "feedback.deleteFailed": "Delete failed",
25795
26517
  "feedback.requestFailed": "Request failed",
25796
26518
  "feedback.triggered": "Task #{id} created",
25797
- "feedback.configSaved": "Settings saved. Restart Gateway to apply them.",
26519
+ "feedback.taskCreated": "Task #{id} added to the queue",
26520
+ "feedback.taskUpdated": "Task #{id} updated",
26521
+ "feedback.templateCreated": "Scheduled task created",
26522
+ "feedback.templateUpdated": "Scheduled task updated",
26523
+ "feedback.configSaved": "Settings saved",
26524
+ "feedback.sessionCommandCopied": "Session command copied. Paste it into a terminal to continue.",
26525
+ "feedback.restarting": "Gateway is restarting\u2026",
26526
+ "feedback.restartTimeout": "Gateway has not recovered yet. Refresh later or check PM2 status.",
25798
26527
  "feedback.databaseCleared": "Database cleared. Backup: {path}",
25799
26528
  "feedback.copyFailed": "Copy failed. Select the content manually.",
25800
26529
  "a11y.skip": "Skip to main content",
@@ -25829,8 +26558,8 @@ var init_ui = __esm({
25829
26558
  radial-gradient(circle at 10% -10%,var(--bg-glow),transparent 34rem),var(--bg);
25830
26559
  font-family:Inter,ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
25831
26560
  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; }
26561
+ button,input,select,textarea { font:inherit; }
26562
+ button,a,select,input,textarea { -webkit-tap-highlight-color:transparent; }
25834
26563
  a { color:inherit; }
25835
26564
  code,.mono,.m { font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace; }
25836
26565
  .skip-link { position:fixed; left:16px; top:-60px; z-index:200; padding:10px 14px; border-radius:8px;
@@ -25911,6 +26640,16 @@ var init_ui = __esm({
25911
26640
  .panel-head { min-height:52px; display:flex; align-items:center; justify-content:space-between; gap:12px; padding:0 18px;
25912
26641
  border-bottom:1px solid var(--border); }
25913
26642
  .panel-head h2,.panel-head h3 { margin:0; font-size:14px; letter-spacing:-.01em; }
26643
+ .panel-head p { margin:3px 0 0; color:var(--text-2); font-size:11px; }
26644
+ .project-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(230px,1fr)); gap:10px; padding:14px; }
26645
+ .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; }
26646
+ .project-card:hover { border-color:var(--border-strong); background:var(--surface-3); }
26647
+ .project-card.active { border-color:color-mix(in srgb,var(--primary) 45%,var(--border)); background:var(--primary-soft); }
26648
+ .project-card-head { display:flex; align-items:center; justify-content:space-between; gap:10px; }
26649
+ .project-card-head strong { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
26650
+ .project-card-head span { color:var(--text-3); font-size:11px; }
26651
+ .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; }
26652
+ .project-counts { margin-top:8px; color:var(--text-2); font-size:11px; }
25914
26653
  .table-wrap { width:100%; overflow-x:auto; }
25915
26654
  table { width:100%; border-collapse:separate; border-spacing:0; font-size:13px; }
25916
26655
  th { height:42px; padding:0 13px; color:var(--text-3); background:var(--surface-2); border-bottom:1px solid var(--border);
@@ -25921,6 +26660,7 @@ var init_ui = __esm({
25921
26660
  tbody tr:hover { background:color-mix(in srgb,var(--primary-soft) 30%,transparent); }
25922
26661
  .task-name { font-weight:680; color:var(--text); }
25923
26662
  .task-prompt { max-width:520px; margin-top:3px; color:var(--text-2); font-size:12px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
26663
+ .task-context { margin-top:6px; }
25924
26664
  .muted,.mu { color:var(--text-2); }
25925
26665
  .faint { color:var(--text-3); }
25926
26666
  .small,.sm { font-size:12px; }
@@ -26004,6 +26744,18 @@ var init_ui = __esm({
26004
26744
  .dialog-head p { margin:3px 0 0; color:var(--text-2); font-size:11px; }
26005
26745
  .dialog-body { max-height:70vh; overflow:auto; padding:18px; }
26006
26746
  .dialog-actions { display:flex; align-items:center; justify-content:flex-end; gap:8px; padding:14px 18px; border-top:1px solid var(--border); }
26747
+ .template-dialog { width:min(880px,calc(100% - 32px)); }
26748
+ .template-form-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:15px; }
26749
+ .form-field { display:flex; min-width:0; flex-direction:column; gap:6px; color:var(--text-2); font-size:12px; font-weight:650; }
26750
+ .form-field-wide { grid-column:1 / -1; }
26751
+ .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;
26752
+ outline:none; color:var(--text); background:var(--surface-2); font-weight:450; }
26753
+ .form-field textarea { resize:vertical; line-height:1.5; }
26754
+ .form-field input:focus,.form-field select:focus,.form-field textarea:focus { border-color:var(--primary); box-shadow:var(--focus); background:var(--surface); }
26755
+ .form-field small { color:var(--text-3); font-size:10px; font-weight:450; }
26756
+ .advanced-fields { margin-top:18px; padding-top:14px; border-top:1px solid var(--border); }
26757
+ .advanced-fields summary { margin-bottom:14px; color:var(--text-2); cursor:pointer; font-size:12px; font-weight:700; }
26758
+ .form-note { margin:16px 0 0; color:var(--text-3); font-size:11px; }
26007
26759
  .json-view { min-height:160px; margin:0; padding:15px; overflow:auto; border:1px solid var(--border); border-radius:10px; color:var(--text-2);
26008
26760
  background:var(--surface-2); font-size:12px; white-space:pre-wrap; overflow-wrap:anywhere; }
26009
26761
  .confirm-copy { color:var(--text-2); margin:0; }
@@ -26069,6 +26821,9 @@ var init_ui = __esm({
26069
26821
  .responsive-table td[data-primary]::before { display:none; }
26070
26822
  .responsive-table .task-prompt { max-width:100%; }
26071
26823
  .responsive-table .actions { justify-content:flex-start; }
26824
+ .project-grid { grid-template-columns:1fr; }
26825
+ .template-form-grid { grid-template-columns:1fr; }
26826
+ .form-field-wide { grid-column:auto; }
26072
26827
  }
26073
26828
  @media (max-width:520px) {
26074
26829
  .stats-grid,.stats-grid.three { grid-template-columns:1fr 1fr; }
@@ -26096,23 +26851,186 @@ var init_ui = __esm({
26096
26851
  var web_exports = {};
26097
26852
  __export(web_exports, {
26098
26853
  dashboardApp: () => dashboardApp,
26099
- default: () => web_default
26854
+ default: () => web_default,
26855
+ isSafeDashboardRestartTarget: () => isSafeDashboardRestartTarget,
26856
+ resolveDashboardConfigState: () => resolveDashboardConfigState,
26857
+ setDashboardRuntimeConfig: () => setDashboardRuntimeConfig
26100
26858
  });
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";
26103
- function parsePositiveInteger2(value) {
26104
- if (!/^\d+$/.test(value)) return null;
26105
- const parsed = Number(value);
26106
- return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
26859
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync5, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "fs";
26860
+ import { basename as basename3, dirname as dirname8 } from "path";
26861
+ function setDashboardRuntimeConfig(config) {
26862
+ runtimeConfig = config === null ? null : structuredClone(config);
26107
26863
  }
26108
- function parseTaskStatus2(value) {
26109
- return TASK_STATUSES2.has(value) ? value : null;
26864
+ function isValidSessionId(value) {
26865
+ return typeof value === "string" && SESSION_ID_PATTERN.test(value);
26110
26866
  }
26111
- function safeStatus(value) {
26112
- return value && TASK_STATUSES2.has(value) ? value : "unknown";
26867
+ function maskSessionId(value) {
26868
+ return isValidSessionId(value) ? `${value.slice(4, 7)}***${value.slice(-3)}` : "\u2014";
26113
26869
  }
26114
- function resolveLocale(c) {
26115
- const requested = c.req.query("lang");
26870
+ function sessionCommand(value) {
26871
+ if (!isValidSessionId(value)) throw new Error("session unavailable");
26872
+ return `opencode --session ${value}`;
26873
+ }
26874
+ function configsEqual(left, right) {
26875
+ return JSON.stringify(left) === JSON.stringify(right);
26876
+ }
26877
+ function resolveDashboardConfigState(runtimeAvailable, restartRequired, managedRestart) {
26878
+ if (!runtimeAvailable) return "foreground";
26879
+ if (!restartRequired) return "applied";
26880
+ return managedRestart ? "pending" : "manual";
26881
+ }
26882
+ function isSafeDashboardRestartTarget(diagnostic, currentPid) {
26883
+ return diagnostic.pm2Installed && diagnostic.processFound && diagnostic.status === "online" && diagnostic.pid === currentPid && diagnostic.ready && diagnostic.scopeMatches;
26884
+ }
26885
+ function canRestartCurrentGateway() {
26886
+ if (runtimeConfig === null) return false;
26887
+ try {
26888
+ return isSafeDashboardRestartTarget(getGatewayDiagnostic(), process.pid);
26889
+ } catch {
26890
+ return false;
26891
+ }
26892
+ }
26893
+ function parsePositiveInteger2(value) {
26894
+ if (!/^\d+$/.test(value)) return null;
26895
+ const parsed = Number(value);
26896
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
26897
+ }
26898
+ function parseTaskStatus2(value) {
26899
+ return TASK_STATUSES2.has(value) ? value : null;
26900
+ }
26901
+ function parseTaskPayload(value) {
26902
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
26903
+ throw new Error("\u8BF7\u6C42\u5185\u5BB9\u5FC5\u987B\u662F\u5BF9\u8C61");
26904
+ }
26905
+ const input = value;
26906
+ const requiredString = (name) => {
26907
+ const field = input[name];
26908
+ if (typeof field !== "string" || !field.trim()) throw new Error(`${name} \u4E0D\u80FD\u4E3A\u7A7A`);
26909
+ return field.trim();
26910
+ };
26911
+ const optionalString = (name, fallback = null) => {
26912
+ const field = input[name];
26913
+ if (field === void 0 || field === null || field === "") return fallback;
26914
+ if (typeof field !== "string") throw new Error(`${name} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`);
26915
+ return field.trim() || fallback;
26916
+ };
26917
+ const integer2 = (name, fallback) => {
26918
+ const field = input[name];
26919
+ if (field === void 0 || field === null || field === "") return fallback;
26920
+ if (typeof field !== "number" || !Number.isSafeInteger(field)) {
26921
+ throw new Error(`${name} \u5FC5\u987B\u662F\u6574\u6570`);
26922
+ }
26923
+ return field;
26924
+ };
26925
+ const duration = (name, fallback, allowZero = false) => {
26926
+ const raw2 = optionalString(name, fallback);
26927
+ if (raw2 === null) return null;
26928
+ if (allowZero && raw2 === "0") return 0;
26929
+ if (Number.isFinite(Number(raw2))) {
26930
+ throw new Error(`${name} \u5FC5\u987B\u5E26\u65F6\u95F4\u5355\u4F4D\uFF0C\u8BF7\u4F7F\u7528 30s\u30015min\u30011h \u6216 2d`);
26931
+ }
26932
+ const milliseconds = parseDuration(raw2);
26933
+ if (milliseconds === null || !Number.isSafeInteger(milliseconds)) {
26934
+ throw new Error(`${name} \u683C\u5F0F\u65E0\u6548\uFF0C\u8BF7\u4F7F\u7528 30s\u30015min\u30011h \u6216 2d`);
26935
+ }
26936
+ return milliseconds;
26937
+ };
26938
+ return {
26939
+ name: requiredString("name"),
26940
+ cwd: requiredString("cwd"),
26941
+ agent: requiredString("agent"),
26942
+ model: optionalString("model", "default"),
26943
+ prompt: requiredString("prompt"),
26944
+ category: optionalString("category", "general"),
26945
+ batchId: optionalString("batchId"),
26946
+ importance: integer2("importance", 3),
26947
+ urgency: integer2("urgency", 3),
26948
+ maxRetries: integer2("maxRetries", 3),
26949
+ retryBackoffMs: duration("retryBackoff", "30s", true),
26950
+ timeoutMs: duration("timeout", null)
26951
+ };
26952
+ }
26953
+ function editableTaskPayload(input) {
26954
+ return {
26955
+ name: input.name,
26956
+ agent: input.agent,
26957
+ model: input.model ?? "default",
26958
+ prompt: input.prompt,
26959
+ category: input.category ?? "general",
26960
+ batchId: input.batchId ?? null,
26961
+ importance: input.importance ?? 3,
26962
+ urgency: input.urgency ?? 3,
26963
+ maxRetries: input.maxRetries ?? 3,
26964
+ retryBackoffMs: input.retryBackoffMs ?? 3e4,
26965
+ timeoutMs: input.timeoutMs ?? null
26966
+ };
26967
+ }
26968
+ function parseTemplatePayload(value) {
26969
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
26970
+ throw new Error("\u8BF7\u6C42\u5185\u5BB9\u5FC5\u987B\u662F\u5BF9\u8C61");
26971
+ }
26972
+ const input = value;
26973
+ const requiredString = (name) => {
26974
+ const field = input[name];
26975
+ if (typeof field !== "string" || !field.trim()) throw new Error(`${name} \u4E0D\u80FD\u4E3A\u7A7A`);
26976
+ return field.trim();
26977
+ };
26978
+ const optionalString = (name, fallback = null) => {
26979
+ const field = input[name];
26980
+ if (field === void 0 || field === null || field === "") return fallback;
26981
+ if (typeof field !== "string") throw new Error(`${name} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`);
26982
+ return field.trim() || fallback;
26983
+ };
26984
+ const integer2 = (name, fallback) => {
26985
+ const field = input[name];
26986
+ if (field === void 0 || field === null || field === "") return fallback;
26987
+ if (typeof field !== "number" || !Number.isSafeInteger(field)) {
26988
+ throw new Error(`${name} \u5FC5\u987B\u662F\u6574\u6570`);
26989
+ }
26990
+ return field;
26991
+ };
26992
+ const duration = (name, fallback, allowZero = false) => {
26993
+ const raw2 = optionalString(name, fallback);
26994
+ if (raw2 === null) return null;
26995
+ if (allowZero && raw2 === "0") return 0;
26996
+ if (Number.isFinite(Number(raw2))) {
26997
+ throw new Error(`${name} \u5FC5\u987B\u5E26\u65F6\u95F4\u5355\u4F4D\uFF0C\u8BF7\u4F7F\u7528 30s\u30015min\u30011h \u6216 2d`);
26998
+ }
26999
+ const milliseconds = parseDuration(raw2);
27000
+ if (milliseconds === null || !Number.isSafeInteger(milliseconds)) {
27001
+ throw new Error(`${name} \u683C\u5F0F\u65E0\u6548\uFF0C\u8BF7\u4F7F\u7528 30s\u30015min\u30011h \u6216 2d`);
27002
+ }
27003
+ return milliseconds;
27004
+ };
27005
+ const scheduleType = requiredString("scheduleType");
27006
+ if (!["cron", "delayed", "recurring"].includes(scheduleType)) {
27007
+ throw new Error("scheduleType \u5FC5\u987B\u662F cron\u3001delayed \u6216 recurring");
27008
+ }
27009
+ return {
27010
+ name: requiredString("name"),
27011
+ agent: requiredString("agent"),
27012
+ model: optionalString("model", "default"),
27013
+ prompt: requiredString("prompt"),
27014
+ cwd: requiredString("cwd"),
27015
+ category: optionalString("category", "general"),
27016
+ importance: integer2("importance", 3),
27017
+ urgency: integer2("urgency", 3),
27018
+ batchId: optionalString("batchId"),
27019
+ scheduleType,
27020
+ cronExpr: scheduleType === "cron" ? requiredString("cronExpr") : null,
27021
+ intervalMs: scheduleType === "recurring" ? duration("interval", null) : null,
27022
+ runAt: scheduleType === "delayed" ? integer2("runAt", null) : null,
27023
+ maxInstances: integer2("maxInstances", 1),
27024
+ maxRetries: integer2("maxRetries", 3),
27025
+ retryBackoffMs: duration("retryBackoff", "30s", true),
27026
+ timeoutMs: duration("timeout", null)
27027
+ };
27028
+ }
27029
+ function safeStatus(value) {
27030
+ return value && TASK_STATUSES2.has(value) ? value : "unknown";
27031
+ }
27032
+ function resolveLocale(c) {
27033
+ const requested = c.req.query("lang");
26116
27034
  if (requested === "en" || requested === "zh-CN") return requested;
26117
27035
  const cookie = c.req.header("Cookie") ?? "";
26118
27036
  const match2 = /(?:^|;\s*)supertask_locale=([^;]+)/.exec(cookie);
@@ -26142,9 +27060,9 @@ function esc(value) {
26142
27060
  }
26143
27061
  function readCurrentConfig() {
26144
27062
  const configPath = getConfigPath();
26145
- if (!existsSync7(configPath)) return {};
27063
+ if (!existsSync8(configPath)) return {};
26146
27064
  try {
26147
- return JSON.parse(readFileSync4(configPath, "utf-8"));
27065
+ return JSON.parse(readFileSync5(configPath, "utf-8"));
26148
27066
  } catch {
26149
27067
  return {};
26150
27068
  }
@@ -26152,7 +27070,7 @@ function readCurrentConfig() {
26152
27070
  function writeConfig(cfg) {
26153
27071
  const configPath = getConfigPath();
26154
27072
  const dir = dirname8(configPath);
26155
- if (!existsSync7(dir)) mkdirSync5(dir, { recursive: true });
27073
+ if (!existsSync8(dir)) mkdirSync5(dir, { recursive: true });
26156
27074
  const tempPath = `${configPath}.${process.pid}.tmp`;
26157
27075
  writeFileSync3(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
26158
27076
  renameSync2(tempPath, configPath);
@@ -26187,13 +27105,14 @@ function pagination(locale, basePath, page, pages, total, suffix = "") {
26187
27105
  const next = page < pages ? `<a class="btn" href="${basePath}?page=${page + 1}${suffix}">${t(locale, "pagination.next")}${icon("chevronRight")}</a>` : "";
26188
27106
  return `<div class="pagination">${previous}<span class="summary">${t(locale, "pagination.summary", { page, pages, total })}</span>${next}</div>`;
26189
27107
  }
26190
- var app, TASK_STATUSES2, dashboardApp, web_default;
27108
+ var app, LEGACY_PROJECT_FILTER, TASK_STATUSES2, SESSION_ID_PATTERN, runtimeConfig, restartScheduled, dashboardApp, web_default;
26191
27109
  var init_web = __esm({
26192
27110
  "src/web/index.tsx"() {
26193
27111
  "use strict";
26194
27112
  init_dist();
26195
27113
  init_drizzle_orm();
26196
27114
  init_db2();
27115
+ init_duration();
26197
27116
  init_database_maintenance_service();
26198
27117
  init_task_run_service();
26199
27118
  init_task_service();
@@ -26201,8 +27120,10 @@ var init_web = __esm({
26201
27120
  init_config();
26202
27121
  init_health();
26203
27122
  init_job_templates();
27123
+ init_pm2();
26204
27124
  init_ui();
26205
27125
  app = new Hono2();
27126
+ LEGACY_PROJECT_FILTER = "__supertask_legacy__";
26206
27127
  TASK_STATUSES2 = /* @__PURE__ */ new Set([
26207
27128
  "pending",
26208
27129
  "running",
@@ -26211,6 +27132,9 @@ var init_web = __esm({
26211
27132
  "dead_letter",
26212
27133
  "cancelled"
26213
27134
  ]);
27135
+ SESSION_ID_PATTERN = /^ses_[A-Za-z0-9_]+$/;
27136
+ runtimeConfig = null;
27137
+ restartScheduled = false;
26214
27138
  app.use("*", async (c, next) => {
26215
27139
  await next();
26216
27140
  c.header("X-Content-Type-Options", "nosniff");
@@ -26247,24 +27171,52 @@ var init_web = __esm({
26247
27171
  const page = parsePositiveInteger2(c.req.query("page") || "1");
26248
27172
  if (page === null) return c.text("invalid page", 400);
26249
27173
  const statusFilter = c.req.query("status") || "";
27174
+ const requestedCwd = c.req.query("cwd") || "";
27175
+ const legacyProjectFilter = requestedCwd === LEGACY_PROJECT_FILTER;
27176
+ const cwdFilter = legacyProjectFilter ? "" : requestedCwd;
27177
+ const projectFilter = legacyProjectFilter ? LEGACY_PROJECT_FILTER : cwdFilter;
26250
27178
  const parsedStatus = statusFilter ? parseTaskStatus2(statusFilter) : null;
26251
27179
  if (statusFilter && !parsedStatus) return c.text("invalid status", 400);
26252
27180
  const limit = 50;
26253
27181
  const offset = (page - 1) * limit;
26254
- const [tasks4, statsData] = await Promise.all([
26255
- TaskService.list({ limit, offset, ...parsedStatus ? { status: parsedStatus } : {} }),
26256
- TaskService.stats({})
27182
+ const [tasks4, statsData, globalStats, projects, globalRunning, legacyStats, legacyRunning] = await Promise.all([
27183
+ TaskService.list({
27184
+ limit,
27185
+ offset,
27186
+ ...parsedStatus === "running" ? { activeExecution: true } : parsedStatus ? { status: parsedStatus } : {},
27187
+ ...legacyProjectFilter ? { legacyCwd: true } : cwdFilter ? { cwd: cwdFilter } : {}
27188
+ }),
27189
+ TaskService.stats(legacyProjectFilter ? { legacyCwd: true } : cwdFilter ? { cwd: cwdFilter } : {}),
27190
+ TaskService.stats(),
27191
+ TaskService.projectSummaries(1e3),
27192
+ TaskService.countRunning(),
27193
+ TaskService.stats({ legacyCwd: true }),
27194
+ TaskService.countRunning({ legacyCwd: true })
26257
27195
  ]);
26258
27196
  const latestRuns = await TaskRunService.getLatestByTaskIds(tasks4.map((task) => task.id));
27197
+ const selectedRunning = legacyProjectFilter ? legacyRunning : cwdFilter ? projects.find((project) => project.cwd === cwdFilter)?.running ?? 0 : globalRunning;
26259
27198
  const counts = {
26260
27199
  pending: statsData.pending || 0,
26261
- running: statsData.running || 0,
27200
+ running: selectedRunning,
26262
27201
  done: statsData.done || 0,
26263
27202
  failed: (statsData.failed || 0) + (statsData.dead_letter || 0),
26264
27203
  total: statsData.total || 0
26265
27204
  };
26266
- const filteredTotal = parsedStatus ? Number(statsData[parsedStatus] ?? 0) : counts.total;
27205
+ const filteredTotal = parsedStatus === "running" ? selectedRunning : parsedStatus ? Number(statsData[parsedStatus] ?? 0) : counts.total;
26267
27206
  const totalPages = Math.max(1, Math.ceil(filteredTotal / limit));
27207
+ if (page > totalPages) {
27208
+ const params = new URLSearchParams({ page: String(totalPages) });
27209
+ if (statusFilter) params.set("status", statusFilter);
27210
+ if (projectFilter) params.set("cwd", projectFilter);
27211
+ return c.redirect(`/?${params.toString()}`);
27212
+ }
27213
+ const taskListUrl = (status, cwd) => {
27214
+ const params = new URLSearchParams();
27215
+ if (status) params.set("status", status);
27216
+ if (cwd) params.set("cwd", cwd);
27217
+ const query = params.toString();
27218
+ return query ? `/?${query}` : "/";
27219
+ };
26268
27220
  const filterItems = [
26269
27221
  { status: "", label: t(locale, "filter.all") },
26270
27222
  { status: "pending", label: statusText(locale, "pending") },
@@ -26275,22 +27227,27 @@ var init_web = __esm({
26275
27227
  { status: "cancelled", label: statusText(locale, "cancelled") }
26276
27228
  ];
26277
27229
  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>`;
27230
+ const href = taskListUrl(status, projectFilter);
27231
+ return `<a href="${esc(href)}" class="filter-chip ${statusFilter === status ? "active" : ""}">${label}</a>`;
26280
27232
  }).join("");
26281
27233
  const rows = tasks4.map((task) => {
26282
27234
  const status = safeStatus(task.status);
26283
- const executionActive = latestRuns.get(task.id)?.status === "running";
26284
- const searchable = esc(`${task.name} ${task.agent} ${task.prompt}`);
27235
+ const latestRun = latestRuns.get(task.id);
27236
+ const executionActive = latestRun?.status === "running";
27237
+ const batchId = task.batchId?.trim() || null;
27238
+ const searchable = esc(`${task.name} ${task.agent} ${task.prompt} ${task.cwd ?? ""} ${task.batchId ?? ""} ${task.category ?? ""}`);
26285
27239
  return `<tr data-task-row data-search="${searchable}">
26286
27240
  <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>
27241
+ <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>
27242
+ <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
27243
  <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>
27244
+ <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>
27245
+ <td data-label="${t(locale, "table.duration")}" class="small ${executionActive || task.status === "running" ? "" : "muted"}">${formatDuration(task.startedAt, task.finishedAt)}</td>
26291
27246
  <td data-label="${t(locale, "table.retries")}" class="muted small">${(task.retryCount ?? 0) > 0 ? task.retryCount : "\u2014"}</td>
26292
27247
  <td data-label="${t(locale, "table.actions")}"><div class="actions">
27248
+ ${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
27249
  <button type="button" class="btn" onclick="showDetail(${task.id})">${t(locale, "action.details")}</button>
27250
+ ${isValidSessionId(latestRun?.sessionId) ? `<button type="button" class="btn" onclick="copySessionCommand(${latestRun.id})">${icon("copy")}${t(locale, "action.continueSession")}</button>` : ""}
26294
27251
  ${task.status === "failed" || task.status === "dead_letter" ? `<button type="button" class="btn btn-warning" onclick="retryTask(${task.id})">${t(locale, "action.retry")}</button>` : ""}
26295
27252
  ${["pending", "running", "failed"].includes(task.status ?? "") ? `<button type="button" class="btn btn-warning" onclick="cancelTask(${task.id})">${t(locale, "action.cancel")}</button>` : ""}
26296
27253
  ${task.status === "running" || executionActive ? "" : `<button type="button" class="btn btn-danger" onclick="deleteTask(${task.id})">${t(locale, "action.delete")}</button>`}
@@ -26302,7 +27259,32 @@ var init_web = __esm({
26302
27259
  <tbody>${rows}</tbody>
26303
27260
  </table></div>
26304
27261
  <div id="search-empty" hidden>${emptyState(t(locale, "filter.noResults"), "")}</div>`;
26305
- const suffix = statusFilter ? `&status=${statusFilter}` : "";
27262
+ const paginationParts = [];
27263
+ if (statusFilter) paginationParts.push(`status=${encodeURIComponent(statusFilter)}`);
27264
+ if (projectFilter) paginationParts.push(`cwd=${encodeURIComponent(projectFilter)}`);
27265
+ const suffix = paginationParts.length > 0 ? `&${paginationParts.join("&")}` : "";
27266
+ const projectCards = [
27267
+ `<a class="project-card ${projectFilter ? "" : "active"}" href="${esc(taskListUrl(statusFilter, ""))}">
27268
+ <div class="project-card-head"><strong>${t(locale, "projects.all")}</strong><span>${globalStats.total || 0}</span></div>
27269
+ <div class="project-counts">${t(locale, "projects.counts", { running: globalRunning, pending: globalStats.pending || 0, failed: (globalStats.failed || 0) + (globalStats.dead_letter || 0) })}</div>
27270
+ </a>`,
27271
+ ...legacyStats.total > 0 ? [`<a class="project-card ${legacyProjectFilter ? "active" : ""}" href="${esc(taskListUrl(statusFilter, LEGACY_PROJECT_FILTER))}">
27272
+ <div class="project-card-head"><strong>${t(locale, "projects.legacy")}</strong><span>${legacyStats.total}</span></div>
27273
+ <div class="project-path">${t(locale, "projects.legacyHint")}</div>
27274
+ <div class="project-counts">${t(locale, "projects.counts", { running: legacyRunning, pending: legacyStats.pending || 0, failed: (legacyStats.failed || 0) + (legacyStats.dead_letter || 0) })}</div>
27275
+ </a>`] : [],
27276
+ ...projects.map((project) => `<a class="project-card ${cwdFilter === project.cwd ? "active" : ""}" href="${esc(taskListUrl(statusFilter, project.cwd))}" title="${esc(project.cwd)}">
27277
+ <div class="project-card-head"><strong>${esc(basename3(project.cwd) || project.cwd)}</strong><span>${project.total}</span></div>
27278
+ <div class="project-path">${esc(project.cwd)}</div>
27279
+ <div class="project-counts">${t(locale, "projects.counts", { running: project.running, pending: project.pending, failed: project.failed })}</div>
27280
+ </a>`)
27281
+ ].join("");
27282
+ const projectData = JSON.stringify(Object.fromEntries(projects.map((project) => [project.cwd, {
27283
+ total: project.total,
27284
+ pending: project.pending,
27285
+ running: project.running,
27286
+ failed: project.failed
27287
+ }]))).replace(/</g, "\\u003c");
26306
27288
  const body = `
26307
27289
  <div class="stats-grid">
26308
27290
  ${statCard(counts.pending, t(locale, "stats.pending"), "tone-neutral", icon("clock"))}
@@ -26310,19 +27292,61 @@ var init_web = __esm({
26310
27292
  ${statCard(counts.done, t(locale, "stats.done"), "tone-green", icon("check"), "reveal-delay-1")}
26311
27293
  ${statCard(counts.failed, t(locale, "stats.failedDead"), "tone-red", icon("alert"), "reveal-delay-2")}
26312
27294
  </div>
27295
+ <section class="panel project-panel reveal reveal-delay-1">
27296
+ <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>
27297
+ <div class="project-grid">${projectCards}</div>
27298
+ </section>
26313
27299
  <div class="toolbar reveal reveal-delay-1">
26314
27300
  <div class="filters">${filters}</div>
26315
27301
  <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
27302
  </div>
26317
27303
  <section class="panel reveal reveal-delay-2">${table}</section>
26318
- ${pagination(locale, "/", page, totalPages, filteredTotal, suffix)}`;
27304
+ ${pagination(locale, "/", page, totalPages, filteredTotal, suffix)}
27305
+ <script type="application/json" id="task-project-data">${projectData}</script>
27306
+ <dialog id="task-dialog" class="template-dialog">
27307
+ <form id="task-form" data-default-cwd="${esc(cwdFilter)}" onsubmit="saveTask(event)">
27308
+ <input id="task-id" type="hidden">
27309
+ <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>
27310
+ <div class="dialog-body">
27311
+ <div class="template-form-grid">
27312
+ <label class="form-field"><span>${t(locale, "template.name")}</span><input id="task-name" required maxlength="200" autocomplete="off"></label>
27313
+ <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>
27314
+ <datalist id="task-cwd-options">${projects.map((project) => `<option value="${esc(project.cwd)}"></option>`).join("")}</datalist>
27315
+ <label class="form-field"><span>${t(locale, "template.agent")}</span><input id="task-agent" required autocomplete="off" value="build" placeholder="build"></label>
27316
+ <label class="form-field"><span>${t(locale, "template.model")}</span><input id="task-model" required autocomplete="off" value="default" placeholder="default"></label>
27317
+ <label class="form-field form-field-wide"><span>${t(locale, "template.prompt")}</span><textarea id="task-prompt" rows="6" required></textarea></label>
27318
+ </div>
27319
+ <p id="task-project-status" class="form-note"></p>
27320
+ <details class="advanced-fields">
27321
+ <summary>${t(locale, "template.advanced")}</summary>
27322
+ <div class="template-form-grid">
27323
+ <label class="form-field"><span>${t(locale, "template.category")}</span><input id="task-category" autocomplete="off" value="general"></label>
27324
+ <label class="form-field"><span>${t(locale, "template.batchId")}</span><input id="task-batch" autocomplete="off"><small>${t(locale, "task.batchHint")}</small></label>
27325
+ <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>
27326
+ <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>
27327
+ <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>
27328
+ <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>
27329
+ <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>
27330
+ </div>
27331
+ </details>
27332
+ </div>
27333
+ <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>
27334
+ </form>
27335
+ </dialog>`;
26319
27336
  return c.html(renderLayout({ locale, activeTab: "tasks", body }));
26320
27337
  });
26321
27338
  app.get("/templates", async (c) => {
26322
27339
  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;
27340
+ const page = parsePositiveInteger2(c.req.query("page") || "1");
27341
+ if (page === null) return c.text("invalid page", 400);
27342
+ const limit = 50;
27343
+ const offset = (page - 1) * limit;
27344
+ const [templates, templateStats] = await Promise.all([
27345
+ TaskTemplateService.list(limit, offset),
27346
+ TaskTemplateService.stats()
27347
+ ]);
27348
+ const totalPages = Math.max(1, Math.ceil(templateStats.total / limit));
27349
+ if (page > totalPages) return c.redirect(`/templates?page=${totalPages}`);
26326
27350
  const rows = templates.map((template) => {
26327
27351
  const scheduleType = ["cron", "recurring", "delayed"].includes(template.scheduleType) ? template.scheduleType : "unknown";
26328
27352
  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 +27364,56 @@ var init_web = __esm({
26340
27364
  <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
27365
  <td data-label="${t(locale, "table.lastRun")}" class="small muted">${formatRelative(template.lastRunAt, locale)}</td>
26342
27366
  <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>
27367
+ <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
27368
  <button type="button" class="btn btn-primary" onclick="triggerTmpl(${template.id})">${t(locale, "action.trigger")}</button>${toggle}
26345
27369
  <button type="button" class="btn btn-danger" onclick="deleteTmpl(${template.id})">${t(locale, "action.delete")}</button></div></td>
26346
27370
  </tr>`;
26347
27371
  }).join("");
26348
27372
  const body = `
26349
27373
  <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")}
27374
+ ${statCard(templateStats.total, t(locale, "stats.templates"), "tone-purple", icon("templates"))}
27375
+ ${statCard(templateStats.enabled, t(locale, "stats.enabled"), "tone-green", icon("check"), "reveal-delay-1")}
27376
+ ${statCard(templateStats.disabled, t(locale, "stats.disabled"), "tone-neutral", icon("clock"), "reveal-delay-2")}
26353
27377
  </div>
26354
27378
  <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>`;
27379
+ <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>
27380
+ ${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>`}
27381
+ </section>
27382
+ <dialog id="template-dialog" class="template-dialog">
27383
+ <form id="template-form" onsubmit="saveTemplate(event)">
27384
+ <input id="template-id" type="hidden">
27385
+ <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>
27386
+ <div class="dialog-body">
27387
+ <div class="template-form-grid">
27388
+ <label class="form-field"><span>${t(locale, "template.name")}</span><input id="template-name" required maxlength="200" autocomplete="off"></label>
27389
+ <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>
27390
+ <label class="form-field"><span>${t(locale, "template.agent")}</span><input id="template-agent" required autocomplete="off" placeholder="build"></label>
27391
+ <label class="form-field"><span>${t(locale, "template.model")}</span><input id="template-model" required autocomplete="off" value="default" placeholder="default"></label>
27392
+ <label class="form-field form-field-wide"><span>${t(locale, "template.prompt")}</span><textarea id="template-prompt" rows="6" required></textarea></label>
27393
+ <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>
27394
+ <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>
27395
+ <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>
27396
+ <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>
27397
+ </div>
27398
+ <details class="advanced-fields">
27399
+ <summary>${t(locale, "template.advanced")}</summary>
27400
+ <div class="template-form-grid">
27401
+ <label class="form-field"><span>${t(locale, "template.category")}</span><input id="template-category" autocomplete="off" value="general"></label>
27402
+ <label class="form-field"><span>${t(locale, "template.batchId")}</span><input id="template-batch" autocomplete="off"><small>${t(locale, "template.optional")}</small></label>
27403
+ <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>
27404
+ <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>
27405
+ <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>
27406
+ <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>
27407
+ <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>
27408
+ <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>
27409
+ </div>
27410
+ </details>
27411
+ <p class="form-note">${t(locale, "template.futureOnly")}</p>
27412
+ </div>
27413
+ <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>
27414
+ </form>
27415
+ </dialog>
27416
+ ${pagination(locale, "/templates", page, totalPages, templateStats.total)}`;
26358
27417
  return c.html(renderLayout({ locale, activeTab: "templates", body }));
26359
27418
  });
26360
27419
  app.get("/runs", async (c) => {
@@ -26382,9 +27441,11 @@ var init_web = __esm({
26382
27441
  const totalResult = await db.select({ count: sql`count(*)` }).from(taskRuns4);
26383
27442
  const total = Number(totalResult[0]?.count ?? 0);
26384
27443
  const totalPages = Math.max(1, Math.ceil(total / limit));
27444
+ if (page > totalPages) return c.redirect(`/runs?page=${totalPages}`);
26385
27445
  const logs = [];
26386
27446
  const rows = runs.map((run) => {
26387
27447
  const status = safeStatus(run.status);
27448
+ const resumable = isValidSessionId(run.sessionId);
26388
27449
  if (run.log) {
26389
27450
  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
27451
  }
@@ -26392,10 +27453,12 @@ var init_web = __esm({
26392
27453
  <td class="faint" data-label="${t(locale, "table.run")}">#${run.id}</td>
26393
27454
  <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
27455
  <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>
27456
+ <td data-label="${t(locale, "table.session")}" class="m small">${esc(maskSessionId(run.sessionId))}</td>
27457
+ <td data-label="${t(locale, "table.status")}"><span class="badge b-${status}">${runStatusText(locale, run.status ?? "unknown")}</span></td>
26396
27458
  <td data-label="${t(locale, "table.duration")}" class="small">${formatDuration(run.startedAt, run.finishedAt)}</td>
26397
27459
  <td data-label="${t(locale, "table.heartbeat")}" class="small muted">${formatRelative(run.heartbeatAt, locale)}</td>
26398
27460
  <td data-label="${t(locale, "table.actions")}"><div class="actions"><button type="button" class="btn" onclick="showRunDetail(${run.id})">${t(locale, "action.details")}</button>
27461
+ ${resumable ? `<button type="button" class="btn" onclick="copySessionCommand(${run.id})">${icon("copy")}${t(locale, "action.continueSession")}</button>` : ""}
26399
27462
  ${run.log ? `<button type="button" class="btn" aria-expanded="false" onclick="toggleLog(${run.id},this)">${t(locale, "action.logs")}</button>` : ""}</div></td>
26400
27463
  </tr>`;
26401
27464
  }).join("");
@@ -26406,22 +27469,37 @@ var init_web = __esm({
26406
27469
  ${statCard(runs.filter((run) => run.status === "failed").length, t(locale, "stats.pageFailed"), "tone-red", icon("alert"), "reveal-delay-1")}
26407
27470
  ${statCard(runs.filter((run) => run.status === "running").length, t(locale, "stats.pageRunning"), "tone-blue", icon("activity"), "reveal-delay-2")}
26408
27471
  </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>
27472
+ <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
27473
  ${logs.join("")}${pagination(locale, "/runs", page, totalPages, total)}`;
26411
27474
  return c.html(renderLayout({ locale, activeTab: "runs", body }));
26412
27475
  });
26413
27476
  app.get("/system", async (c) => {
26414
27477
  const locale = resolveLocale(c);
26415
27478
  const config = loadConfig();
27479
+ const activeConfig = runtimeConfig ?? config;
27480
+ const restartRequired = runtimeConfig !== null && !configsEqual(config, runtimeConfig);
27481
+ const managedRestart = canRestartCurrentGateway();
27482
+ const configState = resolveDashboardConfigState(
27483
+ runtimeConfig !== null,
27484
+ restartRequired,
27485
+ managedRestart
27486
+ );
27487
+ const configStateKey = {
27488
+ foreground: "system.configForeground",
27489
+ applied: "system.configApplied",
27490
+ pending: "system.configPending",
27491
+ manual: "system.configRestartManually"
27492
+ }[configState];
27493
+ const configStateText = t(locale, configStateKey);
26416
27494
  const configPath = getConfigPath();
26417
- const [stats, runningRuns, templates] = await Promise.all([
27495
+ const [stats, runningRuns, templateStats] = await Promise.all([
26418
27496
  TaskService.stats({}),
26419
27497
  TaskRunService.getAllRunningRuns(),
26420
- TaskTemplateService.list(100)
27498
+ TaskTemplateService.stats()
26421
27499
  ]);
26422
- const configExists = existsSync7(configPath);
27500
+ const configExists = existsSync8(configPath);
26423
27501
  const runRows = runningRuns.map((run) => {
26424
- const session = run.sessionId ? `${run.sessionId.slice(4, 7)}***${run.sessionId.slice(-3)}` : "\u2014";
27502
+ const session = maskSessionId(run.sessionId);
26425
27503
  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
27504
  <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
27505
  <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 +27515,10 @@ var init_web = __esm({
26437
27515
  <div class="field"><label for="hi">${t(locale, "system.heartbeatInterval")}</label>${unitInput("hi", config.worker.heartbeatIntervalMs / 1e3, 5, t(locale, "system.seconds"))}</div>
26438
27516
  <div class="field"><label for="to">${t(locale, "system.taskTimeout")}</label>${unitInput("to", config.worker.taskTimeoutMs / 6e4, 1, t(locale, "system.minutes"))}</div>
26439
27517
  </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>
27518
+ <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
27519
  <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
27520
  <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>
27521
+ <div class="info-row"><span class="info-key">${t(locale, "system.activeTemplates")}</span><span class="info-value">${templateStats.enabled} / ${templateStats.total}</span></div>
26444
27522
  </section>
26445
27523
  <section class="card settings-card reveal-delay-2"><h2 class="settings-title"><span>${icon("system")}${t(locale, "system.watchdog")}</span></h2>
26446
27524
  <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 +27527,10 @@ var init_web = __esm({
26449
27527
  <div class="field"><label for="rd">${t(locale, "system.retentionDays")}</label>${unitInput("rd", config.watchdog.retentionDays, 1, t(locale, "system.days"))}</div>
26450
27528
  </section>
26451
27529
  </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>
27530
+ <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
27531
  </form>
26454
27532
  <section class="panel reveal">
26455
- <div class="panel-head"><h2>${t(locale, "system.runningTasks", { running: runningRuns.length, limit: config.worker.maxConcurrency })}</h2></div>
27533
+ <div class="panel-head"><h2>${t(locale, "system.runningTasks", { running: runningRuns.length, limit: activeConfig.worker.maxConcurrency })}</h2></div>
26456
27534
  ${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
27535
  </section>
26458
27536
  <section class="panel reveal reveal-delay-1"><div class="panel-head"><h2>${t(locale, "system.taskStats")}</h2></div><div class="overview-grid">
@@ -26469,6 +27547,30 @@ var init_web = __esm({
26469
27547
  <button type="button" class="btn btn-danger" onclick="clearDatabase()">${icon("database")}${t(locale, "action.clearDatabase")}</button></section>`;
26470
27548
  return c.html(renderLayout({ locale, activeTab: "system", body }));
26471
27549
  });
27550
+ app.post("/api/tasks", async (c) => {
27551
+ try {
27552
+ const task = await TaskService.add(parseTaskPayload(await c.req.json()));
27553
+ return c.json({ success: true, task }, 201);
27554
+ } catch (error) {
27555
+ return c.json({
27556
+ error: error instanceof Error ? error.message : String(error)
27557
+ }, 400);
27558
+ }
27559
+ });
27560
+ app.put("/api/tasks/:id", async (c) => {
27561
+ const id = parsePositiveInteger2(c.req.param("id"));
27562
+ if (id === null) return c.json({ error: "invalid id" }, 400);
27563
+ try {
27564
+ const input = parseTaskPayload(await c.req.json());
27565
+ const task = await TaskService.update(id, editableTaskPayload(input));
27566
+ if (task) return c.json({ success: true, task });
27567
+ return await TaskService.getById(id) ? c.json({ error: "task status does not allow editing" }, 409) : c.json({ error: "not found" }, 404);
27568
+ } catch (error) {
27569
+ return c.json({
27570
+ error: error instanceof Error ? error.message : String(error)
27571
+ }, 400);
27572
+ }
27573
+ });
26472
27574
  app.get("/api/tasks/:id", async (c) => {
26473
27575
  const id = parsePositiveInteger2(c.req.param("id"));
26474
27576
  if (id === null) return c.json({ error: "invalid id" }, 400);
@@ -26484,6 +27586,24 @@ var init_web = __esm({
26484
27586
  if (!run) return c.json({ error: "not found" }, 404);
26485
27587
  return c.json(run);
26486
27588
  });
27589
+ app.get("/api/runs/:id/session-command", async (c) => {
27590
+ const id = parsePositiveInteger2(c.req.param("id"));
27591
+ if (id === null) return c.json({ error: "invalid id" }, 400);
27592
+ const run = await TaskRunService.getById(id);
27593
+ if (!run) return c.json({ error: "not found" }, 404);
27594
+ if (!isValidSessionId(run.sessionId)) return c.json({ error: "session unavailable" }, 409);
27595
+ return c.json({ command: sessionCommand(run.sessionId) });
27596
+ });
27597
+ app.get("/api/gateway/status", (c) => {
27598
+ const savedConfig = loadConfig();
27599
+ const managed = canRestartCurrentGateway();
27600
+ return c.json({
27601
+ pid: process.pid,
27602
+ managed,
27603
+ ready: managed && getGatewayHealth().status === "ok",
27604
+ restartRequired: runtimeConfig === null || !configsEqual(savedConfig, runtimeConfig)
27605
+ });
27606
+ });
26487
27607
  app.get("/api/templates/:id", async (c) => {
26488
27608
  const id = parsePositiveInteger2(c.req.param("id"));
26489
27609
  if (id === null) return c.json({ error: "invalid id" }, 400);
@@ -26491,6 +27611,30 @@ var init_web = __esm({
26491
27611
  if (!template) return c.json({ error: "not found" }, 404);
26492
27612
  return c.json(template);
26493
27613
  });
27614
+ app.post("/api/templates", async (c) => {
27615
+ try {
27616
+ const input = parseTemplatePayload(await c.req.json());
27617
+ const template = await TaskTemplateService.create(input);
27618
+ return c.json({ success: true, template }, 201);
27619
+ } catch (error) {
27620
+ return c.json({
27621
+ error: error instanceof Error ? error.message : String(error)
27622
+ }, 400);
27623
+ }
27624
+ });
27625
+ app.put("/api/templates/:id", async (c) => {
27626
+ const id = parsePositiveInteger2(c.req.param("id"));
27627
+ if (id === null) return c.json({ error: "invalid id" }, 400);
27628
+ try {
27629
+ const input = parseTemplatePayload(await c.req.json());
27630
+ const template = await TaskTemplateService.update(id, input);
27631
+ return template ? c.json({ success: true, template }) : c.json({ error: "not found" }, 404);
27632
+ } catch (error) {
27633
+ return c.json({
27634
+ error: error instanceof Error ? error.message : String(error)
27635
+ }, 400);
27636
+ }
27637
+ });
26494
27638
  app.post("/api/tasks/:id/retry", async (c) => {
26495
27639
  const id = parsePositiveInteger2(c.req.param("id"));
26496
27640
  if (id === null) return c.json({ error: "invalid id" }, 400);
@@ -26539,22 +27683,31 @@ var init_web = __esm({
26539
27683
  app.post("/api/templates/:id/trigger", async (c) => {
26540
27684
  const id = parsePositiveInteger2(c.req.param("id"));
26541
27685
  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
27686
  const task = await triggerTaskFromTemplate(id);
26545
- if (!task) return c.json({ error: "maxInstances reached" }, 409);
26546
- return c.json({ success: true, taskId: task.id });
27687
+ return task ? c.json({ success: true, taskId: task.id }) : c.json({ error: "not found" }, 404);
26547
27688
  });
26548
27689
  app.put("/api/config", async (c) => {
26549
27690
  try {
26550
- const body = await c.req.json();
27691
+ const rawBody = await c.req.json();
27692
+ if (!rawBody || typeof rawBody !== "object" || Array.isArray(rawBody)) {
27693
+ throw new Error("\u8BF7\u6C42\u5185\u5BB9\u5FC5\u987B\u662F\u5BF9\u8C61");
27694
+ }
27695
+ const body = rawBody;
27696
+ const section = (name) => {
27697
+ const value = body[name];
27698
+ if (value === void 0) return {};
27699
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
27700
+ throw new Error(`${name} \u5FC5\u987B\u662F\u5BF9\u8C61`);
27701
+ }
27702
+ return value;
27703
+ };
26551
27704
  const current = readCurrentConfig();
26552
27705
  const currentWorker = current.worker ?? {};
26553
27706
  const currentScheduler = current.scheduler ?? {};
26554
27707
  const currentWatchdog = current.watchdog ?? {};
26555
- const bodyWorker = body.worker ?? {};
26556
- const bodyScheduler = body.scheduler ?? {};
26557
- const bodyWatchdog = body.watchdog ?? {};
27708
+ const bodyWorker = section("worker");
27709
+ const bodyScheduler = section("scheduler");
27710
+ const bodyWatchdog = section("watchdog");
26558
27711
  const merged = {
26559
27712
  ...current,
26560
27713
  ...body,
@@ -26563,8 +27716,13 @@ var init_web = __esm({
26563
27716
  scheduler: { ...currentScheduler, ...bodyScheduler },
26564
27717
  watchdog: { ...currentWatchdog, ...bodyWatchdog }
26565
27718
  };
26566
- writeConfig(validateConfig(merged));
26567
- return c.json({ success: true });
27719
+ const savedConfig = validateConfig(merged);
27720
+ writeConfig(savedConfig);
27721
+ return c.json({
27722
+ success: true,
27723
+ restartRequired: runtimeConfig === null || !configsEqual(savedConfig, runtimeConfig),
27724
+ managed: canRestartCurrentGateway()
27725
+ });
26568
27726
  } catch (error) {
26569
27727
  return c.json({
26570
27728
  success: false,
@@ -26572,9 +27730,27 @@ var init_web = __esm({
26572
27730
  }, 400);
26573
27731
  }
26574
27732
  });
27733
+ app.post("/api/gateway/restart", async (c) => {
27734
+ const rawBody = await c.req.json().catch(() => null);
27735
+ const confirmation = rawBody && typeof rawBody === "object" && !Array.isArray(rawBody) ? rawBody.confirmation : void 0;
27736
+ if (confirmation !== "RESTART") {
27737
+ return c.json({ error: "confirmation must be RESTART" }, 400);
27738
+ }
27739
+ if (restartScheduled) return c.json({ error: "restart already scheduled" }, 409);
27740
+ if (!canRestartCurrentGateway()) {
27741
+ 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);
27742
+ }
27743
+ restartScheduled = true;
27744
+ const previousPid = process.pid;
27745
+ setTimeout(() => {
27746
+ process.kill(previousPid, "SIGTERM");
27747
+ }, 500);
27748
+ return c.json({ success: true, previousPid }, 202);
27749
+ });
26575
27750
  app.post("/api/database/clear", async (c) => {
26576
- const body = await c.req.json().catch(() => ({}));
26577
- if (body.confirmation !== "CLEAR") {
27751
+ const rawBody = await c.req.json().catch(() => null);
27752
+ const confirmation = rawBody && typeof rawBody === "object" && !Array.isArray(rawBody) ? rawBody.confirmation : void 0;
27753
+ if (confirmation !== "CLEAR") {
26578
27754
  return c.json({ success: false, error: "confirmation must be CLEAR" }, 400);
26579
27755
  }
26580
27756
  try {
@@ -26824,8 +28000,9 @@ async function main() {
26824
28000
  await scheduler.start();
26825
28001
  if (shuttingDown) return;
26826
28002
  if (cfg.dashboard.enabled) {
26827
- const { dashboardApp: dashboardApp2 } = await Promise.resolve().then(() => (init_web(), web_exports));
28003
+ const { dashboardApp: dashboardApp2, setDashboardRuntimeConfig: setDashboardRuntimeConfig2 } = await Promise.resolve().then(() => (init_web(), web_exports));
26828
28004
  if (shuttingDown) return;
28005
+ setDashboardRuntimeConfig2(cfg);
26829
28006
  dashboardServer = Bun.serve({
26830
28007
  hostname: "127.0.0.1",
26831
28008
  port: cfg.dashboard.port,
@@ -26879,185 +28056,6 @@ var init_gateway = __esm({
26879
28056
  }
26880
28057
  });
26881
28058
 
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
28059
  // node_modules/commander/esm.mjs
27062
28060
  var import_index = __toESM(require_commander(), 1);
27063
28061
  var {
@@ -27081,37 +28079,7 @@ init_task_run_service();
27081
28079
  init_task_template_service();
27082
28080
  init_database_maintenance_service();
27083
28081
  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
28082
+ init_duration();
27115
28083
  init_pm2();
27116
28084
  init_config();
27117
28085
  import { spawnSync as spawnSync4 } from "child_process";
@@ -27245,6 +28213,7 @@ function parseTaskStatus(value) {
27245
28213
  }
27246
28214
 
27247
28215
  // src/cli/index.ts
28216
+ init_update2();
27248
28217
  async function withDb(fn, formatError = (error) => JSON.stringify({
27249
28218
  error: error instanceof Error ? error.message : String(error)
27250
28219
  })) {
@@ -27295,6 +28264,44 @@ program2.command("add").description("\u521B\u5EFA\u65B0\u4EFB\u52A1").requiredOp
27295
28264
  });
27296
28265
  console.log(JSON.stringify({ id: task.id, status: "created" }, null, 2));
27297
28266
  }));
28267
+ 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 () => {
28268
+ if (options.batch !== void 0 && options.clearBatch) {
28269
+ throw new Error("batch \u548C clear-batch \u4E0D\u80FD\u540C\u65F6\u4F7F\u7528");
28270
+ }
28271
+ if (options.timeout !== void 0 && options.clearTimeout) {
28272
+ throw new Error("timeout \u548C clear-timeout \u4E0D\u80FD\u540C\u65F6\u4F7F\u7528");
28273
+ }
28274
+ const update = {};
28275
+ for (const field of ["name", "agent", "model", "prompt", "category"]) {
28276
+ if (options[field] !== void 0) update[field] = options[field];
28277
+ }
28278
+ if (options.importance !== void 0) {
28279
+ update.importance = parseBoundedInteger(options.importance, "importance", 1, 5);
28280
+ }
28281
+ if (options.urgency !== void 0) {
28282
+ update.urgency = parseBoundedInteger(options.urgency, "urgency", 1, 5);
28283
+ }
28284
+ if (options.maxRetries !== void 0) {
28285
+ update.maxRetries = parseBoundedInteger(options.maxRetries, "max-retries", 0, 1e3);
28286
+ }
28287
+ if (options.batch !== void 0 || options.clearBatch) {
28288
+ update.batchId = options.clearBatch ? null : options.batch;
28289
+ }
28290
+ if (options.retryBackoff !== void 0) {
28291
+ const retryBackoffMs = parseDuration(options.retryBackoff);
28292
+ if (retryBackoffMs === null) throw new Error("retry-backoff \u683C\u5F0F\u65E0\u6548");
28293
+ update.retryBackoffMs = retryBackoffMs;
28294
+ }
28295
+ if (options.timeout !== void 0 || options.clearTimeout) {
28296
+ const timeoutMs = options.clearTimeout ? null : parseDuration(options.timeout);
28297
+ if (timeoutMs === null && !options.clearTimeout) throw new Error("timeout \u683C\u5F0F\u65E0\u6548");
28298
+ update.timeoutMs = timeoutMs;
28299
+ }
28300
+ const id = parsePositiveInteger(options.id, "id");
28301
+ const task = await TaskService.update(id, update, { cwd: process.cwd() });
28302
+ 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`);
28303
+ console.log(JSON.stringify({ id: task.id, status: task.status, updated: true }, null, 2));
28304
+ }));
27298
28305
  program2.command("next").description("\u83B7\u53D6\u4E0B\u4E00\u4E2A\u5F85\u6267\u884C\u7684\u4EFB\u52A1").action(async () => withDb(async () => {
27299
28306
  const task = await TaskService.next({ cwd: process.cwd() });
27300
28307
  if (task) {
@@ -27379,7 +28386,7 @@ program2.command("delete").description("\u5220\u9664\u4EFB\u52A1").requiredOptio
27379
28386
  console.log(JSON.stringify({ deleted, id }));
27380
28387
  }));
27381
28388
  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 () => {
28389
+ 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
28390
  let intervalMs = null;
27384
28391
  let runAt = null;
27385
28392
  const retryBackoffMs = parseDuration(options.retryBackoff);
@@ -27540,6 +28547,7 @@ program2.command("doctor").description("\u68C0\u67E5 OpenCode\u3001\u6570\u636E\
27540
28547
  config.watchdog.heartbeatTimeoutMs
27541
28548
  );
27542
28549
  const gateway = getGatewayDiagnostic();
28550
+ const packageVersion = getPackageVersion();
27543
28551
  const opencodeBin2 = process.env.SUPERTASK_OPENCODE_BIN ?? "opencode";
27544
28552
  const opencodeResult = spawnSync4(opencodeBin2, ["--version"], {
27545
28553
  encoding: "utf8",
@@ -27551,6 +28559,7 @@ program2.command("doctor").description("\u68C0\u67E5 OpenCode\u3001\u6570\u636E\
27551
28559
  version: opencodeResult.status === 0 ? opencodeResult.stdout.trim() : null,
27552
28560
  error: opencodeResult.status === 0 ? null : opencodeResult.error?.message || opencodeResult.stderr.trim() || `\u9000\u51FA\u7801 ${opencodeResult.status}`
27553
28561
  };
28562
+ const plugin = getOpenCodePluginDiagnostic();
27554
28563
  let dashboard = {
27555
28564
  enabled: config.dashboard.enabled,
27556
28565
  ok: !config.dashboard.enabled,
@@ -27570,6 +28579,26 @@ program2.command("doctor").description("\u68C0\u67E5 OpenCode\u3001\u6570\u636E\
27570
28579
  }
27571
28580
  }
27572
28581
  const warnings = [];
28582
+ const gatewayEntryPinned = gateway.gatewayEntry === null || !/[\\/]opencode-supertask@(latest|next)[\\/]/.test(gateway.gatewayEntry);
28583
+ const gatewayVersionMatchesPackage = gateway.gatewayPackageVersion !== null && gateway.runningVersion === gateway.gatewayPackageVersion;
28584
+ const configuredVersionsMatch = plugin.ok && plugin.version === gateway.gatewayPackageVersion;
28585
+ if (!plugin.ok && plugin.error) {
28586
+ warnings.push(plugin.error);
28587
+ }
28588
+ if (plugin.version !== null && plugin.version !== packageVersion) {
28589
+ 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`);
28590
+ }
28591
+ if (!gatewayEntryPinned) {
28592
+ warnings.push(`PM2 Gateway \u4ECD\u4ECE\u6D6E\u52A8\u7F13\u5B58\u8DEF\u5F84\u542F\u52A8\uFF1A${gateway.gatewayEntry}`);
28593
+ }
28594
+ if (gateway.processFound && gateway.gatewayPackageVersion === null) {
28595
+ warnings.push(`\u65E0\u6CD5\u4ECE PM2 Gateway \u5165\u53E3\u786E\u8BA4 opencode-supertask \u5305\u7248\u672C\uFF1A${gateway.gatewayEntry ?? "unknown"}`);
28596
+ } else if (gateway.processFound && !gatewayVersionMatchesPackage) {
28597
+ warnings.push(`Gateway ready \u9501\u7248\u672C ${gateway.runningVersion ?? "unknown"} \u4E0E\u5165\u53E3\u5305\u7248\u672C ${gateway.gatewayPackageVersion ?? "unknown"} \u4E0D\u4E00\u81F4`);
28598
+ }
28599
+ if (plugin.version !== null && gateway.gatewayPackageVersion !== null && plugin.version !== gateway.gatewayPackageVersion) {
28600
+ warnings.push(`OpenCode \u63D2\u4EF6 v${plugin.version} \u4E0E PM2 Gateway v${gateway.gatewayPackageVersion} \u4E0D\u4E00\u81F4\uFF1B\u6267\u884C supertask upgrade`);
28601
+ }
27573
28602
  if (gateway.pm2Installed && !gateway.logRotationInstalled) {
27574
28603
  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
28604
  }
@@ -27587,12 +28616,13 @@ program2.command("doctor").description("\u68C0\u67E5 OpenCode\u3001\u6570\u636E\
27587
28616
  `\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
28617
  );
27589
28618
  }
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;
28619
+ 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
28620
  const report = {
27592
28621
  ok,
27593
- packageVersion: getPackageVersion(),
28622
+ packageVersion,
27594
28623
  configPath: getConfigPath(),
27595
28624
  opencode,
28625
+ plugin,
27596
28626
  database,
27597
28627
  legacyQuarantinedRuns,
27598
28628
  gateway,
@@ -27606,8 +28636,9 @@ program2.command("doctor").description("\u68C0\u67E5 OpenCode\u3001\u6570\u636E\
27606
28636
  const mark = (value) => value ? "\u2713" : "\u2717";
27607
28637
  console.log(`SuperTask doctor: ${ok ? "\u6B63\u5E38" : "\u5F02\u5E38"}`);
27608
28638
  console.log(`${mark(opencode.ok)} OpenCode ${opencode.version ?? opencode.error ?? "\u4E0D\u53EF\u7528"}`);
28639
+ 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
28640
  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}` : ""}`);
28641
+ 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
28642
  console.log(`${mark(dashboard.ok)} Dashboard ${dashboard.enabled ? dashboard.url : "\u5DF2\u7981\u7528"}`);
27612
28643
  for (const warning of warnings) console.log(`! ${warning}`);
27613
28644
  }
@@ -27663,6 +28694,9 @@ program2.command("upgrade").description("Update OpenCode plugin cache and restar
27663
28694
  console.log(`
27664
28695
  SuperTask upgraded: ${result.before ?? "unknown"} \u2192 ${result.after}`);
27665
28696
  console.log("Gateway restarted. Please restart opencode to load the new plugin.");
28697
+ if (getPackageVersion() !== installed.version) {
28698
+ 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).`);
28699
+ }
27666
28700
  } catch (err) {
27667
28701
  let detail = err instanceof Error ? err.message : String(err);
27668
28702
  try {