opencode-supertask 0.1.31 → 0.1.32-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1204,7 +1204,7 @@ var init_sql = __esm({
1204
1204
  return new SQL([new StringChunk(str)]);
1205
1205
  }
1206
1206
  sql2.raw = raw2;
1207
- function join5(chunks, separator) {
1207
+ function join6(chunks, separator) {
1208
1208
  const result = [];
1209
1209
  for (const [i, chunk] of chunks.entries()) {
1210
1210
  if (i > 0 && separator !== void 0) {
@@ -1214,7 +1214,7 @@ var init_sql = __esm({
1214
1214
  }
1215
1215
  return new SQL(result);
1216
1216
  }
1217
- sql2.join = join5;
1217
+ sql2.join = join6;
1218
1218
  function identifier(value) {
1219
1219
  return new Name(value);
1220
1220
  }
@@ -3830,7 +3830,7 @@ var init_select2 = __esm({
3830
3830
  return (table, on) => {
3831
3831
  const baseTableName = this.tableName;
3832
3832
  const tableName = getTableLikeName(table);
3833
- if (typeof tableName === "string" && this.config.joins?.some((join5) => join5.alias === tableName)) {
3833
+ if (typeof tableName === "string" && this.config.joins?.some((join6) => join6.alias === tableName)) {
3834
3834
  throw new Error(`Alias "${tableName}" is already used in this query`);
3835
3835
  }
3836
3836
  if (!this.isPartialSelect) {
@@ -4672,7 +4672,7 @@ var init_update = __esm({
4672
4672
  createJoin(joinType) {
4673
4673
  return (table, on) => {
4674
4674
  const tableName = getTableLikeName(table);
4675
- if (typeof tableName === "string" && this.config.joins.some((join5) => join5.alias === tableName)) {
4675
+ if (typeof tableName === "string" && this.config.joins.some((join6) => join6.alias === tableName)) {
4676
4676
  throw new Error(`Alias "${tableName}" is already used in this query`);
4677
4677
  }
4678
4678
  if (typeof on === "function") {
@@ -6107,8 +6107,8 @@ import { homedir } from "os";
6107
6107
  import { join, dirname } from "path";
6108
6108
  import { fileURLToPath } from "url";
6109
6109
  function getMigrationsFolder() {
6110
- const __dirname = dirname(fileURLToPath(import.meta.url));
6111
- let dir = __dirname;
6110
+ const __dirname2 = dirname(fileURLToPath(import.meta.url));
6111
+ let dir = __dirname2;
6112
6112
  for (let i = 0; i < 5; i++) {
6113
6113
  const candidate = join(dir, "drizzle", "meta", "_journal.json");
6114
6114
  if (existsSync(candidate)) {
@@ -6116,7 +6116,7 @@ function getMigrationsFolder() {
6116
6116
  }
6117
6117
  dir = dirname(dir);
6118
6118
  }
6119
- return join(__dirname, "../../drizzle");
6119
+ return join(__dirname2, "../../drizzle");
6120
6120
  }
6121
6121
  function ensureGatewayLock(sqliteDb) {
6122
6122
  sqliteDb.exec(`
@@ -6391,6 +6391,54 @@ var init_backoff = __esm({
6391
6391
  }
6392
6392
  });
6393
6393
 
6394
+ // src/core/task-working-directory.ts
6395
+ import { statSync } from "fs";
6396
+ import { isAbsolute } from "path";
6397
+ function validateTaskWorkingDirectory(cwd) {
6398
+ if (cwd == null) return;
6399
+ if (!cwd.trim()) {
6400
+ throw new InvalidTaskWorkingDirectoryError("\u4EFB\u52A1\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u80FD\u4E3A\u7A7A");
6401
+ }
6402
+ if (!isAbsolute(cwd)) {
6403
+ throw new InvalidTaskWorkingDirectoryError(`\u4EFB\u52A1\u5DE5\u4F5C\u76EE\u5F55\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84\uFF1A${cwd}`);
6404
+ }
6405
+ let stat;
6406
+ try {
6407
+ stat = statSync(cwd);
6408
+ } catch (error) {
6409
+ const detail = error instanceof Error ? error.message : String(error);
6410
+ throw new InvalidTaskWorkingDirectoryError(`\u4EFB\u52A1\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u65E0\u6CD5\u8BBF\u95EE\uFF1A${cwd}\uFF08${detail}\uFF09`);
6411
+ }
6412
+ if (!stat.isDirectory()) {
6413
+ throw new InvalidTaskWorkingDirectoryError(`\u4EFB\u52A1\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u662F\u76EE\u5F55\uFF1A${cwd}`);
6414
+ }
6415
+ }
6416
+ var InvalidTaskWorkingDirectoryError;
6417
+ var init_task_working_directory = __esm({
6418
+ "src/core/task-working-directory.ts"() {
6419
+ "use strict";
6420
+ InvalidTaskWorkingDirectoryError = class extends Error {
6421
+ constructor(message) {
6422
+ super(message);
6423
+ this.name = "InvalidTaskWorkingDirectoryError";
6424
+ }
6425
+ };
6426
+ }
6427
+ });
6428
+
6429
+ // src/core/task-batch.ts
6430
+ function normalizeTaskBatchId(batchId) {
6431
+ if (batchId == null) return batchId;
6432
+ return batchId.trim() || null;
6433
+ }
6434
+ var TASK_BATCH_TRIM_CHARACTERS;
6435
+ var init_task_batch = __esm({
6436
+ "src/core/task-batch.ts"() {
6437
+ "use strict";
6438
+ 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";
6439
+ }
6440
+ });
6441
+
6394
6442
  // src/core/services/task.service.ts
6395
6443
  function hasNoExecutableDependents() {
6396
6444
  return sql`NOT EXISTS (
@@ -6444,6 +6492,8 @@ var init_task_service = __esm({
6444
6492
  init_db2();
6445
6493
  init_drizzle_orm();
6446
6494
  init_backoff();
6495
+ init_task_working_directory();
6496
+ init_task_batch();
6447
6497
  ({ tasks: tasks2, taskRuns: taskRuns2 } = schema_exports);
6448
6498
  cleanupInvocation = 0;
6449
6499
  TaskDeletionConflictError = class extends Error {
@@ -6461,34 +6511,75 @@ var init_task_service = __esm({
6461
6511
  return conditions;
6462
6512
  }
6463
6513
  static async add(data) {
6464
- this.validateNewTask(data);
6514
+ const normalizedData = {
6515
+ ...data,
6516
+ batchId: normalizeTaskBatchId(data.batchId)
6517
+ };
6518
+ this.validateNewTask(normalizedData);
6465
6519
  return db.transaction((tx) => {
6466
- if (data.dependsOn != null) {
6520
+ if (normalizedData.dependsOn != null) {
6467
6521
  const dependency = tx.select({
6468
6522
  id: tasks2.id,
6469
6523
  cwd: tasks2.cwd,
6470
6524
  status: tasks2.status,
6471
6525
  retryCount: tasks2.retryCount,
6472
6526
  maxRetries: tasks2.maxRetries
6473
- }).from(tasks2).where(eq(tasks2.id, data.dependsOn)).get();
6527
+ }).from(tasks2).where(eq(tasks2.id, normalizedData.dependsOn)).get();
6474
6528
  if (!dependency) {
6475
- throw new Error(`dependsOn \u6307\u5411\u7684\u4EFB\u52A1 #${data.dependsOn} \u4E0D\u5B58\u5728`);
6529
+ throw new Error(`dependsOn \u6307\u5411\u7684\u4EFB\u52A1 #${normalizedData.dependsOn} \u4E0D\u5B58\u5728`);
6476
6530
  }
6477
- if ((dependency.cwd ?? null) !== (data.cwd ?? null)) {
6531
+ if ((dependency.cwd ?? null) !== (normalizedData.cwd ?? null)) {
6478
6532
  throw new Error("dependsOn \u5FC5\u987B\u6307\u5411\u540C\u4E00 cwd \u7684\u4EFB\u52A1");
6479
6533
  }
6480
6534
  const dependencyIsRecoverable = dependency.status === "pending" || dependency.status === "running" || dependency.status === "done" || dependency.status === "failed" && (dependency.retryCount ?? 0) <= (dependency.maxRetries ?? 3);
6481
6535
  if (!dependencyIsRecoverable) {
6482
- throw new Error(`dependsOn \u6307\u5411\u7684\u4EFB\u52A1 #${data.dependsOn} \u5DF2\u8FDB\u5165\u4E0D\u53EF\u6062\u590D\u7EC8\u6001`);
6536
+ throw new Error(`dependsOn \u6307\u5411\u7684\u4EFB\u52A1 #${normalizedData.dependsOn} \u5DF2\u8FDB\u5165\u4E0D\u53EF\u6062\u590D\u7EC8\u6001`);
6483
6537
  }
6484
6538
  }
6485
- return tx.insert(tasks2).values(data).returning().get();
6539
+ return tx.insert(tasks2).values(normalizedData).returning().get();
6540
+ }, { behavior: "immediate" });
6541
+ }
6542
+ static async update(id, data, scope = {}) {
6543
+ if (Object.keys(data).length === 0) throw new Error("\u81F3\u5C11\u63D0\u4F9B\u4E00\u4E2A\u8981\u4FEE\u6539\u7684\u5B57\u6BB5");
6544
+ const normalizedData = data.batchId === void 0 ? data : { ...data, batchId: normalizeTaskBatchId(data.batchId) ?? null };
6545
+ return db.transaction((tx) => {
6546
+ const task = tx.select().from(tasks2).where(and(
6547
+ eq(tasks2.id, id),
6548
+ sql`${tasks2.status} IN ('pending', 'failed', 'dead_letter')`,
6549
+ ...this.buildScopeWhere(scope)
6550
+ )).get();
6551
+ if (!task) return null;
6552
+ this.validateNewTask({
6553
+ name: normalizedData.name ?? task.name,
6554
+ agent: normalizedData.agent ?? task.agent,
6555
+ model: normalizedData.model ?? task.model,
6556
+ prompt: normalizedData.prompt ?? task.prompt,
6557
+ cwd: task.cwd,
6558
+ category: normalizedData.category ?? task.category,
6559
+ importance: normalizedData.importance ?? task.importance,
6560
+ urgency: normalizedData.urgency ?? task.urgency,
6561
+ batchId: normalizedData.batchId === void 0 ? task.batchId : normalizedData.batchId,
6562
+ maxRetries: normalizedData.maxRetries ?? task.maxRetries,
6563
+ retryBackoffMs: normalizedData.retryBackoffMs ?? task.retryBackoffMs,
6564
+ timeoutMs: normalizedData.timeoutMs === void 0 ? task.timeoutMs : normalizedData.timeoutMs,
6565
+ dependsOn: task.dependsOn
6566
+ });
6567
+ const maxRetries = normalizedData.maxRetries ?? task.maxRetries ?? 3;
6568
+ const exhausted = task.status === "failed" && (task.retryCount ?? 0) > maxRetries;
6569
+ return tx.update(tasks2).set({
6570
+ ...normalizedData,
6571
+ ...exhausted ? {
6572
+ status: "dead_letter",
6573
+ retryAfter: null
6574
+ } : {}
6575
+ }).where(eq(tasks2.id, id)).returning().get() ?? null;
6486
6576
  }, { behavior: "immediate" });
6487
6577
  }
6488
6578
  static validateNewTask(data) {
6489
6579
  if (!data.name.trim()) throw new Error("name \u4E0D\u80FD\u4E3A\u7A7A");
6490
6580
  if (!data.agent.trim()) throw new Error("agent \u4E0D\u80FD\u4E3A\u7A7A");
6491
6581
  if (!data.prompt.trim()) throw new Error("prompt \u4E0D\u80FD\u4E3A\u7A7A");
6582
+ validateTaskWorkingDirectory(data.cwd);
6492
6583
  this.validateInteger("importance", data.importance, 1, 5);
6493
6584
  this.validateInteger("urgency", data.urgency, 1, 5);
6494
6585
  this.validateInteger("maxRetries", data.maxRetries, 0, 1e3);
@@ -6509,12 +6600,14 @@ var init_task_service = __esm({
6509
6600
  isNull(tasks2.retryAfter),
6510
6601
  sql`${tasks2.retryAfter} <= ${nowMs}`
6511
6602
  );
6512
- const hasExcludedBatches = scope.excludedBatchIds && scope.excludedBatchIds.length > 0;
6603
+ const excludedBatchIds = [...new Set((scope.excludedBatchIds ?? []).map((batchId) => normalizeTaskBatchId(batchId)).filter((batchId) => Boolean(batchId)))];
6604
+ const hasExcludedBatches = excludedBatchIds.length > 0;
6513
6605
  let batchFilter;
6514
6606
  if (hasExcludedBatches) {
6515
6607
  batchFilter = or(
6516
6608
  isNull(tasks2.batchId),
6517
- sql`${tasks2.batchId} NOT IN ${scope.excludedBatchIds}`
6609
+ sql`trim(${tasks2.batchId}, ${TASK_BATCH_TRIM_CHARACTERS}) = ''`,
6610
+ sql`trim(${tasks2.batchId}, ${TASK_BATCH_TRIM_CHARACTERS}) NOT IN ${excludedBatchIds}`
6518
6611
  );
6519
6612
  }
6520
6613
  const statusConditions = or(
@@ -6548,9 +6641,11 @@ var init_task_service = __esm({
6548
6641
  ),
6549
6642
  or(
6550
6643
  isNull(tasks2.batchId),
6644
+ sql`trim(${tasks2.batchId}, ${TASK_BATCH_TRIM_CHARACTERS}) = ''`,
6551
6645
  sql`NOT EXISTS (
6552
6646
  SELECT 1 FROM tasks AS running_batch_task
6553
- WHERE running_batch_task.batch_id = ${tasks2.batchId}
6647
+ WHERE trim(running_batch_task.batch_id, ${TASK_BATCH_TRIM_CHARACTERS})
6648
+ = trim(${tasks2.batchId}, ${TASK_BATCH_TRIM_CHARACTERS})
6554
6649
  AND (
6555
6650
  running_batch_task.status = 'running'
6556
6651
  OR EXISTS (
@@ -6574,12 +6669,18 @@ var init_task_service = __esm({
6574
6669
  ).limit(1);
6575
6670
  return result[0] ?? null;
6576
6671
  }
6577
- static async countRunning() {
6672
+ static async countRunning(scope = {}) {
6673
+ const scopeConditions = scope.legacyCwd ? [sql`${tasks2.cwd} IS NULL OR trim(${tasks2.cwd}) = ''`] : this.buildScopeWhere(scope);
6674
+ if (scope.batchId !== void 0) {
6675
+ const batchId = normalizeTaskBatchId(scope.batchId);
6676
+ scopeConditions.push(batchId ? sql`trim(${tasks2.batchId}, ${TASK_BATCH_TRIM_CHARACTERS}) = ${batchId}` : sql`0`);
6677
+ }
6578
6678
  return db.transaction((tx) => {
6579
- const runningTasks = tx.select({ count: sql`count(*)` }).from(tasks2).where(eq(tasks2.status, "running")).get();
6679
+ const runningTasks = tx.select({ count: sql`count(*)` }).from(tasks2).where(and(eq(tasks2.status, "running"), ...scopeConditions)).get();
6580
6680
  const runsWithoutRunningTask = tx.select({ count: sql`count(DISTINCT ${taskRuns2.taskId})` }).from(taskRuns2).innerJoin(tasks2, eq(tasks2.id, taskRuns2.taskId)).where(and(
6581
6681
  eq(taskRuns2.status, "running"),
6582
- sql`${tasks2.status} <> 'running'`
6682
+ sql`${tasks2.status} <> 'running'`,
6683
+ ...scopeConditions
6583
6684
  )).get();
6584
6685
  return Number(runningTasks?.count ?? 0) + Number(runsWithoutRunningTask?.count ?? 0);
6585
6686
  }, { behavior: "deferred" });
@@ -6912,15 +7013,17 @@ var init_task_service = __esm({
6912
7013
  }).where(and(...conditions, hasViableDependency())).returning().get() ?? null, { behavior: "immediate" });
6913
7014
  }
6914
7015
  static async retryBatch(batchId, scope = {}) {
7016
+ const normalizedBatchId = normalizeTaskBatchId(batchId);
7017
+ if (!normalizedBatchId) return 0;
6915
7018
  const sqlite2 = getSqlite();
6916
7019
  const scopeFilter = scope.cwd === void 0 ? "" : "AND candidate.cwd = ?";
6917
- const parameters = scope.cwd === void 0 ? [batchId] : [batchId, scope.cwd];
7020
+ const parameters = scope.cwd === void 0 ? [TASK_BATCH_TRIM_CHARACTERS, normalizedBatchId] : [TASK_BATCH_TRIM_CHARACTERS, normalizedBatchId, scope.cwd];
6918
7021
  return db.transaction(() => sqlite2.query(`
6919
7022
  WITH RECURSIVE
6920
7023
  candidate(id, cwd, depends_on) AS MATERIALIZED (
6921
7024
  SELECT candidate.id, candidate.cwd, candidate.depends_on
6922
7025
  FROM tasks AS candidate
6923
- WHERE candidate.batch_id = ?
7026
+ WHERE trim(candidate.batch_id, ?) = ?
6924
7027
  AND candidate.status IN ('failed', 'dead_letter')
6925
7028
  ${scopeFilter}
6926
7029
  ),
@@ -6974,16 +7077,28 @@ var init_task_service = __esm({
6974
7077
  static async list(options = {}) {
6975
7078
  let query = db.select().from(tasks2).$dynamic();
6976
7079
  const conditions = [];
6977
- if (options.status) {
7080
+ if (options.activeExecution) {
7081
+ conditions.push(or(
7082
+ eq(tasks2.status, "running"),
7083
+ sql`EXISTS (
7084
+ SELECT 1 FROM task_runs AS active_list_run
7085
+ WHERE active_list_run.task_id = ${tasks2.id}
7086
+ AND active_list_run.status = 'running'
7087
+ )`
7088
+ ));
7089
+ } else if (options.status) {
6978
7090
  conditions.push(eq(tasks2.status, options.status));
6979
7091
  }
6980
- if (options.batchId) {
6981
- conditions.push(eq(tasks2.batchId, options.batchId));
7092
+ if (options.batchId !== void 0) {
7093
+ const batchId = normalizeTaskBatchId(options.batchId);
7094
+ conditions.push(batchId ? sql`trim(${tasks2.batchId}, ${TASK_BATCH_TRIM_CHARACTERS}) = ${batchId}` : sql`0`);
6982
7095
  }
6983
7096
  if (options.category) {
6984
7097
  conditions.push(eq(tasks2.category, options.category));
6985
7098
  }
6986
- if (options.cwd !== void 0) {
7099
+ if (options.legacyCwd) {
7100
+ conditions.push(sql`${tasks2.cwd} IS NULL OR trim(${tasks2.cwd}) = ''`);
7101
+ } else if (options.cwd !== void 0) {
6987
7102
  conditions.push(eq(tasks2.cwd, options.cwd));
6988
7103
  }
6989
7104
  if (conditions.length > 0) {
@@ -7001,9 +7116,12 @@ var init_task_service = __esm({
7001
7116
  static async stats(options = {}) {
7002
7117
  const conditions = [];
7003
7118
  if (options.batchId !== void 0) {
7004
- conditions.push(eq(tasks2.batchId, options.batchId));
7119
+ const batchId = normalizeTaskBatchId(options.batchId);
7120
+ conditions.push(batchId ? sql`trim(${tasks2.batchId}, ${TASK_BATCH_TRIM_CHARACTERS}) = ${batchId}` : sql`0`);
7005
7121
  }
7006
- if (options.cwd !== void 0) {
7122
+ if (options.legacyCwd) {
7123
+ conditions.push(sql`${tasks2.cwd} IS NULL OR trim(${tasks2.cwd}) = ''`);
7124
+ } else if (options.cwd !== void 0) {
7007
7125
  conditions.push(eq(tasks2.cwd, options.cwd));
7008
7126
  }
7009
7127
  const whereCondition = conditions.length > 0 ? and(...conditions) : void 0;
@@ -7028,6 +7146,33 @@ var init_task_service = __esm({
7028
7146
  }
7029
7147
  return stats;
7030
7148
  }
7149
+ static async projectSummaries(limit = 100) {
7150
+ this.validateInteger("limit", limit, 1, 1e3);
7151
+ const lastCreatedAt = sql`max(${tasks2.createdAt})`;
7152
+ const lastTaskId = sql`max(${tasks2.id})`;
7153
+ const rows = await db.select({
7154
+ cwd: tasks2.cwd,
7155
+ total: sql`count(*)`,
7156
+ pending: sql`sum(CASE WHEN ${tasks2.status} = 'pending' THEN 1 ELSE 0 END)`,
7157
+ running: sql`sum(CASE WHEN ${tasks2.status} = 'running' OR EXISTS (
7158
+ SELECT 1 FROM task_runs AS active_project_run
7159
+ WHERE active_project_run.task_id = ${tasks2.id}
7160
+ AND active_project_run.status = 'running'
7161
+ ) THEN 1 ELSE 0 END)`,
7162
+ failed: sql`sum(CASE WHEN ${tasks2.status} IN ('failed', 'dead_letter') THEN 1 ELSE 0 END)`,
7163
+ done: sql`sum(CASE WHEN ${tasks2.status} = 'done' THEN 1 ELSE 0 END)`,
7164
+ lastCreatedAt
7165
+ }).from(tasks2).where(sql`${tasks2.cwd} IS NOT NULL AND trim(${tasks2.cwd}) <> ''`).groupBy(tasks2.cwd).orderBy(desc(lastCreatedAt), desc(lastTaskId)).limit(limit);
7166
+ return rows.flatMap((row) => row.cwd === null ? [] : [{
7167
+ cwd: row.cwd,
7168
+ total: Number(row.total),
7169
+ pending: Number(row.pending),
7170
+ running: Number(row.running),
7171
+ failed: Number(row.failed),
7172
+ done: Number(row.done),
7173
+ lastCreatedAt: row.lastCreatedAt === null ? null : Number(row.lastCreatedAt) * 1e3
7174
+ }]);
7175
+ }
7031
7176
  static async delete(id, scope = {}) {
7032
7177
  const conditions = [
7033
7178
  eq(tasks2.id, id),
@@ -7161,12 +7306,15 @@ var init_task_service = __esm({
7161
7306
  function isLaunchIdentity(value) {
7162
7307
  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);
7163
7308
  }
7309
+ function drainProofAckForIdentity(launchIdentity) {
7310
+ return { type: DRAIN_PROOF_ACK_MESSAGE_TYPE, identity: launchIdentity };
7311
+ }
7164
7312
  function isMatchingDrainProof(message, launchIdentity) {
7165
7313
  if (typeof message !== "object" || message == null) return false;
7166
7314
  const candidate = message;
7167
7315
  return candidate.type === DRAIN_PROOF_MESSAGE_TYPE && candidate.identity === launchIdentity;
7168
7316
  }
7169
- var LEGACY_GUARDIAN_LAUNCH_PROTOCOL, TOKEN_GUARDIAN_LAUNCH_PROTOCOL, LAUNCH_IDENTITY_ARGUMENT, DRAIN_PROOF_MESSAGE_TYPE, MANAGED_RUN_ENV, MANAGED_RUN_ENV_VALUE;
7317
+ 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;
7170
7318
  var init_launch_protocol = __esm({
7171
7319
  "src/core/launch-protocol.ts"() {
7172
7320
  "use strict";
@@ -7174,6 +7322,7 @@ var init_launch_protocol = __esm({
7174
7322
  TOKEN_GUARDIAN_LAUNCH_PROTOCOL = "gated-v3-token-guardian";
7175
7323
  LAUNCH_IDENTITY_ARGUMENT = "--supertask-launch-identity";
7176
7324
  DRAIN_PROOF_MESSAGE_TYPE = "supertask-drained";
7325
+ DRAIN_PROOF_ACK_MESSAGE_TYPE = "supertask-drained-ack";
7177
7326
  MANAGED_RUN_ENV = "SUPERTASK_MANAGED_RUN";
7178
7327
  MANAGED_RUN_ENV_VALUE = "1";
7179
7328
  }
@@ -16760,8 +16909,8 @@ var require_CronFileParser = __commonJS({
16760
16909
  * @throws If file cannot be read
16761
16910
  */
16762
16911
  static parseFileSync(filePath) {
16763
- const { readFileSync: readFileSync4 } = __require("fs");
16764
- const data = readFileSync4(filePath, "utf8");
16912
+ const { readFileSync: readFileSync5 } = __require("fs");
16913
+ const data = readFileSync5(filePath, "utf8");
16765
16914
  return _CronFileParser.#parseContent(data);
16766
16915
  }
16767
16916
  /**
@@ -16894,27 +17043,34 @@ var init_task_template_service = __esm({
16894
17043
  init_db2();
16895
17044
  init_drizzle_orm();
16896
17045
  init_cron_parser();
17046
+ init_task_working_directory();
17047
+ init_task_batch();
16897
17048
  ({ taskTemplates: taskTemplates2 } = schema_exports);
16898
17049
  TaskTemplateService = class {
16899
17050
  static async create(data) {
16900
- this.validate(data);
17051
+ const normalizedData = {
17052
+ ...data,
17053
+ batchId: normalizeTaskBatchId(data.batchId)
17054
+ };
17055
+ this.validate(normalizedData);
16901
17056
  const now = Date.now();
16902
- const nextRunAt = data.nextRunAt ?? this.calculateNextRunAt(
16903
- data.scheduleType,
17057
+ const nextRunAt = normalizedData.nextRunAt ?? this.calculateNextRunAt(
17058
+ normalizedData.scheduleType,
16904
17059
  {
16905
- cronExpr: data.cronExpr ?? null,
16906
- intervalMs: data.intervalMs ?? null,
16907
- runAt: data.runAt ?? null
17060
+ cronExpr: normalizedData.cronExpr ?? null,
17061
+ intervalMs: normalizedData.intervalMs ?? null,
17062
+ runAt: normalizedData.runAt ?? null
16908
17063
  },
16909
17064
  now
16910
17065
  );
16911
- const result = await db.insert(taskTemplates2).values({ ...data, nextRunAt, createdAt: now, updatedAt: now }).returning();
17066
+ const result = await db.insert(taskTemplates2).values({ ...normalizedData, nextRunAt, createdAt: now, updatedAt: now }).returning();
16912
17067
  return result[0];
16913
17068
  }
16914
17069
  static validate(data) {
16915
17070
  if (!data.name.trim()) throw new Error("name \u4E0D\u80FD\u4E3A\u7A7A");
16916
17071
  if (!data.agent.trim()) throw new Error("agent \u4E0D\u80FD\u4E3A\u7A7A");
16917
17072
  if (!data.prompt.trim()) throw new Error("prompt \u4E0D\u80FD\u4E3A\u7A7A");
17073
+ validateTaskWorkingDirectory(data.cwd);
16918
17074
  const scheduleType = data.scheduleType;
16919
17075
  if (!["cron", "delayed", "recurring"].includes(scheduleType)) {
16920
17076
  throw new Error("scheduleType \u5FC5\u987B\u662F cron\u3001delayed \u6216 recurring");
@@ -16941,13 +17097,40 @@ var init_task_template_service = __esm({
16941
17097
  throw new Error(`${name} \u5FC5\u987B\u662F ${min} \u5230 ${max} \u4E4B\u95F4\u7684\u6574\u6570`);
16942
17098
  }
16943
17099
  }
16944
- static async list(limit = 50) {
16945
- return await db.select().from(taskTemplates2).orderBy(desc(taskTemplates2.createdAt), desc(taskTemplates2.id)).limit(limit);
17100
+ static async list(limit = 50, offset = 0) {
17101
+ return await db.select().from(taskTemplates2).orderBy(desc(taskTemplates2.createdAt), desc(taskTemplates2.id)).limit(limit).offset(offset);
17102
+ }
17103
+ static async stats() {
17104
+ const result = await db.select({
17105
+ total: sql`count(*)`,
17106
+ enabled: sql`sum(case when ${taskTemplates2.enabled} = 1 then 1 else 0 end)`
17107
+ }).from(taskTemplates2);
17108
+ const total = Number(result[0]?.total ?? 0);
17109
+ const enabled = Number(result[0]?.enabled ?? 0);
17110
+ return { total, enabled, disabled: total - enabled };
16946
17111
  }
16947
17112
  static async getById(id) {
16948
17113
  const result = await db.select().from(taskTemplates2).where(eq(taskTemplates2.id, id));
16949
17114
  return result[0] || null;
16950
17115
  }
17116
+ static async update(id, data) {
17117
+ const normalizedData = {
17118
+ ...data,
17119
+ batchId: normalizeTaskBatchId(data.batchId) ?? null
17120
+ };
17121
+ this.validate(normalizedData);
17122
+ const now = Date.now();
17123
+ const nextRunAt = this.calculateNextRunAt(
17124
+ normalizedData.scheduleType,
17125
+ normalizedData,
17126
+ now
17127
+ );
17128
+ return db.transaction((tx) => {
17129
+ const existing = tx.select({ id: taskTemplates2.id }).from(taskTemplates2).where(eq(taskTemplates2.id, id)).limit(1).get();
17130
+ if (!existing) return null;
17131
+ return tx.update(taskTemplates2).set({ ...normalizedData, nextRunAt, updatedAt: now }).where(eq(taskTemplates2.id, id)).returning().get() ?? null;
17132
+ }, { behavior: "immediate" });
17133
+ }
16951
17134
  static async enable(id) {
16952
17135
  return db.transaction((tx) => {
16953
17136
  const template = tx.select().from(taskTemplates2).where(eq(taskTemplates2.id, id)).limit(1).get();
@@ -17034,8 +17217,11 @@ var init_task_template_service = __esm({
17034
17217
  });
17035
17218
 
17036
17219
  // src/gateway/scheduler/job-templates.ts
17037
- async function cloneTaskFromTemplate(templateId) {
17038
- return createTaskFromTemplate(templateId, { advanceSchedule: true });
17220
+ async function cloneTaskFromTemplate(templateId, expectedNextRunAt) {
17221
+ return createTaskFromTemplate(templateId, {
17222
+ advanceSchedule: true,
17223
+ expectedNextRunAt
17224
+ });
17039
17225
  }
17040
17226
  async function triggerTaskFromTemplate(templateId) {
17041
17227
  return createTaskFromTemplate(templateId, {
@@ -17048,37 +17234,40 @@ function createTaskFromTemplate(templateId, options) {
17048
17234
  return db.transaction((tx) => {
17049
17235
  const tmpl = tx.select().from(taskTemplates3).where(eq(taskTemplates3.id, templateId)).limit(1).get();
17050
17236
  if (!tmpl || options.advanceSchedule && !tmpl.enabled) return null;
17051
- const activeTasks = tx.select({ count: sql`count(*)` }).from(schema_exports.tasks).where(and(
17052
- eq(schema_exports.tasks.templateId, templateId),
17053
- sql`${schema_exports.tasks.status} IN ('pending', 'running')`
17054
- )).get();
17055
- const retryableTasks = tx.select({ count: sql`count(*)` }).from(schema_exports.tasks).where(and(
17056
- eq(schema_exports.tasks.templateId, templateId),
17057
- eq(schema_exports.tasks.status, "failed"),
17058
- sql`${schema_exports.tasks.retryCount} <= ${schema_exports.tasks.maxRetries}`
17059
- )).get();
17060
- 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(
17061
- eq(schema_exports.taskRuns.status, "running"),
17062
- eq(schema_exports.tasks.templateId, templateId),
17063
- sql`NOT (
17064
- ${schema_exports.tasks.status} IN ('pending', 'running')
17065
- OR (
17066
- ${schema_exports.tasks.status} = 'failed'
17067
- AND ${schema_exports.tasks.retryCount} <= ${schema_exports.tasks.maxRetries}
17068
- )
17069
- )`
17070
- )).get();
17071
- const activeCount = Number(activeTasks?.count ?? 0) + Number(retryableTasks?.count ?? 0) + Number(detachedRuns?.count ?? 0);
17072
- if (activeCount >= (tmpl.maxInstances ?? 1)) {
17073
- if (options.advanceSchedule && tmpl.scheduleType !== "delayed") {
17074
- const nextRunAt2 = TaskTemplateService.calculateNextRunAt(
17075
- tmpl.scheduleType,
17076
- tmpl,
17077
- nowMs
17078
- );
17079
- tx.update(taskTemplates3).set({ nextRunAt: nextRunAt2, updatedAt: nowMs }).where(and(eq(taskTemplates3.id, templateId), eq(taskTemplates3.enabled, true))).run();
17237
+ if (options.advanceSchedule && options.expectedNextRunAt !== void 0 && tmpl.nextRunAt !== options.expectedNextRunAt) return null;
17238
+ if (options.advanceSchedule) {
17239
+ const activeTasks = tx.select({ count: sql`count(*)` }).from(schema_exports.tasks).where(and(
17240
+ eq(schema_exports.tasks.templateId, templateId),
17241
+ sql`${schema_exports.tasks.status} IN ('pending', 'running')`
17242
+ )).get();
17243
+ const retryableTasks = tx.select({ count: sql`count(*)` }).from(schema_exports.tasks).where(and(
17244
+ eq(schema_exports.tasks.templateId, templateId),
17245
+ eq(schema_exports.tasks.status, "failed"),
17246
+ sql`${schema_exports.tasks.retryCount} <= ${schema_exports.tasks.maxRetries}`
17247
+ )).get();
17248
+ 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(
17249
+ eq(schema_exports.taskRuns.status, "running"),
17250
+ eq(schema_exports.tasks.templateId, templateId),
17251
+ sql`NOT (
17252
+ ${schema_exports.tasks.status} IN ('pending', 'running')
17253
+ OR (
17254
+ ${schema_exports.tasks.status} = 'failed'
17255
+ AND ${schema_exports.tasks.retryCount} <= ${schema_exports.tasks.maxRetries}
17256
+ )
17257
+ )`
17258
+ )).get();
17259
+ const activeCount = Number(activeTasks?.count ?? 0) + Number(retryableTasks?.count ?? 0) + Number(detachedRuns?.count ?? 0);
17260
+ if (activeCount >= (tmpl.maxInstances ?? 1)) {
17261
+ if (tmpl.scheduleType !== "delayed") {
17262
+ const nextRunAt2 = TaskTemplateService.calculateNextRunAt(
17263
+ tmpl.scheduleType,
17264
+ tmpl,
17265
+ nowMs
17266
+ );
17267
+ tx.update(taskTemplates3).set({ nextRunAt: nextRunAt2, updatedAt: nowMs }).where(and(eq(taskTemplates3.id, templateId), eq(taskTemplates3.enabled, true))).run();
17268
+ }
17269
+ return null;
17080
17270
  }
17081
- return null;
17082
17271
  }
17083
17272
  const isDelayed = tmpl.scheduleType === "delayed";
17084
17273
  const nextRunAt = isDelayed ? null : TaskTemplateService.calculateNextRunAt(
@@ -17095,7 +17284,7 @@ function createTaskFromTemplate(templateId, options) {
17095
17284
  category: tmpl.category ?? "general",
17096
17285
  importance: tmpl.importance ?? 3,
17097
17286
  urgency: tmpl.urgency ?? 3,
17098
- batchId: tmpl.batchId,
17287
+ batchId: normalizeTaskBatchId(tmpl.batchId),
17099
17288
  maxRetries: tmpl.maxRetries ?? 3,
17100
17289
  retryBackoffMs: tmpl.retryBackoffMs ?? 3e4,
17101
17290
  timeoutMs: tmpl.timeoutMs,
@@ -17156,11 +17345,38 @@ var init_job_templates = __esm({
17156
17345
  init_db2();
17157
17346
  init_drizzle_orm();
17158
17347
  init_task_template_service();
17348
+ init_task_batch();
17159
17349
  ({ taskTemplates: taskTemplates3 } = schema_exports);
17160
17350
  DUE_TEMPLATE_BATCH_SIZE = 100;
17161
17351
  }
17162
17352
  });
17163
17353
 
17354
+ // src/core/package-version.ts
17355
+ import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
17356
+ import { dirname as dirname4, join as join4 } from "path";
17357
+ import { fileURLToPath as fileURLToPath4 } from "url";
17358
+ function getPackageVersion() {
17359
+ let directory = dirname4(fileURLToPath4(import.meta.url));
17360
+ for (let depth = 0; depth < 5; depth += 1) {
17361
+ const packagePath = join4(directory, "package.json");
17362
+ if (existsSync4(packagePath)) {
17363
+ try {
17364
+ const pkg = JSON.parse(readFileSync2(packagePath, "utf8"));
17365
+ if (typeof pkg.version === "string") return pkg.version;
17366
+ } catch {
17367
+ return "0.0.0";
17368
+ }
17369
+ }
17370
+ directory = dirname4(directory);
17371
+ }
17372
+ return "0.0.0";
17373
+ }
17374
+ var init_package_version = __esm({
17375
+ "src/core/package-version.ts"() {
17376
+ "use strict";
17377
+ }
17378
+ });
17379
+
17164
17380
  // node_modules/hono/dist/compose.js
17165
17381
  var compose;
17166
17382
  var init_compose = __esm({
@@ -19377,6 +19593,41 @@ var init_dist = __esm({
19377
19593
  }
19378
19594
  });
19379
19595
 
19596
+ // src/core/duration.ts
19597
+ function parseDuration(input) {
19598
+ const trimmed = input.trim();
19599
+ const simple = DURATION_REGEX.exec(trimmed);
19600
+ if (simple) {
19601
+ const value = parseFloat(simple[1]);
19602
+ const unit = simple[2].toLowerCase();
19603
+ if (unit === "ms") return value;
19604
+ if (unit === "s" || unit === "sec" || unit === "second" || unit === "seconds") return value * 1e3;
19605
+ if (unit === "min" || unit === "minute" || unit === "minutes" || unit === "m") return value * 6e4;
19606
+ if (unit === "h" || unit === "hour" || unit === "hours") return value * 36e5;
19607
+ if (unit === "d" || unit === "day" || unit === "days") return value * 864e5;
19608
+ if (unit === "w" || unit === "week" || unit === "weeks") return value * 6048e5;
19609
+ }
19610
+ const iso = ISO8601_REGEX.exec(trimmed);
19611
+ if (iso) {
19612
+ const days = parseFloat(iso[1] ?? "0");
19613
+ const hours = parseFloat(iso[2] ?? "0");
19614
+ const minutes = parseFloat(iso[3] ?? "0");
19615
+ const seconds = parseFloat(iso[4] ?? "0");
19616
+ return (days * 86400 + hours * 3600 + minutes * 60 + seconds) * 1e3;
19617
+ }
19618
+ const asNumber = Number(trimmed);
19619
+ if (!isNaN(asNumber) && asNumber > 0) return asNumber;
19620
+ return null;
19621
+ }
19622
+ var DURATION_REGEX, ISO8601_REGEX;
19623
+ var init_duration = __esm({
19624
+ "src/core/duration.ts"() {
19625
+ "use strict";
19626
+ DURATION_REGEX = /^(\d+(?:\.\d+)?)\s*(ms|s|sec|seconds?|min|minutes?|m|h|hours?|d|days?|w|weeks?)$/i;
19627
+ ISO8601_REGEX = /^P(?:([.\d]+)D)?(?:T(?:([.\d]+)H)?(?:([.\d]+)M)?(?:([.\d]+)S)?)?$/i;
19628
+ }
19629
+ });
19630
+
19380
19631
  // src/core/services/database-maintenance.service.ts
19381
19632
  import { Database as Database3 } from "bun:sqlite";
19382
19633
  import { randomUUID as randomUUID2 } from "crypto";
@@ -19385,7 +19636,7 @@ import {
19385
19636
  existsSync as existsSync5,
19386
19637
  mkdirSync as mkdirSync2,
19387
19638
  renameSync,
19388
- statSync,
19639
+ statSync as statSync2,
19389
19640
  unlinkSync,
19390
19641
  writeFileSync
19391
19642
  } from "fs";
@@ -19487,9 +19738,9 @@ var init_database_maintenance_service = __esm({
19487
19738
  if (!existsSync5(source)) {
19488
19739
  throw new Error(`\u5907\u4EFD\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${source}`);
19489
19740
  }
19490
- const sourceStat = statSync(source);
19741
+ const sourceStat = statSync2(source);
19491
19742
  if (!sourceStat.isFile()) throw new Error(`\u5907\u4EFD\u8DEF\u5F84\u4E0D\u662F\u6587\u4EF6\uFF1A${source}`);
19492
- const liveStat = statSync(livePath);
19743
+ const liveStat = statSync2(livePath);
19493
19744
  if (sourceStat.dev === liveStat.dev && sourceStat.ino === liveStat.ino) {
19494
19745
  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");
19495
19746
  }
@@ -19730,7 +19981,7 @@ var init_database_maintenance_service = __esm({
19730
19981
  const counts = missingTables.length === 0 ? this.readCounts(sqlite2) : { tasks: 0, taskRuns: 0, taskTemplates: 0 };
19731
19982
  const runningTasks = missingTables.includes("tasks") ? 0 : this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM tasks WHERE status = 'running'");
19732
19983
  const runningRuns = missingTables.includes("task_runs") ? 0 : this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM task_runs WHERE status = 'running'");
19733
- const sizeBytes = path === ":memory:" || !existsSync5(path) ? sqlite2.serialize().byteLength : statSync(path).size;
19984
+ const sizeBytes = path === ":memory:" || !existsSync5(path) ? sqlite2.serialize().byteLength : statSync2(path).size;
19734
19985
  return {
19735
19986
  ok: integrityMessages.length === 1 && integrityMessages[0] === "ok" && foreignKeyViolations === 0 && missingTables.length === 0,
19736
19987
  path: normalizedPath(path),
@@ -19767,7 +20018,7 @@ var init_database_maintenance_service = __esm({
19767
20018
  const check = this.inspectFile(temporary);
19768
20019
  if (!check.ok) throw new Error("\u65B0\u5907\u4EFD\u672A\u901A\u8FC7\u5B8C\u6574\u6027\u6821\u9A8C");
19769
20020
  renameSync(temporary, output);
19770
- const finalCheck = { ...check, path: output, sizeBytes: statSync(output).size };
20021
+ const finalCheck = { ...check, path: output, sizeBytes: statSync2(output).size };
19771
20022
  return { path: output, sizeBytes: finalCheck.sizeBytes, check: finalCheck };
19772
20023
  } catch (error) {
19773
20024
  safeUnlink(temporary);
@@ -19813,6 +20064,445 @@ var init_database_maintenance_service = __esm({
19813
20064
  }
19814
20065
  });
19815
20066
 
20067
+ // src/daemon/management-lock.ts
20068
+ import { Database as Database4 } from "bun:sqlite";
20069
+ var init_management_lock = __esm({
20070
+ "src/daemon/management-lock.ts"() {
20071
+ "use strict";
20072
+ }
20073
+ });
20074
+
20075
+ // src/daemon/pm2.ts
20076
+ import { execSync, spawnSync as spawnSync2 } from "child_process";
20077
+ import {
20078
+ accessSync,
20079
+ chmodSync as chmodSync2,
20080
+ constants,
20081
+ existsSync as existsSync6,
20082
+ mkdirSync as mkdirSync3,
20083
+ readFileSync as readFileSync3,
20084
+ rmSync,
20085
+ statSync as statSync3,
20086
+ writeFileSync as writeFileSync2
20087
+ } from "fs";
20088
+ import { homedir as homedir3, userInfo } from "os";
20089
+ import { delimiter, dirname as dirname6, isAbsolute as isAbsolute2, join as join5, resolve as resolve3 } from "path";
20090
+ import { fileURLToPath as fileURLToPath5 } from "url";
20091
+ import { Database as Database5 } from "bun:sqlite";
20092
+ function runtimeHome(env) {
20093
+ return resolve3(env.HOME || homedir3());
20094
+ }
20095
+ function runtimePath(value, cwd) {
20096
+ return resolve3(cwd, value);
20097
+ }
20098
+ function versionFile(env = process.env, cwd = process.cwd()) {
20099
+ return env.SUPERTASK_VERSION_FILE ? runtimePath(env.SUPERTASK_VERSION_FILE, cwd) : join5(runtimeHome(env), ".local/share/opencode/supertask-gateway-version");
20100
+ }
20101
+ function getRunningVersion(env = process.env, cwd = process.cwd()) {
20102
+ try {
20103
+ const path = versionFile(env, cwd);
20104
+ if (!existsSync6(path)) return null;
20105
+ return readFileSync3(path, "utf-8").trim() || null;
20106
+ } catch {
20107
+ return null;
20108
+ }
20109
+ }
20110
+ function packageVersionFromGatewayEntry(gatewayEntry) {
20111
+ if (gatewayEntry === null) return null;
20112
+ let directory = dirname6(gatewayEntry);
20113
+ for (let depth = 0; depth < 6; depth += 1) {
20114
+ const packagePath = join5(directory, "package.json");
20115
+ if (existsSync6(packagePath)) {
20116
+ try {
20117
+ const pkg = JSON.parse(readFileSync3(packagePath, "utf8"));
20118
+ if (pkg.name === "opencode-supertask" && typeof pkg.version === "string") {
20119
+ return pkg.version;
20120
+ }
20121
+ } catch {
20122
+ }
20123
+ }
20124
+ const parent = dirname6(directory);
20125
+ if (parent === directory) break;
20126
+ directory = parent;
20127
+ }
20128
+ return null;
20129
+ }
20130
+ function resolveRuntimeExecutable(command, env, cwd) {
20131
+ if (isAbsolute2(command) || command.includes("/")) return runtimePath(command, cwd);
20132
+ for (const entry of (env.PATH ?? "").split(delimiter)) {
20133
+ if (!entry) continue;
20134
+ const candidate = resolve3(cwd, entry, command);
20135
+ try {
20136
+ accessSync(candidate, constants.X_OK);
20137
+ return candidate;
20138
+ } catch {
20139
+ }
20140
+ }
20141
+ return command;
20142
+ }
20143
+ function runtimeScope(runtime) {
20144
+ const { cwd, env } = runtime;
20145
+ const home = runtimeHome(env);
20146
+ return {
20147
+ cwd: resolve3(cwd),
20148
+ databasePath: env.SUPERTASK_DB_PATH ? runtimePath(env.SUPERTASK_DB_PATH, cwd) : join5(home, ".local/share/opencode/tasks.db"),
20149
+ configPath: env.SUPERTASK_CONFIG_PATH ? runtimePath(env.SUPERTASK_CONFIG_PATH, cwd) : join5(home, ".config/opencode/supertask.json"),
20150
+ opencodePath: resolveRuntimeExecutable(env.SUPERTASK_OPENCODE_BIN ?? "opencode", env, cwd),
20151
+ home,
20152
+ pm2Home: env.PM2_HOME ? runtimePath(env.PM2_HOME, cwd) : join5(home, ".pm2"),
20153
+ managementLockPath: canonicalManagementLockPath(env, cwd)
20154
+ };
20155
+ }
20156
+ function scopesMatch(left, right) {
20157
+ return left.databasePath === right.databasePath && left.configPath === right.configPath && left.opencodePath === right.opencodePath && left.home === right.home && left.pm2Home === right.pm2Home && left.managementLockPath === right.managementLockPath;
20158
+ }
20159
+ function currentScope() {
20160
+ return runtimeScope({ cwd: process.cwd(), env: process.env });
20161
+ }
20162
+ function getGatewayDiagnostic() {
20163
+ const producerScope = currentScope();
20164
+ if (!isPm2Installed()) {
20165
+ return {
20166
+ pm2Installed: false,
20167
+ processFound: false,
20168
+ status: null,
20169
+ pid: null,
20170
+ ready: false,
20171
+ runningVersion: getRunningVersion(),
20172
+ gatewayEntry: null,
20173
+ gatewayPackageVersion: null,
20174
+ logRotationInstalled: false,
20175
+ startupConfigured: process.platform === "darwin" || process.platform === "linux" ? false : null,
20176
+ currentScope: producerScope,
20177
+ gatewayScope: null,
20178
+ scopeMatches: false
20179
+ };
20180
+ }
20181
+ const processes = pm2JsonList();
20182
+ const gateway = processes.find((item) => item.name === PROCESS_NAME);
20183
+ const runtime = gatewayRuntimeFromProcess(gateway);
20184
+ const gatewayEnv = runtime?.env ?? process.env;
20185
+ const managedScope = runtime ? runtimeScope(runtime) : null;
20186
+ const pid = typeof gateway?.pid === "number" ? gateway.pid : null;
20187
+ const readyPath = managedScope?.databasePath ?? databasePath();
20188
+ const lockedVersion = pid == null ? void 0 : gatewayVersionFromLock(pid, readyPath);
20189
+ const gatewayEntry = runtime?.gatewayEntry ?? null;
20190
+ return {
20191
+ pm2Installed: true,
20192
+ processFound: gateway != null,
20193
+ status: gateway?.pm2_env?.status ?? null,
20194
+ pid,
20195
+ ready: pid != null && isGatewayReady(pid, readyPath),
20196
+ runningVersion: lockedVersion === void 0 ? getRunningVersion(gatewayEnv, runtime?.cwd) : lockedVersion,
20197
+ gatewayEntry,
20198
+ gatewayPackageVersion: packageVersionFromGatewayEntry(gatewayEntry),
20199
+ logRotationInstalled: processes.some((item) => item.name === "pm2-logrotate" && item.pm2_env?.status === "online"),
20200
+ startupConfigured: process.platform === "darwin" ? isMacLaunchAgentConfigured(
20201
+ gatewayEnv.PM2_HOME ?? join5(runtimeHome(gatewayEnv), ".pm2"),
20202
+ runtime ?? void 0
20203
+ ) : process.platform === "linux" ? isLinuxStartupConfigured(gatewayEnv, runtime?.cwd, runtime ?? void 0) : null,
20204
+ currentScope: producerScope,
20205
+ gatewayScope: managedScope,
20206
+ scopeMatches: managedScope != null && scopesMatch(producerScope, managedScope)
20207
+ };
20208
+ }
20209
+ function pm2Bin(env = process.env) {
20210
+ return env.SUPERTASK_PM2_BIN ?? (process.platform === "win32" ? "pm2.cmd" : "pm2");
20211
+ }
20212
+ function pm2CommandTimeoutMs(env = process.env) {
20213
+ const configured = Number(env.SUPERTASK_PM2_COMMAND_TIMEOUT_MS ?? 15e3);
20214
+ return Number.isFinite(configured) && configured > 0 ? configured : 15e3;
20215
+ }
20216
+ function resolvePm2Bin() {
20217
+ const configured = pm2Bin();
20218
+ if (isAbsolute2(configured)) return configured;
20219
+ const result = spawnSync2("which", [configured], { encoding: "utf8" });
20220
+ const resolved = result.status === 0 ? result.stdout.trim().split("\n")[0] : "";
20221
+ if (!resolved) throw new Error(`[supertask] \u65E0\u6CD5\u89E3\u6790 pm2 \u53EF\u6267\u884C\u6587\u4EF6: ${configured}`);
20222
+ return resolved;
20223
+ }
20224
+ function launchAgentPath() {
20225
+ return process.env.SUPERTASK_LAUNCH_AGENT_PATH ?? join5(runtimeHome(process.env), "Library/LaunchAgents", `${MAC_LAUNCH_AGENT_LABEL}.plist`);
20226
+ }
20227
+ function launchctlBin() {
20228
+ return process.env.SUPERTASK_LAUNCHCTL_BIN ?? "launchctl";
20229
+ }
20230
+ function xmlUnescape(value) {
20231
+ return value.replaceAll("&apos;", "'").replaceAll("&quot;", '"').replaceAll("&gt;", ">").replaceAll("&lt;", "<").replaceAll("&amp;", "&");
20232
+ }
20233
+ function plistValue(contents, key) {
20234
+ const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
20235
+ const match2 = contents.match(new RegExp(`<key>\\s*${escapedKey}\\s*</key>\\s*<string>([^<]*)</string>`));
20236
+ return match2?.[1] ? xmlUnescape(match2[1]) : null;
20237
+ }
20238
+ function isMacLaunchAgentConfigured(expectedPm2Home, expectedRuntime) {
20239
+ if (typeof process.getuid !== "function") return false;
20240
+ const plistPath = launchAgentPath();
20241
+ if (!existsSync6(plistPath)) return false;
20242
+ try {
20243
+ const contents = readFileSync3(plistPath, "utf8");
20244
+ const argumentsBlock = contents.match(
20245
+ /<key>\s*ProgramArguments\s*<\/key>\s*<array>([\s\S]*?)<\/array>/
20246
+ )?.[1];
20247
+ const programArguments = argumentsBlock ? [...argumentsBlock.matchAll(/<string>([^<]*)<\/string>/g)].map((match2) => xmlUnescape(match2[1])) : [];
20248
+ const bunPath = programArguments[0];
20249
+ const supervisorEntry = programArguments[1];
20250
+ const pm2Path = programArguments[2];
20251
+ const pm2Home = plistValue(contents, "PM2_HOME");
20252
+ const managementLock = plistValue(contents, "SUPERTASK_PM2_MANAGEMENT_LOCK");
20253
+ if (!bunPath || !supervisorEntry || !pm2Path || !pm2Home || !managementLock) return false;
20254
+ if (expectedPm2Home && pm2Home !== expectedPm2Home) return false;
20255
+ const expectedManagementLock = expectedRuntime ? managementLockPath(expectedRuntime.env, expectedRuntime.cwd) : process.env.SUPERTASK_PM2_MANAGEMENT_LOCK ? managementLockPath() : join5(expectedPm2Home ?? currentScope().pm2Home, "supertask-gateway.manage.sqlite");
20256
+ if (managementLock !== expectedManagementLock) return false;
20257
+ accessSync(bunPath, constants.X_OK);
20258
+ accessSync(supervisorEntry, constants.R_OK);
20259
+ accessSync(pm2Path, constants.X_OK);
20260
+ if (spawnSync2(bunPath, ["--version"], {
20261
+ stdio: "ignore",
20262
+ timeout: pm2CommandTimeoutMs(),
20263
+ killSignal: "SIGKILL"
20264
+ }).status !== 0) return false;
20265
+ if (spawnSync2(pm2Path, ["--version"], {
20266
+ stdio: "ignore",
20267
+ env: { ...process.env, PM2_HOME: pm2Home },
20268
+ timeout: pm2CommandTimeoutMs(),
20269
+ killSignal: "SIGKILL"
20270
+ }).status !== 0) return false;
20271
+ const loaded = spawnSync2(
20272
+ launchctlBin(),
20273
+ ["print", `gui/${process.getuid()}/${MAC_LAUNCH_AGENT_LABEL}`],
20274
+ {
20275
+ encoding: "utf8",
20276
+ stdio: ["ignore", "pipe", "ignore"],
20277
+ timeout: pm2CommandTimeoutMs(),
20278
+ killSignal: "SIGKILL"
20279
+ }
20280
+ );
20281
+ if (loaded.status !== 0) return false;
20282
+ if (!loaded.stdout.includes("state = running")) return false;
20283
+ if (!loaded.stdout.includes(`path = ${plistPath}`)) return false;
20284
+ if (!loaded.stdout.includes(`program = ${bunPath}`)) return false;
20285
+ const dumpPath = join5(pm2Home, "dump.pm2");
20286
+ const dump = JSON.parse(readFileSync3(dumpPath, "utf8"));
20287
+ if (!Array.isArray(dump)) return false;
20288
+ const gateway = dump.find((item) => item.name === PROCESS_NAME);
20289
+ const dumpRuntime = gatewayRuntimeFromProcess(gateway);
20290
+ if (!dumpRuntime || !hasRestorableSavedGatewayRuntime(gateway)) return false;
20291
+ return expectedRuntime === void 0 || dumpRuntime.gatewayEntry === expectedRuntime.gatewayEntry && dumpRuntime.bunPath === expectedRuntime.bunPath && dumpRuntime.cwd === expectedRuntime.cwd && scopesMatch(runtimeScope(dumpRuntime), runtimeScope(expectedRuntime));
20292
+ } catch {
20293
+ return false;
20294
+ }
20295
+ }
20296
+ function systemctlBin(env = process.env) {
20297
+ return env.SUPERTASK_SYSTEMCTL_BIN ?? "systemctl";
20298
+ }
20299
+ function isLinuxStartupConfigured(env = process.env, cwd = process.cwd(), expectedRuntime) {
20300
+ const unit = env.SUPERTASK_PM2_SYSTEMD_UNIT ?? `pm2-${userInfo().username}.service`;
20301
+ const enabled = spawnSync2(systemctlBin(env), ["is-enabled", unit], {
20302
+ encoding: "utf8",
20303
+ env,
20304
+ stdio: ["ignore", "pipe", "ignore"],
20305
+ timeout: pm2CommandTimeoutMs(env),
20306
+ killSignal: "SIGKILL"
20307
+ });
20308
+ if (enabled.status !== 0 || enabled.stdout.trim() !== "enabled") return false;
20309
+ const contents = spawnSync2(systemctlBin(env), ["cat", unit], {
20310
+ encoding: "utf8",
20311
+ env,
20312
+ stdio: ["ignore", "pipe", "ignore"],
20313
+ timeout: pm2CommandTimeoutMs(env),
20314
+ killSignal: "SIGKILL"
20315
+ });
20316
+ if (contents.status !== 0) return false;
20317
+ const expectedPm2Home = env.PM2_HOME ?? join5(runtimeHome(env), ".pm2");
20318
+ if (!contents.stdout.includes("resurrect") || !contents.stdout.includes(expectedPm2Home)) {
20319
+ return false;
20320
+ }
20321
+ try {
20322
+ const dump = JSON.parse(readFileSync3(join5(expectedPm2Home, "dump.pm2"), "utf8"));
20323
+ if (!Array.isArray(dump)) return false;
20324
+ const gateway = dump.find((item) => item.name === PROCESS_NAME);
20325
+ const runtime = gatewayRuntimeFromProcess(gateway);
20326
+ if (!runtime || !hasRestorableSavedGatewayRuntime(gateway)) return false;
20327
+ return runtime.cwd === resolve3(cwd) && scopesMatch(runtimeScope(runtime), runtimeScope({ cwd, env })) && (expectedRuntime === void 0 || runtime.gatewayEntry === expectedRuntime.gatewayEntry && runtime.bunPath === expectedRuntime.bunPath);
20328
+ } catch {
20329
+ return false;
20330
+ }
20331
+ }
20332
+ function isPm2Installed(env = process.env) {
20333
+ const result = spawnSync2(pm2Bin(env), ["--version"], {
20334
+ stdio: "ignore",
20335
+ env,
20336
+ shell: process.platform === "win32",
20337
+ timeout: pm2CommandTimeoutMs(env),
20338
+ killSignal: "SIGKILL"
20339
+ });
20340
+ return result.status === 0;
20341
+ }
20342
+ function resolveCommand(command) {
20343
+ const lookup = process.platform === "win32" ? "where" : "which";
20344
+ const result = spawnSync2(lookup, [command], { encoding: "utf8" });
20345
+ return result.status === 0 ? result.stdout.trim().split("\n")[0] || null : null;
20346
+ }
20347
+ function npmPreferredEnvironment() {
20348
+ if (process.platform === "win32") return null;
20349
+ const npm = resolveCommand("npm");
20350
+ const node = resolveCommand("node");
20351
+ if (!npm || !node) return null;
20352
+ const command = resolvePm2Bin();
20353
+ const path = [.../* @__PURE__ */ new Set([
20354
+ dirname6(command),
20355
+ dirname6(npm),
20356
+ dirname6(node),
20357
+ "/usr/local/bin",
20358
+ "/usr/bin",
20359
+ "/bin"
20360
+ ])].join(delimiter);
20361
+ return { command, env: { ...process.env, PATH: path } };
20362
+ }
20363
+ function pm2Exec(args, options = {}) {
20364
+ const npmEnvironment = options.preferNpm ? npmPreferredEnvironment() : null;
20365
+ const effectiveEnv = options.env ?? npmEnvironment?.env ?? process.env;
20366
+ const result = spawnSync2(npmEnvironment?.command ?? pm2Bin(effectiveEnv), args, {
20367
+ stdio: ["pipe", "pipe", "pipe"],
20368
+ encoding: "utf-8",
20369
+ env: effectiveEnv,
20370
+ shell: process.platform === "win32",
20371
+ timeout: options.timeoutMs ?? pm2CommandTimeoutMs(effectiveEnv),
20372
+ killSignal: "SIGKILL"
20373
+ });
20374
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
20375
+ if (result.error) return { ok: false, output: result.error.message };
20376
+ return { ok: result.status === 0, output };
20377
+ }
20378
+ function requirePm2(args, action, options = {}) {
20379
+ const result = pm2Exec(args, options);
20380
+ if (!result.ok) throw new Error(`[supertask] ${action} failed: ${result.output || "unknown pm2 error"}`);
20381
+ return result.output;
20382
+ }
20383
+ function pm2JsonList(env = process.env) {
20384
+ const output = requirePm2(["jlist"], "pm2 jlist", { env });
20385
+ try {
20386
+ const parsed = JSON.parse(output);
20387
+ if (!Array.isArray(parsed)) throw new Error("result is not an array");
20388
+ return parsed;
20389
+ } catch (error) {
20390
+ const message = error instanceof Error ? error.message : String(error);
20391
+ throw new Error(`[supertask] Invalid pm2 jlist output: ${message}`);
20392
+ }
20393
+ }
20394
+ function gatewayEntryFromProcess(processInfo) {
20395
+ const args = processInfo?.pm2_env?.args ?? processInfo?.args;
20396
+ const candidates = Array.isArray(args) ? [...args].reverse() : typeof args === "string" ? [args] : [];
20397
+ const savedCwd = processInfo?.pm2_env?.pm_cwd ?? processInfo?.pm_cwd;
20398
+ for (const candidate of candidates) {
20399
+ const path = typeof savedCwd === "string" ? runtimePath(candidate, savedCwd) : candidate;
20400
+ if (existsSync6(path)) return resolve3(path);
20401
+ }
20402
+ return null;
20403
+ }
20404
+ function gatewayEnvironmentFromProcess(processInfo) {
20405
+ const saved = processInfo?.pm2_env?.env ?? processInfo?.env;
20406
+ if (!saved) return { ...process.env };
20407
+ const env = {};
20408
+ for (const [key, value] of Object.entries(saved)) {
20409
+ if (typeof value === "string") env[key] = value;
20410
+ }
20411
+ return env;
20412
+ }
20413
+ function gatewayRuntimeFromProcess(processInfo) {
20414
+ const gatewayEntry = gatewayEntryFromProcess(processInfo);
20415
+ if (!gatewayEntry) return null;
20416
+ const savedBunPath = processInfo?.pm2_env?.pm_exec_path ?? processInfo?.pm_exec_path;
20417
+ const savedCwd = processInfo?.pm2_env?.pm_cwd ?? processInfo?.pm_cwd;
20418
+ const savedEnv = gatewayEnvironmentFromProcess(processInfo);
20419
+ if (typeof savedBunPath !== "string" || typeof savedCwd !== "string") return null;
20420
+ try {
20421
+ accessSync(savedBunPath, constants.X_OK);
20422
+ if (!statSync3(savedCwd).isDirectory()) return null;
20423
+ if (spawnSync2(savedBunPath, ["--version"], {
20424
+ stdio: "ignore",
20425
+ env: savedEnv,
20426
+ timeout: pm2CommandTimeoutMs(savedEnv),
20427
+ killSignal: "SIGKILL"
20428
+ }).status !== 0) return null;
20429
+ } catch {
20430
+ return null;
20431
+ }
20432
+ const killTimeout = processInfo?.pm2_env?.kill_timeout ?? processInfo?.kill_timeout;
20433
+ return {
20434
+ gatewayEntry,
20435
+ bunPath: savedBunPath,
20436
+ cwd: resolve3(savedCwd),
20437
+ env: savedEnv,
20438
+ killTimeoutMs: Number.isInteger(killTimeout) && killTimeout >= 5e3 ? killTimeout : void 0
20439
+ };
20440
+ }
20441
+ function hasRestorableSavedGatewayRuntime(processInfo) {
20442
+ return gatewayRuntimeFromProcess(processInfo) !== null;
20443
+ }
20444
+ function databasePath(env = process.env, cwd = process.cwd()) {
20445
+ return env.SUPERTASK_DB_PATH ? runtimePath(env.SUPERTASK_DB_PATH, cwd) : join5(runtimeHome(env), ".local/share/opencode/tasks.db");
20446
+ }
20447
+ function isGatewayReady(expectedPid, path = databasePath()) {
20448
+ if (!existsSync6(path)) return false;
20449
+ let database = null;
20450
+ try {
20451
+ database = new Database5(path, { readonly: true });
20452
+ const row = database.query(
20453
+ "SELECT pid, heartbeat_at, ready_at FROM gateway_lock WHERE id = 1"
20454
+ ).get();
20455
+ if (!row || row.ready_at == null) return false;
20456
+ if (expectedPid !== void 0 && row.pid !== expectedPid) return false;
20457
+ const ageMs = Date.now() - row.heartbeat_at;
20458
+ return ageMs >= -5e3 && ageMs < GATEWAY_LOCK_STALE_MS;
20459
+ } catch {
20460
+ return false;
20461
+ } finally {
20462
+ database?.close();
20463
+ }
20464
+ }
20465
+ function gatewayVersionFromLock(expectedPid, path) {
20466
+ if (!existsSync6(path)) return void 0;
20467
+ let database = null;
20468
+ try {
20469
+ database = new Database5(path, { readonly: true });
20470
+ const row = database.query(
20471
+ "SELECT pid, version FROM gateway_lock WHERE id = 1"
20472
+ ).get();
20473
+ if (!row || row.pid !== expectedPid) return void 0;
20474
+ return row.version;
20475
+ } catch {
20476
+ return void 0;
20477
+ } finally {
20478
+ database?.close();
20479
+ }
20480
+ }
20481
+ function managementLockPath(env = process.env, cwd = process.cwd()) {
20482
+ const override = env.SUPERTASK_PM2_MANAGEMENT_LOCK;
20483
+ if (override) return runtimePath(override, cwd);
20484
+ return join5(runtimeScope({ env, cwd }).pm2Home, "supertask-gateway.manage.sqlite");
20485
+ }
20486
+ function canonicalManagementLockPath(env = process.env, cwd = process.cwd()) {
20487
+ const home = runtimeHome(env);
20488
+ const pm2Home = env.PM2_HOME ? runtimePath(env.PM2_HOME, cwd) : join5(home, ".pm2");
20489
+ return join5(pm2Home, "supertask-gateway.manage.sqlite");
20490
+ }
20491
+ var __dirname, PROCESS_NAME, MAC_LAUNCH_AGENT_LABEL, GATEWAY_LOCK_STALE_MS;
20492
+ var init_pm2 = __esm({
20493
+ "src/daemon/pm2.ts"() {
20494
+ "use strict";
20495
+ init_config();
20496
+ init_package_version();
20497
+ init_management_lock();
20498
+ init_package_version();
20499
+ __dirname = dirname6(fileURLToPath5(import.meta.url));
20500
+ PROCESS_NAME = "supertask-gateway";
20501
+ MAC_LAUNCH_AGENT_LABEL = "com.supertask.pm2-resurrect";
20502
+ GATEWAY_LOCK_STALE_MS = 3e4;
20503
+ }
20504
+ });
20505
+
19816
20506
  // src/web/ui.ts
19817
20507
  function t(locale, key, values = {}) {
19818
20508
  const template = (locale === "en" ? EN : ZH)[key];
@@ -19822,6 +20512,14 @@ function statusText(locale, status) {
19822
20512
  const key = `status.${status}`;
19823
20513
  return key in ZH ? t(locale, key) : t(locale, "status.unknown");
19824
20514
  }
20515
+ function runStatusText(locale, status) {
20516
+ const key = `runStatus.${status}`;
20517
+ return key in ZH ? t(locale, key) : t(locale, "status.unknown");
20518
+ }
20519
+ function resolveEditedRunAt(originalEpoch, originalLocal, currentLocal) {
20520
+ if (originalEpoch !== null && originalLocal === currentLocal) return originalEpoch;
20521
+ return new Date(currentLocal).getTime();
20522
+ }
19825
20523
  function formatRelative(timestamp2, locale) {
19826
20524
  if (!timestamp2) return "\u2014";
19827
20525
  const deltaMs = timestamp2 - Date.now();
@@ -19898,13 +20596,31 @@ function clientMessages(locale) {
19898
20596
  "dialog.clearTitle",
19899
20597
  "dialog.clearBody",
19900
20598
  "dialog.clearInstruction",
20599
+ "dialog.restartGateway",
20600
+ "dialog.restartGatewayBody",
20601
+ "dialog.restartGatewayRunningBody",
19901
20602
  "feedback.retryFailed",
19902
20603
  "feedback.cancelFailed",
19903
20604
  "feedback.deleteFailed",
19904
20605
  "feedback.requestFailed",
19905
20606
  "feedback.triggered",
20607
+ "feedback.taskCreated",
20608
+ "feedback.taskUpdated",
20609
+ "task.projectExisting",
20610
+ "task.projectNew",
20611
+ "task.createTitle",
20612
+ "task.editTitle",
20613
+ "action.saveTask",
20614
+ "action.updateTask",
19906
20615
  "feedback.configSaved",
19907
20616
  "feedback.databaseCleared",
20617
+ "feedback.templateCreated",
20618
+ "feedback.templateUpdated",
20619
+ "feedback.sessionCommandCopied",
20620
+ "feedback.restarting",
20621
+ "feedback.restartTimeout",
20622
+ "template.createTitle",
20623
+ "template.editTitle",
19908
20624
  "filter.noResults"
19909
20625
  ];
19910
20626
  return Object.fromEntries(keys.map((key) => [key, t(locale, key)]));
@@ -19998,6 +20714,7 @@ function renderLayout(options) {
19998
20714
  themeMedia.addEventListener?.('change',()=>{if((localStorage.getItem('supertask-theme')||'system')==='system')applyTheme('system');});
19999
20715
  applyTheme(localStorage.getItem('supertask-theme')||'system');
20000
20716
  requestAnimationFrame(()=>document.documentElement.classList.add('ui-ready'));
20717
+ ${resolveEditedRunAt.toString()}
20001
20718
  function refreshPage(button){button.classList.add('refreshing');button.setAttribute('aria-label','${t(locale, "a11y.refreshing")}');location.reload();}
20002
20719
  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);}
20003
20720
  async function readJson(response){const data=await response.json().catch(()=>({}));if(!response.ok)throw new Error(data.error||text('feedback.requestFailed'));return data;}
@@ -20009,6 +20726,22 @@ function renderLayout(options) {
20009
20726
  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')}}
20010
20727
  const showDetail=id=>showRecord('/api/tasks/'+id);const showRunDetail=id=>showRecord('/api/runs/'+id);const showTemplateDetail=id=>showRecord('/api/templates/'+id);
20011
20728
  async function copyDetails(){try{await navigator.clipboard.writeText(document.getElementById('detail-content').textContent);showToast(text('details.copySuccess'))}catch{showToast(text('feedback.copyFailed'),'error')}}
20729
+ 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')}}
20730
+ function taskField(name){return document.getElementById('task-'+name)}
20731
+ function taskProjects(){const node=document.getElementById('task-project-data');if(!node)return {};try{return JSON.parse(node.textContent||'{}')}catch{return {}}}
20732
+ 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')}
20733
+ 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)}
20734
+ 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')}}
20735
+ 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}}
20736
+ function templateField(name){return document.getElementById('template-'+name)}
20737
+ 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'}
20738
+ function localDateTime(milliseconds){const date=new Date(milliseconds);const local=new Date(milliseconds-date.getTimezoneOffset()*60000);return local.toISOString().slice(0,23)}
20739
+ 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}}
20740
+ 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}
20741
+ function selectedRunAt(){const input=templateField('run-at');return resolveEditedRunAt(input.dataset.originalEpoch?Number(input.dataset.originalEpoch):null,input.dataset.originalLocal||'',input.value)}
20742
+ 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)}
20743
+ 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')}}
20744
+ 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}}
20012
20745
  async function enableTmpl(id){try{await readJson(await fetch('/api/templates/'+id+'/enable',{method:'POST'}));location.reload()}catch(error){showToast(error.message,'error')}}
20013
20746
  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')}}
20014
20747
  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')}}
@@ -20016,7 +20749,9 @@ function renderLayout(options) {
20016
20749
  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');}
20017
20750
  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;}
20018
20751
  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')}}
20019
- 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')}}
20752
+ async function confirmGatewayRestart(runningCount=0){const body=runningCount>0?text('dialog.restartGatewayRunningBody',{count:runningCount}):text('dialog.restartGatewayBody');return await ask(text('dialog.restartGateway'),body)}
20753
+ 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}}
20754
+ 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')}}
20020
20755
  </script>
20021
20756
  </body>
20022
20757
  </html>`;
@@ -20028,17 +20763,17 @@ var init_ui = __esm({
20028
20763
  ZH = {
20029
20764
  "app.name": "SuperTask",
20030
20765
  "app.dashboard": "\u63A7\u5236\u53F0",
20031
- "app.tagline": "\u53EF\u9760\u7684\u672C\u5730 Agent \u8C03\u5EA6\u4E2D\u5FC3",
20766
+ "app.tagline": "\u53EF\u9760\u7684\u672C\u5730 Agent \u4EFB\u52A1\u4E2D\u5FC3",
20032
20767
  "app.local": "\u672C\u5730\u8FD0\u884C",
20033
20768
  "app.footer": "Local-first \xB7 \u6570\u636E\u7559\u5728\u4F60\u7684\u8BBE\u5907\u4E0A",
20034
20769
  "nav.tasks": "\u4EFB\u52A1",
20035
- "nav.templates": "\u8C03\u5EA6",
20770
+ "nav.templates": "\u5B9A\u65F6\u4EFB\u52A1",
20036
20771
  "nav.runs": "\u6267\u884C\u8BB0\u5F55",
20037
20772
  "nav.system": "\u7CFB\u7EDF",
20038
20773
  "page.tasks.title": "\u4EFB\u52A1\u961F\u5217",
20039
20774
  "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",
20040
- "page.templates.title": "\u8C03\u5EA6\u6A21\u677F",
20041
- "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",
20775
+ "page.templates.title": "\u5B9A\u65F6\u4EFB\u52A1",
20776
+ "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",
20042
20777
  "page.runs.title": "\u6267\u884C\u8BB0\u5F55",
20043
20778
  "page.runs.description": "\u8FFD\u8E2A\u6BCF\u6B21 Agent \u6267\u884C\u7684\u72B6\u6001\u3001\u8017\u65F6\u3001\u5FC3\u8DF3\u4E0E\u8F93\u51FA\u3002",
20044
20779
  "page.system.title": "\u7CFB\u7EDF\u8BBE\u7F6E",
@@ -20048,29 +20783,43 @@ var init_ui = __esm({
20048
20783
  "action.retry": "\u91CD\u8BD5",
20049
20784
  "action.cancel": "\u53D6\u6D88",
20050
20785
  "action.delete": "\u5220\u9664",
20786
+ "action.edit": "\u7F16\u8F91",
20051
20787
  "action.enable": "\u542F\u7528",
20052
20788
  "action.disable": "\u7981\u7528",
20053
- "action.trigger": "\u7ACB\u5373\u89E6\u53D1",
20789
+ "action.trigger": "\u7ACB\u5373\u8FD0\u884C\u4E00\u6B21",
20790
+ "action.continueSession": "\u7EE7\u7EED\u4F1A\u8BDD",
20054
20791
  "action.logs": "\u67E5\u770B\u65E5\u5FD7",
20055
20792
  "action.hideLogs": "\u6536\u8D77\u65E5\u5FD7",
20056
20793
  "action.save": "\u4FDD\u5B58\u8BBE\u7F6E",
20794
+ "action.saveAndRestart": "\u4FDD\u5B58\u5E76\u91CD\u542F",
20057
20795
  "action.copy": "\u590D\u5236 JSON",
20058
20796
  "action.close": "\u5173\u95ED",
20059
20797
  "action.confirm": "\u786E\u8BA4",
20060
20798
  "action.clearDatabase": "\u6E05\u7A7A\u6570\u636E\u5E93",
20799
+ "action.createTask": "\u65B0\u5EFA\u4EFB\u52A1",
20800
+ "action.saveTask": "\u52A0\u5165\u961F\u5217",
20801
+ "action.updateTask": "\u4FDD\u5B58\u4FEE\u6539",
20802
+ "action.createTemplate": "\u65B0\u5EFA\u5B9A\u65F6\u4EFB\u52A1",
20803
+ "action.saveTemplate": "\u4FDD\u5B58\u5B9A\u65F6\u4EFB\u52A1",
20061
20804
  "status.pending": "\u5F85\u6267\u884C",
20062
20805
  "status.running": "\u8FD0\u884C\u4E2D",
20063
20806
  "status.done": "\u5DF2\u5B8C\u6210",
20064
- "status.failed": "\u5931\u8D25",
20065
- "status.dead_letter": "\u6B7B\u4FE1",
20807
+ "status.failed": "\u7B49\u5F85\u91CD\u8BD5",
20808
+ "status.dead_letter": "\u5DF2\u505C\u6B62",
20809
+ "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",
20810
+ "status.deadLetterAction": "\u9700\u68C0\u67E5\u539F\u56E0\u540E\u624B\u52A8\u91CD\u8BD5",
20066
20811
  "status.cancelled": "\u5DF2\u53D6\u6D88",
20812
+ "status.executionStillActive": "\u6267\u884C\u8FDB\u7A0B\u4ECD\u5728\u9000\u51FA\uFF0C\u6682\u65F6\u5360\u7528\u5E76\u53D1",
20067
20813
  "status.unknown": "\u672A\u77E5",
20814
+ "runStatus.running": "\u8FD0\u884C\u4E2D",
20815
+ "runStatus.done": "\u6210\u529F",
20816
+ "runStatus.failed": "\u5931\u8D25",
20068
20817
  "stats.total": "\u603B\u4EFB\u52A1",
20069
20818
  "stats.pending": "\u5F85\u6267\u884C",
20070
20819
  "stats.running": "\u8FD0\u884C\u4E2D",
20071
20820
  "stats.done": "\u5DF2\u5B8C\u6210",
20072
- "stats.failedDead": "\u5931\u8D25\u4E0E\u6B7B\u4FE1",
20073
- "stats.templates": "\u6A21\u677F\u603B\u6570",
20821
+ "stats.failedDead": "\u7B49\u5F85\u91CD\u8BD5\u4E0E\u5DF2\u505C\u6B62",
20822
+ "stats.templates": "\u5B9A\u65F6\u4EFB\u52A1\u603B\u6570",
20074
20823
  "stats.enabled": "\u5DF2\u542F\u7528",
20075
20824
  "stats.disabled": "\u5DF2\u7981\u7528",
20076
20825
  "stats.records": "\u6267\u884C\u603B\u6570",
@@ -20078,7 +20827,7 @@ var init_ui = __esm({
20078
20827
  "stats.pageFailed": "\u672C\u9875\u5931\u8D25",
20079
20828
  "stats.pageRunning": "\u672C\u9875\u8FD0\u884C\u4E2D",
20080
20829
  "filter.all": "\u5168\u90E8",
20081
- "filter.searchTasks": "\u641C\u7D22\u5F53\u524D\u9875\u7684\u4EFB\u52A1\u3001Agent \u6216\u63D0\u793A\u8BCD",
20830
+ "filter.searchTasks": "\u641C\u7D22\u5F53\u524D\u9875\u7684\u4EFB\u52A1\u3001\u9879\u76EE\u3001\u6279\u6B21\u3001Agent \u6216\u63D0\u793A\u8BCD",
20082
20831
  "filter.noResults": "\u6CA1\u6709\u7B26\u5408\u5F53\u524D\u641C\u7D22\u6761\u4EF6\u7684\u4EFB\u52A1",
20083
20832
  "table.id": "ID",
20084
20833
  "table.task": "\u4EFB\u52A1",
@@ -20094,7 +20843,7 @@ var init_ui = __esm({
20094
20843
  "table.nextRun": "\u4E0B\u6B21\u6267\u884C",
20095
20844
  "table.run": "Run",
20096
20845
  "table.heartbeat": "\u5FC3\u8DF3",
20097
- "table.session": "Session",
20846
+ "table.session": "\u4F1A\u8BDD",
20098
20847
  "table.model": "\u6A21\u578B",
20099
20848
  "table.startedAt": "\u542F\u52A8\u65F6\u95F4",
20100
20849
  "table.pid": "PID",
@@ -20102,14 +20851,14 @@ var init_ui = __esm({
20102
20851
  "pagination.next": "\u4E0B\u4E00\u9875",
20103
20852
  "pagination.summary": "\u7B2C {page} \u9875\uFF0C\u5171 {pages} \u9875 \xB7 {total} \u6761",
20104
20853
  "empty.tasks": "\u961F\u5217\u91CC\u8FD8\u6CA1\u6709\u4EFB\u52A1",
20105
- "empty.tasksHint": "\u901A\u8FC7 OpenCode \u63D2\u4EF6\u6216 supertask add \u521B\u5EFA\u7B2C\u4E00\u4E2A\u4EFB\u52A1\u3002",
20106
- "empty.templates": "\u8FD8\u6CA1\u6709\u8C03\u5EA6\u6A21\u677F",
20107
- "empty.templatesHint": "\u4F7F\u7528 CLI \u521B\u5EFA\uFF1Asupertask template add",
20854
+ "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",
20855
+ "empty.templates": "\u8FD8\u6CA1\u6709\u5B9A\u65F6\u4EFB\u52A1",
20856
+ "empty.templatesHint": "\u70B9\u51FB\u53F3\u4E0A\u89D2\u201C\u65B0\u5EFA\u5B9A\u65F6\u4EFB\u52A1\u201D\u5F00\u59CB\u521B\u5EFA\u3002",
20108
20857
  "empty.runs": "\u8FD8\u6CA1\u6709\u6267\u884C\u8BB0\u5F55",
20109
20858
  "empty.running": "\u5F53\u524D\u6CA1\u6709\u8FD0\u884C\u4E2D\u7684\u4EFB\u52A1",
20110
20859
  "schedule.cron": "Cron",
20111
- "schedule.recurring": "\u5FAA\u73AF",
20112
- "schedule.delayed": "\u5EF6\u8FDF",
20860
+ "schedule.recurring": "\u56FA\u5B9A\u95F4\u9694",
20861
+ "schedule.delayed": "\u4E00\u6B21\u6027",
20113
20862
  "schedule.unknown": "\u672A\u77E5",
20114
20863
  "schedule.enabled": "\u5DF2\u542F\u7528",
20115
20864
  "schedule.disabled": "\u5DF2\u7981\u7528",
@@ -20118,14 +20867,14 @@ var init_ui = __esm({
20118
20867
  "schedule.hours": "{count} \u5C0F\u65F6",
20119
20868
  "schedule.days": "{count} \u5929",
20120
20869
  "schedule.overdue": "\u5DF2\u5230\u671F",
20121
- "system.worker": "Worker",
20122
- "system.scheduler": "Scheduler",
20123
- "system.watchdog": "Watchdog",
20870
+ "system.worker": "\u4EFB\u52A1\u6267\u884C",
20871
+ "system.scheduler": "\u5B9A\u65F6\u4EFB\u52A1\u670D\u52A1",
20872
+ "system.watchdog": "\u8FD0\u884C\u76D1\u63A7",
20124
20873
  "system.maxConcurrency": "\u6700\u5927\u5E76\u53D1",
20125
20874
  "system.pollInterval": "\u8F6E\u8BE2\u95F4\u9694",
20126
20875
  "system.heartbeatInterval": "\u5FC3\u8DF3\u95F4\u9694",
20127
20876
  "system.taskTimeout": "\u4EFB\u52A1\u8D85\u65F6",
20128
- "system.schedulerEnabled": "\u542F\u7528\u8C03\u5EA6",
20877
+ "system.schedulerEnabled": "\u542F\u7528\u5B9A\u65F6\u4EFB\u52A1",
20129
20878
  "system.checkInterval": "\u68C0\u67E5\u95F4\u9694",
20130
20879
  "system.heartbeatTimeout": "\u5FC3\u8DF3\u8D85\u65F6",
20131
20880
  "system.cleanupInterval": "\u6E05\u7406\u95F4\u9694",
@@ -20135,8 +20884,12 @@ var init_ui = __esm({
20135
20884
  "system.minutes": "\u5206\u949F",
20136
20885
  "system.hours": "\u5C0F\u65F6",
20137
20886
  "system.days": "\u5929",
20138
- "system.activeTemplates": "\u6D3B\u8DC3\u6A21\u677F",
20139
- "system.saveHint": "\u4FDD\u5B58\u540E\u9700\u91CD\u542F Gateway \u751F\u6548",
20887
+ "system.activeTemplates": "\u5DF2\u542F\u7528\u5B9A\u65F6\u4EFB\u52A1",
20888
+ "system.saveHint": "\u8BBE\u7F6E\u4FDD\u5B58\u540E\u9700\u8981\u91CD\u542F Gateway \u624D\u80FD\u751F\u6548\u3002",
20889
+ "system.configApplied": "\u9875\u9762\u4E2D\u7684\u8BBE\u7F6E\u6B63\u5728\u751F\u6548\u3002",
20890
+ "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",
20891
+ "system.configForeground": "\u6B64\u9875\u9762\u672A\u8FDE\u63A5\u5230 Gateway \u7684\u8FD0\u884C\u914D\u7F6E\uFF1B\u4FDD\u5B58\u540E\u8BF7\u91CD\u542F Gateway\u3002",
20892
+ "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",
20140
20893
  "system.runningTasks": "\u5F53\u524D\u8FD0\u884C\u4EFB\u52A1\uFF08{running} / {limit} \u5E76\u53D1\uFF09",
20141
20894
  "system.taskStats": "\u4EFB\u52A1\u6982\u89C8",
20142
20895
  "system.configFile": "\u914D\u7F6E\u6587\u4EF6",
@@ -20145,7 +20898,46 @@ var init_ui = __esm({
20145
20898
  "system.yes": "\u662F",
20146
20899
  "system.noDefault": "\u5426\uFF0C\u5F53\u524D\u4F7F\u7528\u9ED8\u8BA4\u503C",
20147
20900
  "system.danger": "\u5371\u9669\u64CD\u4F5C",
20148
- "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",
20901
+ "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",
20902
+ "template.createTitle": "\u65B0\u5EFA\u5B9A\u65F6\u4EFB\u52A1",
20903
+ "template.editTitle": "\u7F16\u8F91\u5B9A\u65F6\u4EFB\u52A1",
20904
+ "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",
20905
+ "template.name": "\u540D\u79F0",
20906
+ "template.cwd": "\u9879\u76EE\u76EE\u5F55",
20907
+ "template.cwdHint": "OpenCode \u4F1A\u5728\u8FD9\u4E2A\u76EE\u5F55\u4E2D\u6267\u884C\u4EFB\u52A1\u3002",
20908
+ "template.agent": "Agent",
20909
+ "template.model": "\u6A21\u578B",
20910
+ "template.prompt": "\u63D0\u793A\u8BCD",
20911
+ "template.scheduleType": "\u6267\u884C\u65B9\u5F0F",
20912
+ "template.cronExpr": "Cron \u8868\u8FBE\u5F0F",
20913
+ "template.cronHint": "\u4F8B\u5982\uFF1A0 9 * * *\uFF08\u6BCF\u5929 09:00\uFF09",
20914
+ "template.interval": "\u6267\u884C\u95F4\u9694",
20915
+ "template.runAt": "\u6267\u884C\u65F6\u95F4",
20916
+ "template.durationHint": "\u652F\u6301 30s\u30015min\u30011h\u30012d",
20917
+ "template.advanced": "\u66F4\u591A\u6267\u884C\u8BBE\u7F6E",
20918
+ "template.category": "\u5206\u7C7B",
20919
+ "template.batchId": "\u6279\u6B21 ID",
20920
+ "template.importance": "\u91CD\u8981\u7A0B\u5EA6\uFF081-5\uFF09",
20921
+ "template.urgency": "\u7D27\u6025\u7A0B\u5EA6\uFF081-5\uFF09",
20922
+ "template.maxInstances": "\u81EA\u52A8\u8C03\u5EA6\u4E0A\u9650",
20923
+ "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",
20924
+ "template.maxRetries": "\u5931\u8D25\u91CD\u8BD5\u6B21\u6570",
20925
+ "template.retryBackoff": "\u91CD\u8BD5\u7B49\u5F85",
20926
+ "template.timeout": "\u5355\u6B21\u8D85\u65F6",
20927
+ "template.optional": "\u53EF\u9009",
20928
+ "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",
20929
+ "projects.title": "\u9879\u76EE\u5206\u7EC4",
20930
+ "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",
20931
+ "projects.all": "\u5168\u90E8\u9879\u76EE",
20932
+ "projects.legacy": "\u672A\u5206\u7EC4",
20933
+ "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",
20934
+ "projects.counts": "\u8FD0\u884C {running} \xB7 \u6392\u961F {pending} \xB7 \u5F02\u5E38 {failed}",
20935
+ "task.createTitle": "\u65B0\u5EFA\u4EFB\u52A1",
20936
+ "task.editTitle": "\u7F16\u8F91\u4EFB\u52A1",
20937
+ "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",
20938
+ "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",
20939
+ "task.projectExisting": "\u6B64\u9879\u76EE\u73B0\u6709 {total} \u4E2A\u4EFB\u52A1\uFF1A\u8FD0\u884C {running}\uFF0C\u6392\u961F {pending}\uFF0C\u5F02\u5E38 {failed}\u3002",
20940
+ "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",
20149
20941
  "theme.label": "\u4E3B\u9898",
20150
20942
  "theme.system": "\u8DDF\u968F\u7CFB\u7EDF",
20151
20943
  "theme.light": "\u6D45\u8272",
@@ -20160,21 +20952,31 @@ var init_ui = __esm({
20160
20952
  "dialog.retryTaskBody": "\u4EFB\u52A1\u5C06\u56DE\u5230\u5F85\u6267\u884C\u72B6\u6001\uFF0C\u5E76\u91CD\u7F6E\u81EA\u52A8\u91CD\u8BD5\u9884\u7B97\u3002",
20161
20953
  "dialog.deleteTask": "\u5220\u9664\u4EFB\u52A1 #{id}\uFF1F",
20162
20954
  "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",
20163
- "dialog.disableTemplate": "\u7981\u7528\u8FD9\u4E2A\u8C03\u5EA6\u6A21\u677F\uFF1F",
20164
- "dialog.disableTemplateBody": "\u6A21\u677F\u5C06\u505C\u6B62\u81EA\u52A8\u521B\u5EFA\u65B0\u4EFB\u52A1\uFF0C\u5DF2\u6709\u4EFB\u52A1\u4E0D\u53D7\u5F71\u54CD\u3002",
20165
- "dialog.deleteTemplate": "\u5220\u9664\u8FD9\u4E2A\u8C03\u5EA6\u6A21\u677F\uFF1F",
20166
- "dialog.deleteTemplateBody": "\u6A21\u677F\u914D\u7F6E\u5C06\u6C38\u4E45\u5220\u9664\uFF0C\u6B64\u64CD\u4F5C\u65E0\u6CD5\u64A4\u9500\u3002",
20167
- "dialog.triggerTemplate": "\u7ACB\u5373\u89E6\u53D1\u4E00\u6B21\uFF1F",
20168
- "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",
20955
+ "dialog.disableTemplate": "\u7981\u7528\u8FD9\u4E2A\u5B9A\u65F6\u4EFB\u52A1\uFF1F",
20956
+ "dialog.disableTemplateBody": "\u5B83\u5C06\u505C\u6B62\u81EA\u52A8\u521B\u5EFA\u65B0\u4EFB\u52A1\uFF0C\u5DF2\u6709\u4EFB\u52A1\u4E0D\u53D7\u5F71\u54CD\u3002",
20957
+ "dialog.deleteTemplate": "\u5220\u9664\u8FD9\u4E2A\u5B9A\u65F6\u4EFB\u52A1\uFF1F",
20958
+ "dialog.deleteTemplateBody": "\u5B9A\u65F6\u4EFB\u52A1\u914D\u7F6E\u5C06\u6C38\u4E45\u5220\u9664\uFF0C\u6B64\u64CD\u4F5C\u65E0\u6CD5\u64A4\u9500\u3002",
20959
+ "dialog.triggerTemplate": "\u7ACB\u5373\u8FD0\u884C\u4E00\u6B21\uFF1F",
20960
+ "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",
20169
20961
  "dialog.clearTitle": "\u786E\u8BA4\u6E05\u7A7A\u6570\u636E\u5E93",
20170
- "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",
20962
+ "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",
20171
20963
  "dialog.clearInstruction": "\u8F93\u5165 CLEAR \u4EE5\u786E\u8BA4",
20964
+ "dialog.restartGateway": "\u91CD\u542F\u5E76\u5E94\u7528\u8BBE\u7F6E\uFF1F",
20965
+ "dialog.restartGatewayBody": "Gateway \u4F1A\u77ED\u6682\u79BB\u7EBF\uFF0C\u901A\u5E38\u6570\u79D2\u540E\u7531 PM2 \u81EA\u52A8\u6062\u590D\u3002",
20966
+ "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",
20172
20967
  "feedback.retryFailed": "\u91CD\u8BD5\u5931\u8D25",
20173
20968
  "feedback.cancelFailed": "\u53D6\u6D88\u5931\u8D25",
20174
20969
  "feedback.deleteFailed": "\u5220\u9664\u5931\u8D25",
20175
20970
  "feedback.requestFailed": "\u8BF7\u6C42\u5931\u8D25",
20176
20971
  "feedback.triggered": "\u5DF2\u521B\u5EFA\u4EFB\u52A1 #{id}",
20177
- "feedback.configSaved": "\u8BBE\u7F6E\u5DF2\u4FDD\u5B58\uFF0C\u91CD\u542F Gateway \u540E\u751F\u6548",
20972
+ "feedback.taskCreated": "\u4EFB\u52A1 #{id} \u5DF2\u52A0\u5165\u961F\u5217",
20973
+ "feedback.taskUpdated": "\u4EFB\u52A1 #{id} \u5DF2\u66F4\u65B0",
20974
+ "feedback.templateCreated": "\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u521B\u5EFA",
20975
+ "feedback.templateUpdated": "\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u66F4\u65B0",
20976
+ "feedback.configSaved": "\u8BBE\u7F6E\u5DF2\u4FDD\u5B58",
20977
+ "feedback.sessionCommandCopied": "\u4F1A\u8BDD\u547D\u4EE4\u5DF2\u590D\u5236\uFF0C\u53EF\u76F4\u63A5\u7C98\u8D34\u5230\u7EC8\u7AEF\u8FD0\u884C",
20978
+ "feedback.restarting": "Gateway \u6B63\u5728\u91CD\u542F\uFF0C\u8BF7\u7A0D\u5019\u2026",
20979
+ "feedback.restartTimeout": "Gateway \u5C1A\u672A\u6062\u590D\uFF0C\u8BF7\u7A0D\u540E\u5237\u65B0\u6216\u68C0\u67E5 PM2 \u72B6\u6001",
20178
20980
  "feedback.databaseCleared": "\u6570\u636E\u5E93\u5DF2\u6E05\u7A7A\uFF0C\u5907\u4EFD\u4F4D\u4E8E\uFF1A{path}",
20179
20981
  "feedback.copyFailed": "\u590D\u5236\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u9009\u62E9\u5185\u5BB9",
20180
20982
  "a11y.skip": "\u8DF3\u5230\u4E3B\u8981\u5185\u5BB9",
@@ -20187,13 +20989,13 @@ var init_ui = __esm({
20187
20989
  "app.local": "Running locally",
20188
20990
  "app.footer": "Local-first \xB7 Your data stays on this device",
20189
20991
  "nav.tasks": "Tasks",
20190
- "nav.templates": "Schedules",
20992
+ "nav.templates": "Scheduled tasks",
20191
20993
  "nav.runs": "Runs",
20192
20994
  "nav.system": "System",
20193
20995
  "page.tasks.title": "Task queue",
20194
20996
  "page.tasks.description": "Track priority, execution state, and retries, then act on tasks that need attention.",
20195
- "page.templates.title": "Schedule templates",
20196
- "page.templates.description": "Manage cron, delayed, and recurring work so repeated tasks run on time.",
20997
+ "page.templates.title": "Scheduled tasks",
20998
+ "page.templates.description": "Create and edit cron, fixed-interval, and one-time tasks, including their model, agent, and prompt.",
20197
20999
  "page.runs.title": "Execution history",
20198
21000
  "page.runs.description": "Inspect the status, duration, heartbeat, and output of every agent run.",
20199
21001
  "page.system.title": "System settings",
@@ -20203,29 +21005,43 @@ var init_ui = __esm({
20203
21005
  "action.retry": "Retry",
20204
21006
  "action.cancel": "Cancel",
20205
21007
  "action.delete": "Delete",
21008
+ "action.edit": "Edit",
20206
21009
  "action.enable": "Enable",
20207
21010
  "action.disable": "Disable",
20208
21011
  "action.trigger": "Run now",
21012
+ "action.continueSession": "Continue session",
20209
21013
  "action.logs": "View log",
20210
21014
  "action.hideLogs": "Hide log",
20211
21015
  "action.save": "Save settings",
21016
+ "action.saveAndRestart": "Save and restart",
20212
21017
  "action.copy": "Copy JSON",
20213
21018
  "action.close": "Close",
20214
21019
  "action.confirm": "Confirm",
20215
21020
  "action.clearDatabase": "Clear database",
21021
+ "action.createTask": "New task",
21022
+ "action.saveTask": "Add to queue",
21023
+ "action.updateTask": "Save changes",
21024
+ "action.createTemplate": "New scheduled task",
21025
+ "action.saveTemplate": "Save scheduled task",
20216
21026
  "status.pending": "Pending",
20217
21027
  "status.running": "Running",
20218
21028
  "status.done": "Done",
20219
- "status.failed": "Failed",
20220
- "status.dead_letter": "Dead letter",
21029
+ "status.failed": "Waiting to retry",
21030
+ "status.dead_letter": "Stopped",
21031
+ "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.",
21032
+ "status.deadLetterAction": "Inspect the error, then retry manually",
20221
21033
  "status.cancelled": "Cancelled",
21034
+ "status.executionStillActive": "The execution process is still stopping and temporarily occupies a slot",
20222
21035
  "status.unknown": "Unknown",
21036
+ "runStatus.running": "Running",
21037
+ "runStatus.done": "Succeeded",
21038
+ "runStatus.failed": "Failed",
20223
21039
  "stats.total": "Total tasks",
20224
21040
  "stats.pending": "Pending",
20225
21041
  "stats.running": "Running",
20226
21042
  "stats.done": "Completed",
20227
- "stats.failedDead": "Failed & dead",
20228
- "stats.templates": "Templates",
21043
+ "stats.failedDead": "Waiting to retry & stopped",
21044
+ "stats.templates": "Scheduled tasks",
20229
21045
  "stats.enabled": "Enabled",
20230
21046
  "stats.disabled": "Disabled",
20231
21047
  "stats.records": "Total runs",
@@ -20233,7 +21049,7 @@ var init_ui = __esm({
20233
21049
  "stats.pageFailed": "Failed here",
20234
21050
  "stats.pageRunning": "Running here",
20235
21051
  "filter.all": "All",
20236
- "filter.searchTasks": "Search tasks, agents, or prompts on this page",
21052
+ "filter.searchTasks": "Search tasks, projects, batches, agents, or prompts on this page",
20237
21053
  "filter.noResults": "No tasks match this search",
20238
21054
  "table.id": "ID",
20239
21055
  "table.task": "Task",
@@ -20257,14 +21073,14 @@ var init_ui = __esm({
20257
21073
  "pagination.next": "Next",
20258
21074
  "pagination.summary": "Page {page} of {pages} \xB7 {total} items",
20259
21075
  "empty.tasks": "Your queue is empty",
20260
- "empty.tasksHint": "Create the first task with the OpenCode plugin or supertask add.",
20261
- "empty.templates": "No schedule templates yet",
20262
- "empty.templatesHint": "Create one with: supertask template add",
21076
+ "empty.tasksHint": "Select \u201CNew task\u201D, or use the OpenCode plugin or supertask add.",
21077
+ "empty.templates": "No scheduled tasks yet",
21078
+ "empty.templatesHint": "Select \u201CNew scheduled task\u201D to create one.",
20263
21079
  "empty.runs": "No execution history yet",
20264
21080
  "empty.running": "No tasks are running right now",
20265
21081
  "schedule.cron": "Cron",
20266
- "schedule.recurring": "Recurring",
20267
- "schedule.delayed": "Delayed",
21082
+ "schedule.recurring": "Fixed interval",
21083
+ "schedule.delayed": "One-time",
20268
21084
  "schedule.unknown": "Unknown",
20269
21085
  "schedule.enabled": "Enabled",
20270
21086
  "schedule.disabled": "Disabled",
@@ -20273,14 +21089,14 @@ var init_ui = __esm({
20273
21089
  "schedule.hours": "{count} hr",
20274
21090
  "schedule.days": "{count} days",
20275
21091
  "schedule.overdue": "Overdue",
20276
- "system.worker": "Worker",
20277
- "system.scheduler": "Scheduler",
20278
- "system.watchdog": "Watchdog",
21092
+ "system.worker": "Task execution",
21093
+ "system.scheduler": "Scheduled task service",
21094
+ "system.watchdog": "Runtime monitor",
20279
21095
  "system.maxConcurrency": "Max concurrency",
20280
21096
  "system.pollInterval": "Poll interval",
20281
21097
  "system.heartbeatInterval": "Heartbeat interval",
20282
21098
  "system.taskTimeout": "Task timeout",
20283
- "system.schedulerEnabled": "Enable scheduler",
21099
+ "system.schedulerEnabled": "Enable scheduled tasks",
20284
21100
  "system.checkInterval": "Check interval",
20285
21101
  "system.heartbeatTimeout": "Heartbeat timeout",
20286
21102
  "system.cleanupInterval": "Cleanup interval",
@@ -20290,8 +21106,12 @@ var init_ui = __esm({
20290
21106
  "system.minutes": "minutes",
20291
21107
  "system.hours": "hours",
20292
21108
  "system.days": "days",
20293
- "system.activeTemplates": "Active templates",
20294
- "system.saveHint": "Restart Gateway to apply saved settings",
21109
+ "system.activeTemplates": "Enabled scheduled tasks",
21110
+ "system.saveHint": "Restart Gateway to apply saved settings.",
21111
+ "system.configApplied": "The settings shown here are active.",
21112
+ "system.configPending": "Settings are saved, but this Gateway is still using the values from its last start.",
21113
+ "system.configForeground": "This page is not connected to Gateway runtime settings. Restart Gateway after saving.",
21114
+ "system.configRestartManually": "Settings are saved but not active. Return to the terminal that launched Gateway, press Ctrl-C, then run supertask gateway again.",
20295
21115
  "system.runningTasks": "Active tasks ({running} / {limit} concurrent)",
20296
21116
  "system.taskStats": "Task overview",
20297
21117
  "system.configFile": "Configuration file",
@@ -20301,6 +21121,45 @@ var init_ui = __esm({
20301
21121
  "system.noDefault": "No, using defaults",
20302
21122
  "system.danger": "Danger zone",
20303
21123
  "system.dangerDescription": "A verified backup is created first, then tasks, runs, and templates are cleared transactionally. Active work blocks this operation.",
21124
+ "template.createTitle": "New scheduled task",
21125
+ "template.editTitle": "Edit scheduled task",
21126
+ "template.formSubtitle": "Choose when to run and which project, model, and prompt OpenCode should use.",
21127
+ "template.name": "Name",
21128
+ "template.cwd": "Project directory",
21129
+ "template.cwdHint": "OpenCode runs the task in this directory.",
21130
+ "template.agent": "Agent",
21131
+ "template.model": "Model",
21132
+ "template.prompt": "Prompt",
21133
+ "template.scheduleType": "Schedule",
21134
+ "template.cronExpr": "Cron expression",
21135
+ "template.cronHint": "Example: 0 9 * * * (daily at 09:00)",
21136
+ "template.interval": "Interval",
21137
+ "template.runAt": "Run at",
21138
+ "template.durationHint": "Supports 30s, 5min, 1h, 2d",
21139
+ "template.advanced": "More execution settings",
21140
+ "template.category": "Category",
21141
+ "template.batchId": "Batch ID",
21142
+ "template.importance": "Importance (1-5)",
21143
+ "template.urgency": "Urgency (1-5)",
21144
+ "template.maxInstances": "Automatic scheduling limit",
21145
+ "template.maxInstancesHint": "Limits automatic scheduling only. \u201CRun now\u201D always queues a task. Active instances include pending, running, and retry-waiting tasks.",
21146
+ "template.maxRetries": "Failure retries",
21147
+ "template.retryBackoff": "Retry delay",
21148
+ "template.timeout": "Run timeout",
21149
+ "template.optional": "Optional",
21150
+ "template.futureOnly": "Saved settings apply to tasks created in the future. Editing does not change queued or running tasks.",
21151
+ "projects.title": "Projects",
21152
+ "projects.description": "The project directory is the isolation key. Check active and queued work before adding more.",
21153
+ "projects.all": "All projects",
21154
+ "projects.legacy": "Ungrouped",
21155
+ "projects.legacyHint": "Legacy tasks without a project directory. View, cancel, or delete them; recreate under the correct project to make changes.",
21156
+ "projects.counts": "Running {running} \xB7 queued {pending} \xB7 issues {failed}",
21157
+ "task.createTitle": "New task",
21158
+ "task.editTitle": "Edit task",
21159
+ "task.formSubtitle": "The task enters the durable queue immediately and waits automatically when concurrency is full.",
21160
+ "task.batchHint": "Tasks with the same non-empty batch ID run serially. Blank removes the batch constraint; global concurrency and dependencies still apply.",
21161
+ "task.projectExisting": "This project has {total} tasks: {running} running, {pending} queued, and {failed} with issues.",
21162
+ "task.projectNew": "This is a new project group. It appears in the project list after creation.",
20304
21163
  "theme.label": "Theme",
20305
21164
  "theme.system": "System",
20306
21165
  "theme.light": "Light",
@@ -20318,18 +21177,28 @@ var init_ui = __esm({
20318
21177
  "dialog.disableTemplate": "Disable this schedule?",
20319
21178
  "dialog.disableTemplateBody": "It will stop creating new tasks automatically. Existing tasks are unchanged.",
20320
21179
  "dialog.deleteTemplate": "Delete this schedule?",
20321
- "dialog.deleteTemplateBody": "This template configuration will be permanently deleted.",
21180
+ "dialog.deleteTemplateBody": "This scheduled task configuration will be permanently deleted.",
20322
21181
  "dialog.triggerTemplate": "Run this schedule now?",
20323
- "dialog.triggerTemplateBody": "A new task is created from the template, subject to its instance limit.",
21182
+ "dialog.triggerTemplateBody": "A task is queued immediately using the current settings. If global concurrency is full, it waits to run.",
20324
21183
  "dialog.clearTitle": "Confirm database clear",
20325
- "dialog.clearBody": "This deletes every task, run, and schedule template after creating a backup.",
21184
+ "dialog.clearBody": "This deletes every task, run, and scheduled task after creating a backup.",
20326
21185
  "dialog.clearInstruction": "Type CLEAR to confirm",
21186
+ "dialog.restartGateway": "Restart and apply settings?",
21187
+ "dialog.restartGatewayBody": "Gateway will be briefly unavailable and should recover through PM2 within a few seconds.",
21188
+ "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.",
20327
21189
  "feedback.retryFailed": "Retry failed",
20328
21190
  "feedback.cancelFailed": "Cancellation failed",
20329
21191
  "feedback.deleteFailed": "Delete failed",
20330
21192
  "feedback.requestFailed": "Request failed",
20331
21193
  "feedback.triggered": "Task #{id} created",
20332
- "feedback.configSaved": "Settings saved. Restart Gateway to apply them.",
21194
+ "feedback.taskCreated": "Task #{id} added to the queue",
21195
+ "feedback.taskUpdated": "Task #{id} updated",
21196
+ "feedback.templateCreated": "Scheduled task created",
21197
+ "feedback.templateUpdated": "Scheduled task updated",
21198
+ "feedback.configSaved": "Settings saved",
21199
+ "feedback.sessionCommandCopied": "Session command copied. Paste it into a terminal to continue.",
21200
+ "feedback.restarting": "Gateway is restarting\u2026",
21201
+ "feedback.restartTimeout": "Gateway has not recovered yet. Refresh later or check PM2 status.",
20333
21202
  "feedback.databaseCleared": "Database cleared. Backup: {path}",
20334
21203
  "feedback.copyFailed": "Copy failed. Select the content manually.",
20335
21204
  "a11y.skip": "Skip to main content",
@@ -20364,8 +21233,8 @@ var init_ui = __esm({
20364
21233
  radial-gradient(circle at 10% -10%,var(--bg-glow),transparent 34rem),var(--bg);
20365
21234
  font-family:Inter,ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
20366
21235
  font-size:14px; line-height:1.5; -webkit-font-smoothing:antialiased; }
20367
- button,input,select { font:inherit; }
20368
- button,a,select,input { -webkit-tap-highlight-color:transparent; }
21236
+ button,input,select,textarea { font:inherit; }
21237
+ button,a,select,input,textarea { -webkit-tap-highlight-color:transparent; }
20369
21238
  a { color:inherit; }
20370
21239
  code,.mono,.m { font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace; }
20371
21240
  .skip-link { position:fixed; left:16px; top:-60px; z-index:200; padding:10px 14px; border-radius:8px;
@@ -20446,6 +21315,16 @@ var init_ui = __esm({
20446
21315
  .panel-head { min-height:52px; display:flex; align-items:center; justify-content:space-between; gap:12px; padding:0 18px;
20447
21316
  border-bottom:1px solid var(--border); }
20448
21317
  .panel-head h2,.panel-head h3 { margin:0; font-size:14px; letter-spacing:-.01em; }
21318
+ .panel-head p { margin:3px 0 0; color:var(--text-2); font-size:11px; }
21319
+ .project-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(230px,1fr)); gap:10px; padding:14px; }
21320
+ .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; }
21321
+ .project-card:hover { border-color:var(--border-strong); background:var(--surface-3); }
21322
+ .project-card.active { border-color:color-mix(in srgb,var(--primary) 45%,var(--border)); background:var(--primary-soft); }
21323
+ .project-card-head { display:flex; align-items:center; justify-content:space-between; gap:10px; }
21324
+ .project-card-head strong { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
21325
+ .project-card-head span { color:var(--text-3); font-size:11px; }
21326
+ .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; }
21327
+ .project-counts { margin-top:8px; color:var(--text-2); font-size:11px; }
20449
21328
  .table-wrap { width:100%; overflow-x:auto; }
20450
21329
  table { width:100%; border-collapse:separate; border-spacing:0; font-size:13px; }
20451
21330
  th { height:42px; padding:0 13px; color:var(--text-3); background:var(--surface-2); border-bottom:1px solid var(--border);
@@ -20456,6 +21335,7 @@ var init_ui = __esm({
20456
21335
  tbody tr:hover { background:color-mix(in srgb,var(--primary-soft) 30%,transparent); }
20457
21336
  .task-name { font-weight:680; color:var(--text); }
20458
21337
  .task-prompt { max-width:520px; margin-top:3px; color:var(--text-2); font-size:12px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
21338
+ .task-context { margin-top:6px; }
20459
21339
  .muted,.mu { color:var(--text-2); }
20460
21340
  .faint { color:var(--text-3); }
20461
21341
  .small,.sm { font-size:12px; }
@@ -20539,6 +21419,18 @@ var init_ui = __esm({
20539
21419
  .dialog-head p { margin:3px 0 0; color:var(--text-2); font-size:11px; }
20540
21420
  .dialog-body { max-height:70vh; overflow:auto; padding:18px; }
20541
21421
  .dialog-actions { display:flex; align-items:center; justify-content:flex-end; gap:8px; padding:14px 18px; border-top:1px solid var(--border); }
21422
+ .template-dialog { width:min(880px,calc(100% - 32px)); }
21423
+ .template-form-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:15px; }
21424
+ .form-field { display:flex; min-width:0; flex-direction:column; gap:6px; color:var(--text-2); font-size:12px; font-weight:650; }
21425
+ .form-field-wide { grid-column:1 / -1; }
21426
+ .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;
21427
+ outline:none; color:var(--text); background:var(--surface-2); font-weight:450; }
21428
+ .form-field textarea { resize:vertical; line-height:1.5; }
21429
+ .form-field input:focus,.form-field select:focus,.form-field textarea:focus { border-color:var(--primary); box-shadow:var(--focus); background:var(--surface); }
21430
+ .form-field small { color:var(--text-3); font-size:10px; font-weight:450; }
21431
+ .advanced-fields { margin-top:18px; padding-top:14px; border-top:1px solid var(--border); }
21432
+ .advanced-fields summary { margin-bottom:14px; color:var(--text-2); cursor:pointer; font-size:12px; font-weight:700; }
21433
+ .form-note { margin:16px 0 0; color:var(--text-3); font-size:11px; }
20542
21434
  .json-view { min-height:160px; margin:0; padding:15px; overflow:auto; border:1px solid var(--border); border-radius:10px; color:var(--text-2);
20543
21435
  background:var(--surface-2); font-size:12px; white-space:pre-wrap; overflow-wrap:anywhere; }
20544
21436
  .confirm-copy { color:var(--text-2); margin:0; }
@@ -20604,6 +21496,9 @@ var init_ui = __esm({
20604
21496
  .responsive-table td[data-primary]::before { display:none; }
20605
21497
  .responsive-table .task-prompt { max-width:100%; }
20606
21498
  .responsive-table .actions { justify-content:flex-start; }
21499
+ .project-grid { grid-template-columns:1fr; }
21500
+ .template-form-grid { grid-template-columns:1fr; }
21501
+ .form-field-wide { grid-column:auto; }
20607
21502
  }
20608
21503
  @media (max-width:520px) {
20609
21504
  .stats-grid,.stats-grid.three { grid-template-columns:1fr 1fr; }
@@ -20631,10 +21526,45 @@ var init_ui = __esm({
20631
21526
  var web_exports = {};
20632
21527
  __export(web_exports, {
20633
21528
  dashboardApp: () => dashboardApp,
20634
- default: () => web_default
21529
+ default: () => web_default,
21530
+ isSafeDashboardRestartTarget: () => isSafeDashboardRestartTarget,
21531
+ resolveDashboardConfigState: () => resolveDashboardConfigState,
21532
+ setDashboardRuntimeConfig: () => setDashboardRuntimeConfig
20635
21533
  });
20636
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "fs";
20637
- import { dirname as dirname6 } from "path";
21534
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "fs";
21535
+ import { basename as basename3, dirname as dirname7 } from "path";
21536
+ function setDashboardRuntimeConfig(config) {
21537
+ runtimeConfig = config === null ? null : structuredClone(config);
21538
+ }
21539
+ function isValidSessionId(value) {
21540
+ return typeof value === "string" && SESSION_ID_PATTERN.test(value);
21541
+ }
21542
+ function maskSessionId(value) {
21543
+ return isValidSessionId(value) ? `${value.slice(4, 7)}***${value.slice(-3)}` : "\u2014";
21544
+ }
21545
+ function sessionCommand(value) {
21546
+ if (!isValidSessionId(value)) throw new Error("session unavailable");
21547
+ return `opencode --session ${value}`;
21548
+ }
21549
+ function configsEqual(left, right) {
21550
+ return JSON.stringify(left) === JSON.stringify(right);
21551
+ }
21552
+ function resolveDashboardConfigState(runtimeAvailable, restartRequired, managedRestart) {
21553
+ if (!runtimeAvailable) return "foreground";
21554
+ if (!restartRequired) return "applied";
21555
+ return managedRestart ? "pending" : "manual";
21556
+ }
21557
+ function isSafeDashboardRestartTarget(diagnostic, currentPid) {
21558
+ return diagnostic.pm2Installed && diagnostic.processFound && diagnostic.status === "online" && diagnostic.pid === currentPid && diagnostic.ready && diagnostic.scopeMatches;
21559
+ }
21560
+ function canRestartCurrentGateway() {
21561
+ if (runtimeConfig === null) return false;
21562
+ try {
21563
+ return isSafeDashboardRestartTarget(getGatewayDiagnostic(), process.pid);
21564
+ } catch {
21565
+ return false;
21566
+ }
21567
+ }
20638
21568
  function parsePositiveInteger(value) {
20639
21569
  if (!/^\d+$/.test(value)) return null;
20640
21570
  const parsed = Number(value);
@@ -20643,6 +21573,134 @@ function parsePositiveInteger(value) {
20643
21573
  function parseTaskStatus(value) {
20644
21574
  return TASK_STATUSES.has(value) ? value : null;
20645
21575
  }
21576
+ function parseTaskPayload(value) {
21577
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
21578
+ throw new Error("\u8BF7\u6C42\u5185\u5BB9\u5FC5\u987B\u662F\u5BF9\u8C61");
21579
+ }
21580
+ const input = value;
21581
+ const requiredString = (name) => {
21582
+ const field = input[name];
21583
+ if (typeof field !== "string" || !field.trim()) throw new Error(`${name} \u4E0D\u80FD\u4E3A\u7A7A`);
21584
+ return field.trim();
21585
+ };
21586
+ const optionalString = (name, fallback = null) => {
21587
+ const field = input[name];
21588
+ if (field === void 0 || field === null || field === "") return fallback;
21589
+ if (typeof field !== "string") throw new Error(`${name} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`);
21590
+ return field.trim() || fallback;
21591
+ };
21592
+ const integer2 = (name, fallback) => {
21593
+ const field = input[name];
21594
+ if (field === void 0 || field === null || field === "") return fallback;
21595
+ if (typeof field !== "number" || !Number.isSafeInteger(field)) {
21596
+ throw new Error(`${name} \u5FC5\u987B\u662F\u6574\u6570`);
21597
+ }
21598
+ return field;
21599
+ };
21600
+ const duration = (name, fallback, allowZero = false) => {
21601
+ const raw2 = optionalString(name, fallback);
21602
+ if (raw2 === null) return null;
21603
+ if (allowZero && raw2 === "0") return 0;
21604
+ if (Number.isFinite(Number(raw2))) {
21605
+ throw new Error(`${name} \u5FC5\u987B\u5E26\u65F6\u95F4\u5355\u4F4D\uFF0C\u8BF7\u4F7F\u7528 30s\u30015min\u30011h \u6216 2d`);
21606
+ }
21607
+ const milliseconds = parseDuration(raw2);
21608
+ if (milliseconds === null || !Number.isSafeInteger(milliseconds)) {
21609
+ throw new Error(`${name} \u683C\u5F0F\u65E0\u6548\uFF0C\u8BF7\u4F7F\u7528 30s\u30015min\u30011h \u6216 2d`);
21610
+ }
21611
+ return milliseconds;
21612
+ };
21613
+ return {
21614
+ name: requiredString("name"),
21615
+ cwd: requiredString("cwd"),
21616
+ agent: requiredString("agent"),
21617
+ model: optionalString("model", "default"),
21618
+ prompt: requiredString("prompt"),
21619
+ category: optionalString("category", "general"),
21620
+ batchId: optionalString("batchId"),
21621
+ importance: integer2("importance", 3),
21622
+ urgency: integer2("urgency", 3),
21623
+ maxRetries: integer2("maxRetries", 3),
21624
+ retryBackoffMs: duration("retryBackoff", "30s", true),
21625
+ timeoutMs: duration("timeout", null)
21626
+ };
21627
+ }
21628
+ function editableTaskPayload(input) {
21629
+ return {
21630
+ name: input.name,
21631
+ agent: input.agent,
21632
+ model: input.model ?? "default",
21633
+ prompt: input.prompt,
21634
+ category: input.category ?? "general",
21635
+ batchId: input.batchId ?? null,
21636
+ importance: input.importance ?? 3,
21637
+ urgency: input.urgency ?? 3,
21638
+ maxRetries: input.maxRetries ?? 3,
21639
+ retryBackoffMs: input.retryBackoffMs ?? 3e4,
21640
+ timeoutMs: input.timeoutMs ?? null
21641
+ };
21642
+ }
21643
+ function parseTemplatePayload(value) {
21644
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
21645
+ throw new Error("\u8BF7\u6C42\u5185\u5BB9\u5FC5\u987B\u662F\u5BF9\u8C61");
21646
+ }
21647
+ const input = value;
21648
+ const requiredString = (name) => {
21649
+ const field = input[name];
21650
+ if (typeof field !== "string" || !field.trim()) throw new Error(`${name} \u4E0D\u80FD\u4E3A\u7A7A`);
21651
+ return field.trim();
21652
+ };
21653
+ const optionalString = (name, fallback = null) => {
21654
+ const field = input[name];
21655
+ if (field === void 0 || field === null || field === "") return fallback;
21656
+ if (typeof field !== "string") throw new Error(`${name} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`);
21657
+ return field.trim() || fallback;
21658
+ };
21659
+ const integer2 = (name, fallback) => {
21660
+ const field = input[name];
21661
+ if (field === void 0 || field === null || field === "") return fallback;
21662
+ if (typeof field !== "number" || !Number.isSafeInteger(field)) {
21663
+ throw new Error(`${name} \u5FC5\u987B\u662F\u6574\u6570`);
21664
+ }
21665
+ return field;
21666
+ };
21667
+ const duration = (name, fallback, allowZero = false) => {
21668
+ const raw2 = optionalString(name, fallback);
21669
+ if (raw2 === null) return null;
21670
+ if (allowZero && raw2 === "0") return 0;
21671
+ if (Number.isFinite(Number(raw2))) {
21672
+ throw new Error(`${name} \u5FC5\u987B\u5E26\u65F6\u95F4\u5355\u4F4D\uFF0C\u8BF7\u4F7F\u7528 30s\u30015min\u30011h \u6216 2d`);
21673
+ }
21674
+ const milliseconds = parseDuration(raw2);
21675
+ if (milliseconds === null || !Number.isSafeInteger(milliseconds)) {
21676
+ throw new Error(`${name} \u683C\u5F0F\u65E0\u6548\uFF0C\u8BF7\u4F7F\u7528 30s\u30015min\u30011h \u6216 2d`);
21677
+ }
21678
+ return milliseconds;
21679
+ };
21680
+ const scheduleType = requiredString("scheduleType");
21681
+ if (!["cron", "delayed", "recurring"].includes(scheduleType)) {
21682
+ throw new Error("scheduleType \u5FC5\u987B\u662F cron\u3001delayed \u6216 recurring");
21683
+ }
21684
+ return {
21685
+ name: requiredString("name"),
21686
+ agent: requiredString("agent"),
21687
+ model: optionalString("model", "default"),
21688
+ prompt: requiredString("prompt"),
21689
+ cwd: requiredString("cwd"),
21690
+ category: optionalString("category", "general"),
21691
+ importance: integer2("importance", 3),
21692
+ urgency: integer2("urgency", 3),
21693
+ batchId: optionalString("batchId"),
21694
+ scheduleType,
21695
+ cronExpr: scheduleType === "cron" ? requiredString("cronExpr") : null,
21696
+ intervalMs: scheduleType === "recurring" ? duration("interval", null) : null,
21697
+ runAt: scheduleType === "delayed" ? integer2("runAt", null) : null,
21698
+ maxInstances: integer2("maxInstances", 1),
21699
+ maxRetries: integer2("maxRetries", 3),
21700
+ retryBackoffMs: duration("retryBackoff", "30s", true),
21701
+ timeoutMs: duration("timeout", null)
21702
+ };
21703
+ }
20646
21704
  function safeStatus(value) {
20647
21705
  return value && TASK_STATUSES.has(value) ? value : "unknown";
20648
21706
  }
@@ -20677,19 +21735,19 @@ function esc(value) {
20677
21735
  }
20678
21736
  function readCurrentConfig() {
20679
21737
  const configPath = getConfigPath();
20680
- if (!existsSync6(configPath)) return {};
21738
+ if (!existsSync7(configPath)) return {};
20681
21739
  try {
20682
- return JSON.parse(readFileSync3(configPath, "utf-8"));
21740
+ return JSON.parse(readFileSync4(configPath, "utf-8"));
20683
21741
  } catch {
20684
21742
  return {};
20685
21743
  }
20686
21744
  }
20687
21745
  function writeConfig(cfg) {
20688
21746
  const configPath = getConfigPath();
20689
- const dir = dirname6(configPath);
20690
- if (!existsSync6(dir)) mkdirSync3(dir, { recursive: true });
21747
+ const dir = dirname7(configPath);
21748
+ if (!existsSync7(dir)) mkdirSync4(dir, { recursive: true });
20691
21749
  const tempPath = `${configPath}.${process.pid}.tmp`;
20692
- writeFileSync2(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
21750
+ writeFileSync3(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
20693
21751
  renameSync2(tempPath, configPath);
20694
21752
  }
20695
21753
  function statCard(value, label, tone, cardIcon, delay = "") {
@@ -20722,13 +21780,14 @@ function pagination(locale, basePath, page, pages, total, suffix = "") {
20722
21780
  const next = page < pages ? `<a class="btn" href="${basePath}?page=${page + 1}${suffix}">${t(locale, "pagination.next")}${icon("chevronRight")}</a>` : "";
20723
21781
  return `<div class="pagination">${previous}<span class="summary">${t(locale, "pagination.summary", { page, pages, total })}</span>${next}</div>`;
20724
21782
  }
20725
- var app, TASK_STATUSES, dashboardApp, web_default;
21783
+ var app, LEGACY_PROJECT_FILTER, TASK_STATUSES, SESSION_ID_PATTERN, runtimeConfig, restartScheduled, dashboardApp, web_default;
20726
21784
  var init_web = __esm({
20727
21785
  "src/web/index.tsx"() {
20728
21786
  "use strict";
20729
21787
  init_dist();
20730
21788
  init_drizzle_orm();
20731
21789
  init_db2();
21790
+ init_duration();
20732
21791
  init_database_maintenance_service();
20733
21792
  init_task_run_service();
20734
21793
  init_task_service();
@@ -20736,8 +21795,10 @@ var init_web = __esm({
20736
21795
  init_config();
20737
21796
  init_health();
20738
21797
  init_job_templates();
21798
+ init_pm2();
20739
21799
  init_ui();
20740
21800
  app = new Hono2();
21801
+ LEGACY_PROJECT_FILTER = "__supertask_legacy__";
20741
21802
  TASK_STATUSES = /* @__PURE__ */ new Set([
20742
21803
  "pending",
20743
21804
  "running",
@@ -20746,6 +21807,9 @@ var init_web = __esm({
20746
21807
  "dead_letter",
20747
21808
  "cancelled"
20748
21809
  ]);
21810
+ SESSION_ID_PATTERN = /^ses_[A-Za-z0-9_]+$/;
21811
+ runtimeConfig = null;
21812
+ restartScheduled = false;
20749
21813
  app.use("*", async (c, next) => {
20750
21814
  await next();
20751
21815
  c.header("X-Content-Type-Options", "nosniff");
@@ -20782,24 +21846,52 @@ var init_web = __esm({
20782
21846
  const page = parsePositiveInteger(c.req.query("page") || "1");
20783
21847
  if (page === null) return c.text("invalid page", 400);
20784
21848
  const statusFilter = c.req.query("status") || "";
21849
+ const requestedCwd = c.req.query("cwd") || "";
21850
+ const legacyProjectFilter = requestedCwd === LEGACY_PROJECT_FILTER;
21851
+ const cwdFilter = legacyProjectFilter ? "" : requestedCwd;
21852
+ const projectFilter = legacyProjectFilter ? LEGACY_PROJECT_FILTER : cwdFilter;
20785
21853
  const parsedStatus = statusFilter ? parseTaskStatus(statusFilter) : null;
20786
21854
  if (statusFilter && !parsedStatus) return c.text("invalid status", 400);
20787
21855
  const limit = 50;
20788
21856
  const offset = (page - 1) * limit;
20789
- const [tasks4, statsData] = await Promise.all([
20790
- TaskService.list({ limit, offset, ...parsedStatus ? { status: parsedStatus } : {} }),
20791
- TaskService.stats({})
21857
+ const [tasks4, statsData, globalStats, projects, globalRunning, legacyStats, legacyRunning] = await Promise.all([
21858
+ TaskService.list({
21859
+ limit,
21860
+ offset,
21861
+ ...parsedStatus === "running" ? { activeExecution: true } : parsedStatus ? { status: parsedStatus } : {},
21862
+ ...legacyProjectFilter ? { legacyCwd: true } : cwdFilter ? { cwd: cwdFilter } : {}
21863
+ }),
21864
+ TaskService.stats(legacyProjectFilter ? { legacyCwd: true } : cwdFilter ? { cwd: cwdFilter } : {}),
21865
+ TaskService.stats(),
21866
+ TaskService.projectSummaries(1e3),
21867
+ TaskService.countRunning(),
21868
+ TaskService.stats({ legacyCwd: true }),
21869
+ TaskService.countRunning({ legacyCwd: true })
20792
21870
  ]);
20793
21871
  const latestRuns = await TaskRunService.getLatestByTaskIds(tasks4.map((task) => task.id));
21872
+ const selectedRunning = legacyProjectFilter ? legacyRunning : cwdFilter ? projects.find((project) => project.cwd === cwdFilter)?.running ?? 0 : globalRunning;
20794
21873
  const counts = {
20795
21874
  pending: statsData.pending || 0,
20796
- running: statsData.running || 0,
21875
+ running: selectedRunning,
20797
21876
  done: statsData.done || 0,
20798
21877
  failed: (statsData.failed || 0) + (statsData.dead_letter || 0),
20799
21878
  total: statsData.total || 0
20800
21879
  };
20801
- const filteredTotal = parsedStatus ? Number(statsData[parsedStatus] ?? 0) : counts.total;
21880
+ const filteredTotal = parsedStatus === "running" ? selectedRunning : parsedStatus ? Number(statsData[parsedStatus] ?? 0) : counts.total;
20802
21881
  const totalPages = Math.max(1, Math.ceil(filteredTotal / limit));
21882
+ if (page > totalPages) {
21883
+ const params = new URLSearchParams({ page: String(totalPages) });
21884
+ if (statusFilter) params.set("status", statusFilter);
21885
+ if (projectFilter) params.set("cwd", projectFilter);
21886
+ return c.redirect(`/?${params.toString()}`);
21887
+ }
21888
+ const taskListUrl = (status, cwd) => {
21889
+ const params = new URLSearchParams();
21890
+ if (status) params.set("status", status);
21891
+ if (cwd) params.set("cwd", cwd);
21892
+ const query = params.toString();
21893
+ return query ? `/?${query}` : "/";
21894
+ };
20803
21895
  const filterItems = [
20804
21896
  { status: "", label: t(locale, "filter.all") },
20805
21897
  { status: "pending", label: statusText(locale, "pending") },
@@ -20810,22 +21902,27 @@ var init_web = __esm({
20810
21902
  { status: "cancelled", label: statusText(locale, "cancelled") }
20811
21903
  ];
20812
21904
  const filters = filterItems.map(({ status, label }) => {
20813
- const href = status ? `/?status=${status}` : "/";
20814
- return `<a href="${href}" class="filter-chip ${statusFilter === status ? "active" : ""}">${label}</a>`;
21905
+ const href = taskListUrl(status, projectFilter);
21906
+ return `<a href="${esc(href)}" class="filter-chip ${statusFilter === status ? "active" : ""}">${label}</a>`;
20815
21907
  }).join("");
20816
21908
  const rows = tasks4.map((task) => {
20817
21909
  const status = safeStatus(task.status);
20818
- const executionActive = latestRuns.get(task.id)?.status === "running";
20819
- const searchable = esc(`${task.name} ${task.agent} ${task.prompt}`);
21910
+ const latestRun = latestRuns.get(task.id);
21911
+ const executionActive = latestRun?.status === "running";
21912
+ const batchId = task.batchId?.trim() || null;
21913
+ const searchable = esc(`${task.name} ${task.agent} ${task.prompt} ${task.cwd ?? ""} ${task.batchId ?? ""} ${task.category ?? ""}`);
20820
21914
  return `<tr data-task-row data-search="${searchable}">
20821
21915
  <td class="faint" data-label="${t(locale, "table.id")}">#${task.id}</td>
20822
- <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>
21916
+ <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>
21917
+ <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>
20823
21918
  <td data-label="${t(locale, "table.agent")}"><span class="tag">${esc(task.agent)}</span></td>
20824
- <td data-label="${t(locale, "table.status")}"><span class="badge b-${status}">${statusText(locale, status)}</span></td>
20825
- <td data-label="${t(locale, "table.duration")}" class="small ${task.status === "running" ? "" : "muted"}">${formatDuration(task.startedAt, task.finishedAt)}</td>
21919
+ <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>
21920
+ <td data-label="${t(locale, "table.duration")}" class="small ${executionActive || task.status === "running" ? "" : "muted"}">${formatDuration(task.startedAt, task.finishedAt)}</td>
20826
21921
  <td data-label="${t(locale, "table.retries")}" class="muted small">${(task.retryCount ?? 0) > 0 ? task.retryCount : "\u2014"}</td>
20827
21922
  <td data-label="${t(locale, "table.actions")}"><div class="actions">
21923
+ ${task.cwd?.trim() && ["pending", "failed", "dead_letter"].includes(task.status ?? "") ? `<button type="button" class="btn" onclick="openTaskEditor(${task.id})">${t(locale, "action.edit")}</button>` : ""}
20828
21924
  <button type="button" class="btn" onclick="showDetail(${task.id})">${t(locale, "action.details")}</button>
21925
+ ${isValidSessionId(latestRun?.sessionId) ? `<button type="button" class="btn" onclick="copySessionCommand(${latestRun.id})">${icon("copy")}${t(locale, "action.continueSession")}</button>` : ""}
20829
21926
  ${task.status === "failed" || task.status === "dead_letter" ? `<button type="button" class="btn btn-warning" onclick="retryTask(${task.id})">${t(locale, "action.retry")}</button>` : ""}
20830
21927
  ${["pending", "running", "failed"].includes(task.status ?? "") ? `<button type="button" class="btn btn-warning" onclick="cancelTask(${task.id})">${t(locale, "action.cancel")}</button>` : ""}
20831
21928
  ${task.status === "running" || executionActive ? "" : `<button type="button" class="btn btn-danger" onclick="deleteTask(${task.id})">${t(locale, "action.delete")}</button>`}
@@ -20837,7 +21934,32 @@ var init_web = __esm({
20837
21934
  <tbody>${rows}</tbody>
20838
21935
  </table></div>
20839
21936
  <div id="search-empty" hidden>${emptyState(t(locale, "filter.noResults"), "")}</div>`;
20840
- const suffix = statusFilter ? `&status=${statusFilter}` : "";
21937
+ const paginationParts = [];
21938
+ if (statusFilter) paginationParts.push(`status=${encodeURIComponent(statusFilter)}`);
21939
+ if (projectFilter) paginationParts.push(`cwd=${encodeURIComponent(projectFilter)}`);
21940
+ const suffix = paginationParts.length > 0 ? `&${paginationParts.join("&")}` : "";
21941
+ const projectCards = [
21942
+ `<a class="project-card ${projectFilter ? "" : "active"}" href="${esc(taskListUrl(statusFilter, ""))}">
21943
+ <div class="project-card-head"><strong>${t(locale, "projects.all")}</strong><span>${globalStats.total || 0}</span></div>
21944
+ <div class="project-counts">${t(locale, "projects.counts", { running: globalRunning, pending: globalStats.pending || 0, failed: (globalStats.failed || 0) + (globalStats.dead_letter || 0) })}</div>
21945
+ </a>`,
21946
+ ...legacyStats.total > 0 ? [`<a class="project-card ${legacyProjectFilter ? "active" : ""}" href="${esc(taskListUrl(statusFilter, LEGACY_PROJECT_FILTER))}">
21947
+ <div class="project-card-head"><strong>${t(locale, "projects.legacy")}</strong><span>${legacyStats.total}</span></div>
21948
+ <div class="project-path">${t(locale, "projects.legacyHint")}</div>
21949
+ <div class="project-counts">${t(locale, "projects.counts", { running: legacyRunning, pending: legacyStats.pending || 0, failed: (legacyStats.failed || 0) + (legacyStats.dead_letter || 0) })}</div>
21950
+ </a>`] : [],
21951
+ ...projects.map((project) => `<a class="project-card ${cwdFilter === project.cwd ? "active" : ""}" href="${esc(taskListUrl(statusFilter, project.cwd))}" title="${esc(project.cwd)}">
21952
+ <div class="project-card-head"><strong>${esc(basename3(project.cwd) || project.cwd)}</strong><span>${project.total}</span></div>
21953
+ <div class="project-path">${esc(project.cwd)}</div>
21954
+ <div class="project-counts">${t(locale, "projects.counts", { running: project.running, pending: project.pending, failed: project.failed })}</div>
21955
+ </a>`)
21956
+ ].join("");
21957
+ const projectData = JSON.stringify(Object.fromEntries(projects.map((project) => [project.cwd, {
21958
+ total: project.total,
21959
+ pending: project.pending,
21960
+ running: project.running,
21961
+ failed: project.failed
21962
+ }]))).replace(/</g, "\\u003c");
20841
21963
  const body = `
20842
21964
  <div class="stats-grid">
20843
21965
  ${statCard(counts.pending, t(locale, "stats.pending"), "tone-neutral", icon("clock"))}
@@ -20845,19 +21967,61 @@ var init_web = __esm({
20845
21967
  ${statCard(counts.done, t(locale, "stats.done"), "tone-green", icon("check"), "reveal-delay-1")}
20846
21968
  ${statCard(counts.failed, t(locale, "stats.failedDead"), "tone-red", icon("alert"), "reveal-delay-2")}
20847
21969
  </div>
21970
+ <section class="panel project-panel reveal reveal-delay-1">
21971
+ <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>
21972
+ <div class="project-grid">${projectCards}</div>
21973
+ </section>
20848
21974
  <div class="toolbar reveal reveal-delay-1">
20849
21975
  <div class="filters">${filters}</div>
20850
21976
  <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>
20851
21977
  </div>
20852
21978
  <section class="panel reveal reveal-delay-2">${table}</section>
20853
- ${pagination(locale, "/", page, totalPages, filteredTotal, suffix)}`;
21979
+ ${pagination(locale, "/", page, totalPages, filteredTotal, suffix)}
21980
+ <script type="application/json" id="task-project-data">${projectData}</script>
21981
+ <dialog id="task-dialog" class="template-dialog">
21982
+ <form id="task-form" data-default-cwd="${esc(cwdFilter)}" onsubmit="saveTask(event)">
21983
+ <input id="task-id" type="hidden">
21984
+ <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>
21985
+ <div class="dialog-body">
21986
+ <div class="template-form-grid">
21987
+ <label class="form-field"><span>${t(locale, "template.name")}</span><input id="task-name" required maxlength="200" autocomplete="off"></label>
21988
+ <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>
21989
+ <datalist id="task-cwd-options">${projects.map((project) => `<option value="${esc(project.cwd)}"></option>`).join("")}</datalist>
21990
+ <label class="form-field"><span>${t(locale, "template.agent")}</span><input id="task-agent" required autocomplete="off" value="build" placeholder="build"></label>
21991
+ <label class="form-field"><span>${t(locale, "template.model")}</span><input id="task-model" required autocomplete="off" value="default" placeholder="default"></label>
21992
+ <label class="form-field form-field-wide"><span>${t(locale, "template.prompt")}</span><textarea id="task-prompt" rows="6" required></textarea></label>
21993
+ </div>
21994
+ <p id="task-project-status" class="form-note"></p>
21995
+ <details class="advanced-fields">
21996
+ <summary>${t(locale, "template.advanced")}</summary>
21997
+ <div class="template-form-grid">
21998
+ <label class="form-field"><span>${t(locale, "template.category")}</span><input id="task-category" autocomplete="off" value="general"></label>
21999
+ <label class="form-field"><span>${t(locale, "template.batchId")}</span><input id="task-batch" autocomplete="off"><small>${t(locale, "task.batchHint")}</small></label>
22000
+ <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>
22001
+ <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>
22002
+ <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>
22003
+ <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>
22004
+ <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>
22005
+ </div>
22006
+ </details>
22007
+ </div>
22008
+ <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>
22009
+ </form>
22010
+ </dialog>`;
20854
22011
  return c.html(renderLayout({ locale, activeTab: "tasks", body }));
20855
22012
  });
20856
22013
  app.get("/templates", async (c) => {
20857
22014
  const locale = resolveLocale(c);
20858
- const templates = await TaskTemplateService.list(100);
20859
- const enabled = templates.filter((template) => template.enabled).length;
20860
- const disabled = templates.length - enabled;
22015
+ const page = parsePositiveInteger(c.req.query("page") || "1");
22016
+ if (page === null) return c.text("invalid page", 400);
22017
+ const limit = 50;
22018
+ const offset = (page - 1) * limit;
22019
+ const [templates, templateStats] = await Promise.all([
22020
+ TaskTemplateService.list(limit, offset),
22021
+ TaskTemplateService.stats()
22022
+ ]);
22023
+ const totalPages = Math.max(1, Math.ceil(templateStats.total / limit));
22024
+ if (page > totalPages) return c.redirect(`/templates?page=${totalPages}`);
20861
22025
  const rows = templates.map((template) => {
20862
22026
  const scheduleType = ["cron", "recurring", "delayed"].includes(template.scheduleType) ? template.scheduleType : "unknown";
20863
22027
  const typeLabel = scheduleType === "cron" ? t(locale, "schedule.cron") : scheduleType === "recurring" ? t(locale, "schedule.recurring") : scheduleType === "delayed" ? t(locale, "schedule.delayed") : t(locale, "schedule.unknown");
@@ -20875,21 +22039,56 @@ var init_web = __esm({
20875
22039
  <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>
20876
22040
  <td data-label="${t(locale, "table.lastRun")}" class="small muted">${formatRelative(template.lastRunAt, locale)}</td>
20877
22041
  <td data-label="${t(locale, "table.nextRun")}" class="small">${formatFuture(template.nextRunAt, locale)}</td>
20878
- <td data-label="${t(locale, "table.actions")}"><div class="actions"><button type="button" class="btn" onclick="showTemplateDetail(${template.id})">${t(locale, "action.details")}</button>
22042
+ <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>
20879
22043
  <button type="button" class="btn btn-primary" onclick="triggerTmpl(${template.id})">${t(locale, "action.trigger")}</button>${toggle}
20880
22044
  <button type="button" class="btn btn-danger" onclick="deleteTmpl(${template.id})">${t(locale, "action.delete")}</button></div></td>
20881
22045
  </tr>`;
20882
22046
  }).join("");
20883
22047
  const body = `
20884
22048
  <div class="stats-grid three">
20885
- ${statCard(templates.length, t(locale, "stats.templates"), "tone-purple", icon("templates"))}
20886
- ${statCard(enabled, t(locale, "stats.enabled"), "tone-green", icon("check"), "reveal-delay-1")}
20887
- ${statCard(disabled, t(locale, "stats.disabled"), "tone-neutral", icon("clock"), "reveal-delay-2")}
22049
+ ${statCard(templateStats.total, t(locale, "stats.templates"), "tone-purple", icon("templates"))}
22050
+ ${statCard(templateStats.enabled, t(locale, "stats.enabled"), "tone-green", icon("check"), "reveal-delay-1")}
22051
+ ${statCard(templateStats.disabled, t(locale, "stats.disabled"), "tone-neutral", icon("clock"), "reveal-delay-2")}
20888
22052
  </div>
20889
22053
  <section class="panel reveal reveal-delay-2">
20890
- <div class="panel-head"><h2>${t(locale, "page.templates.title")}</h2></div>
20891
- ${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>`}
20892
- </section>`;
22054
+ <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>
22055
+ ${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>`}
22056
+ </section>
22057
+ <dialog id="template-dialog" class="template-dialog">
22058
+ <form id="template-form" onsubmit="saveTemplate(event)">
22059
+ <input id="template-id" type="hidden">
22060
+ <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>
22061
+ <div class="dialog-body">
22062
+ <div class="template-form-grid">
22063
+ <label class="form-field"><span>${t(locale, "template.name")}</span><input id="template-name" required maxlength="200" autocomplete="off"></label>
22064
+ <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>
22065
+ <label class="form-field"><span>${t(locale, "template.agent")}</span><input id="template-agent" required autocomplete="off" placeholder="build"></label>
22066
+ <label class="form-field"><span>${t(locale, "template.model")}</span><input id="template-model" required autocomplete="off" value="default" placeholder="default"></label>
22067
+ <label class="form-field form-field-wide"><span>${t(locale, "template.prompt")}</span><textarea id="template-prompt" rows="6" required></textarea></label>
22068
+ <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>
22069
+ <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>
22070
+ <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>
22071
+ <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>
22072
+ </div>
22073
+ <details class="advanced-fields">
22074
+ <summary>${t(locale, "template.advanced")}</summary>
22075
+ <div class="template-form-grid">
22076
+ <label class="form-field"><span>${t(locale, "template.category")}</span><input id="template-category" autocomplete="off" value="general"></label>
22077
+ <label class="form-field"><span>${t(locale, "template.batchId")}</span><input id="template-batch" autocomplete="off"><small>${t(locale, "template.optional")}</small></label>
22078
+ <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>
22079
+ <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>
22080
+ <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>
22081
+ <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>
22082
+ <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>
22083
+ <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>
22084
+ </div>
22085
+ </details>
22086
+ <p class="form-note">${t(locale, "template.futureOnly")}</p>
22087
+ </div>
22088
+ <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>
22089
+ </form>
22090
+ </dialog>
22091
+ ${pagination(locale, "/templates", page, totalPages, templateStats.total)}`;
20893
22092
  return c.html(renderLayout({ locale, activeTab: "templates", body }));
20894
22093
  });
20895
22094
  app.get("/runs", async (c) => {
@@ -20917,9 +22116,11 @@ var init_web = __esm({
20917
22116
  const totalResult = await db.select({ count: sql`count(*)` }).from(taskRuns4);
20918
22117
  const total = Number(totalResult[0]?.count ?? 0);
20919
22118
  const totalPages = Math.max(1, Math.ceil(total / limit));
22119
+ if (page > totalPages) return c.redirect(`/runs?page=${totalPages}`);
20920
22120
  const logs = [];
20921
22121
  const rows = runs.map((run) => {
20922
22122
  const status = safeStatus(run.status);
22123
+ const resumable = isValidSessionId(run.sessionId);
20923
22124
  if (run.log) {
20924
22125
  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>`);
20925
22126
  }
@@ -20927,10 +22128,12 @@ var init_web = __esm({
20927
22128
  <td class="faint" data-label="${t(locale, "table.run")}">#${run.id}</td>
20928
22129
  <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>
20929
22130
  <td data-label="${t(locale, "table.agent")}"><span class="tag">${esc(run.taskAgent)}</span></td>
20930
- <td data-label="${t(locale, "table.status")}"><span class="badge b-${status}">${statusText(locale, status)}</span></td>
22131
+ <td data-label="${t(locale, "table.session")}" class="m small">${esc(maskSessionId(run.sessionId))}</td>
22132
+ <td data-label="${t(locale, "table.status")}"><span class="badge b-${status}">${runStatusText(locale, run.status ?? "unknown")}</span></td>
20931
22133
  <td data-label="${t(locale, "table.duration")}" class="small">${formatDuration(run.startedAt, run.finishedAt)}</td>
20932
22134
  <td data-label="${t(locale, "table.heartbeat")}" class="small muted">${formatRelative(run.heartbeatAt, locale)}</td>
20933
22135
  <td data-label="${t(locale, "table.actions")}"><div class="actions"><button type="button" class="btn" onclick="showRunDetail(${run.id})">${t(locale, "action.details")}</button>
22136
+ ${resumable ? `<button type="button" class="btn" onclick="copySessionCommand(${run.id})">${icon("copy")}${t(locale, "action.continueSession")}</button>` : ""}
20934
22137
  ${run.log ? `<button type="button" class="btn" aria-expanded="false" onclick="toggleLog(${run.id},this)">${t(locale, "action.logs")}</button>` : ""}</div></td>
20935
22138
  </tr>`;
20936
22139
  }).join("");
@@ -20941,22 +22144,37 @@ var init_web = __esm({
20941
22144
  ${statCard(runs.filter((run) => run.status === "failed").length, t(locale, "stats.pageFailed"), "tone-red", icon("alert"), "reveal-delay-1")}
20942
22145
  ${statCard(runs.filter((run) => run.status === "running").length, t(locale, "stats.pageRunning"), "tone-blue", icon("activity"), "reveal-delay-2")}
20943
22146
  </div>
20944
- <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>
22147
+ <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>
20945
22148
  ${logs.join("")}${pagination(locale, "/runs", page, totalPages, total)}`;
20946
22149
  return c.html(renderLayout({ locale, activeTab: "runs", body }));
20947
22150
  });
20948
22151
  app.get("/system", async (c) => {
20949
22152
  const locale = resolveLocale(c);
20950
22153
  const config = loadConfig();
22154
+ const activeConfig = runtimeConfig ?? config;
22155
+ const restartRequired = runtimeConfig !== null && !configsEqual(config, runtimeConfig);
22156
+ const managedRestart = canRestartCurrentGateway();
22157
+ const configState = resolveDashboardConfigState(
22158
+ runtimeConfig !== null,
22159
+ restartRequired,
22160
+ managedRestart
22161
+ );
22162
+ const configStateKey = {
22163
+ foreground: "system.configForeground",
22164
+ applied: "system.configApplied",
22165
+ pending: "system.configPending",
22166
+ manual: "system.configRestartManually"
22167
+ }[configState];
22168
+ const configStateText = t(locale, configStateKey);
20951
22169
  const configPath = getConfigPath();
20952
- const [stats, runningRuns, templates] = await Promise.all([
22170
+ const [stats, runningRuns, templateStats] = await Promise.all([
20953
22171
  TaskService.stats({}),
20954
22172
  TaskRunService.getAllRunningRuns(),
20955
- TaskTemplateService.list(100)
22173
+ TaskTemplateService.stats()
20956
22174
  ]);
20957
- const configExists = existsSync6(configPath);
22175
+ const configExists = existsSync7(configPath);
20958
22176
  const runRows = runningRuns.map((run) => {
20959
- const session = run.sessionId ? `${run.sessionId.slice(4, 7)}***${run.sessionId.slice(-3)}` : "\u2014";
22177
+ const session = maskSessionId(run.sessionId);
20960
22178
  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>
20961
22179
  <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>
20962
22180
  <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>
@@ -20972,10 +22190,10 @@ var init_web = __esm({
20972
22190
  <div class="field"><label for="hi">${t(locale, "system.heartbeatInterval")}</label>${unitInput("hi", config.worker.heartbeatIntervalMs / 1e3, 5, t(locale, "system.seconds"))}</div>
20973
22191
  <div class="field"><label for="to">${t(locale, "system.taskTimeout")}</label>${unitInput("to", config.worker.taskTimeoutMs / 6e4, 1, t(locale, "system.minutes"))}</div>
20974
22192
  </section>
20975
- <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>
22193
+ <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>
20976
22194
  <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>
20977
22195
  <div class="field"><label for="si">${t(locale, "system.checkInterval")}</label>${unitInput("si", config.scheduler.checkIntervalMs, 100, t(locale, "system.milliseconds"))}</div>
20978
- <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>
22196
+ <div class="info-row"><span class="info-key">${t(locale, "system.activeTemplates")}</span><span class="info-value">${templateStats.enabled} / ${templateStats.total}</span></div>
20979
22197
  </section>
20980
22198
  <section class="card settings-card reveal-delay-2"><h2 class="settings-title"><span>${icon("system")}${t(locale, "system.watchdog")}</span></h2>
20981
22199
  <div class="field"><label for="wt">${t(locale, "system.heartbeatTimeout")}</label>${unitInput("wt", config.watchdog.heartbeatTimeoutMs / 1e3, 10, t(locale, "system.seconds"))}</div>
@@ -20984,10 +22202,10 @@ var init_web = __esm({
20984
22202
  <div class="field"><label for="rd">${t(locale, "system.retentionDays")}</label>${unitInput("rd", config.watchdog.retentionDays, 1, t(locale, "system.days"))}</div>
20985
22203
  </section>
20986
22204
  </div>
20987
- <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>
22205
+ <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>
20988
22206
  </form>
20989
22207
  <section class="panel reveal">
20990
- <div class="panel-head"><h2>${t(locale, "system.runningTasks", { running: runningRuns.length, limit: config.worker.maxConcurrency })}</h2></div>
22208
+ <div class="panel-head"><h2>${t(locale, "system.runningTasks", { running: runningRuns.length, limit: activeConfig.worker.maxConcurrency })}</h2></div>
20991
22209
  ${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>`}
20992
22210
  </section>
20993
22211
  <section class="panel reveal reveal-delay-1"><div class="panel-head"><h2>${t(locale, "system.taskStats")}</h2></div><div class="overview-grid">
@@ -21004,6 +22222,30 @@ var init_web = __esm({
21004
22222
  <button type="button" class="btn btn-danger" onclick="clearDatabase()">${icon("database")}${t(locale, "action.clearDatabase")}</button></section>`;
21005
22223
  return c.html(renderLayout({ locale, activeTab: "system", body }));
21006
22224
  });
22225
+ app.post("/api/tasks", async (c) => {
22226
+ try {
22227
+ const task = await TaskService.add(parseTaskPayload(await c.req.json()));
22228
+ return c.json({ success: true, task }, 201);
22229
+ } catch (error) {
22230
+ return c.json({
22231
+ error: error instanceof Error ? error.message : String(error)
22232
+ }, 400);
22233
+ }
22234
+ });
22235
+ app.put("/api/tasks/:id", async (c) => {
22236
+ const id = parsePositiveInteger(c.req.param("id"));
22237
+ if (id === null) return c.json({ error: "invalid id" }, 400);
22238
+ try {
22239
+ const input = parseTaskPayload(await c.req.json());
22240
+ const task = await TaskService.update(id, editableTaskPayload(input));
22241
+ if (task) return c.json({ success: true, task });
22242
+ return await TaskService.getById(id) ? c.json({ error: "task status does not allow editing" }, 409) : c.json({ error: "not found" }, 404);
22243
+ } catch (error) {
22244
+ return c.json({
22245
+ error: error instanceof Error ? error.message : String(error)
22246
+ }, 400);
22247
+ }
22248
+ });
21007
22249
  app.get("/api/tasks/:id", async (c) => {
21008
22250
  const id = parsePositiveInteger(c.req.param("id"));
21009
22251
  if (id === null) return c.json({ error: "invalid id" }, 400);
@@ -21019,6 +22261,24 @@ var init_web = __esm({
21019
22261
  if (!run) return c.json({ error: "not found" }, 404);
21020
22262
  return c.json(run);
21021
22263
  });
22264
+ app.get("/api/runs/:id/session-command", async (c) => {
22265
+ const id = parsePositiveInteger(c.req.param("id"));
22266
+ if (id === null) return c.json({ error: "invalid id" }, 400);
22267
+ const run = await TaskRunService.getById(id);
22268
+ if (!run) return c.json({ error: "not found" }, 404);
22269
+ if (!isValidSessionId(run.sessionId)) return c.json({ error: "session unavailable" }, 409);
22270
+ return c.json({ command: sessionCommand(run.sessionId) });
22271
+ });
22272
+ app.get("/api/gateway/status", (c) => {
22273
+ const savedConfig = loadConfig();
22274
+ const managed = canRestartCurrentGateway();
22275
+ return c.json({
22276
+ pid: process.pid,
22277
+ managed,
22278
+ ready: managed && getGatewayHealth().status === "ok",
22279
+ restartRequired: runtimeConfig === null || !configsEqual(savedConfig, runtimeConfig)
22280
+ });
22281
+ });
21022
22282
  app.get("/api/templates/:id", async (c) => {
21023
22283
  const id = parsePositiveInteger(c.req.param("id"));
21024
22284
  if (id === null) return c.json({ error: "invalid id" }, 400);
@@ -21026,6 +22286,30 @@ var init_web = __esm({
21026
22286
  if (!template) return c.json({ error: "not found" }, 404);
21027
22287
  return c.json(template);
21028
22288
  });
22289
+ app.post("/api/templates", async (c) => {
22290
+ try {
22291
+ const input = parseTemplatePayload(await c.req.json());
22292
+ const template = await TaskTemplateService.create(input);
22293
+ return c.json({ success: true, template }, 201);
22294
+ } catch (error) {
22295
+ return c.json({
22296
+ error: error instanceof Error ? error.message : String(error)
22297
+ }, 400);
22298
+ }
22299
+ });
22300
+ app.put("/api/templates/:id", async (c) => {
22301
+ const id = parsePositiveInteger(c.req.param("id"));
22302
+ if (id === null) return c.json({ error: "invalid id" }, 400);
22303
+ try {
22304
+ const input = parseTemplatePayload(await c.req.json());
22305
+ const template = await TaskTemplateService.update(id, input);
22306
+ return template ? c.json({ success: true, template }) : c.json({ error: "not found" }, 404);
22307
+ } catch (error) {
22308
+ return c.json({
22309
+ error: error instanceof Error ? error.message : String(error)
22310
+ }, 400);
22311
+ }
22312
+ });
21029
22313
  app.post("/api/tasks/:id/retry", async (c) => {
21030
22314
  const id = parsePositiveInteger(c.req.param("id"));
21031
22315
  if (id === null) return c.json({ error: "invalid id" }, 400);
@@ -21074,22 +22358,31 @@ var init_web = __esm({
21074
22358
  app.post("/api/templates/:id/trigger", async (c) => {
21075
22359
  const id = parsePositiveInteger(c.req.param("id"));
21076
22360
  if (id === null) return c.json({ error: "invalid id" }, 400);
21077
- const template = await TaskTemplateService.getById(id);
21078
- if (!template) return c.json({ error: "not found" }, 404);
21079
22361
  const task = await triggerTaskFromTemplate(id);
21080
- if (!task) return c.json({ error: "maxInstances reached" }, 409);
21081
- return c.json({ success: true, taskId: task.id });
22362
+ return task ? c.json({ success: true, taskId: task.id }) : c.json({ error: "not found" }, 404);
21082
22363
  });
21083
22364
  app.put("/api/config", async (c) => {
21084
22365
  try {
21085
- const body = await c.req.json();
22366
+ const rawBody = await c.req.json();
22367
+ if (!rawBody || typeof rawBody !== "object" || Array.isArray(rawBody)) {
22368
+ throw new Error("\u8BF7\u6C42\u5185\u5BB9\u5FC5\u987B\u662F\u5BF9\u8C61");
22369
+ }
22370
+ const body = rawBody;
22371
+ const section = (name) => {
22372
+ const value = body[name];
22373
+ if (value === void 0) return {};
22374
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
22375
+ throw new Error(`${name} \u5FC5\u987B\u662F\u5BF9\u8C61`);
22376
+ }
22377
+ return value;
22378
+ };
21086
22379
  const current = readCurrentConfig();
21087
22380
  const currentWorker = current.worker ?? {};
21088
22381
  const currentScheduler = current.scheduler ?? {};
21089
22382
  const currentWatchdog = current.watchdog ?? {};
21090
- const bodyWorker = body.worker ?? {};
21091
- const bodyScheduler = body.scheduler ?? {};
21092
- const bodyWatchdog = body.watchdog ?? {};
22383
+ const bodyWorker = section("worker");
22384
+ const bodyScheduler = section("scheduler");
22385
+ const bodyWatchdog = section("watchdog");
21093
22386
  const merged = {
21094
22387
  ...current,
21095
22388
  ...body,
@@ -21098,8 +22391,13 @@ var init_web = __esm({
21098
22391
  scheduler: { ...currentScheduler, ...bodyScheduler },
21099
22392
  watchdog: { ...currentWatchdog, ...bodyWatchdog }
21100
22393
  };
21101
- writeConfig(validateConfig(merged));
21102
- return c.json({ success: true });
22394
+ const savedConfig = validateConfig(merged);
22395
+ writeConfig(savedConfig);
22396
+ return c.json({
22397
+ success: true,
22398
+ restartRequired: runtimeConfig === null || !configsEqual(savedConfig, runtimeConfig),
22399
+ managed: canRestartCurrentGateway()
22400
+ });
21103
22401
  } catch (error) {
21104
22402
  return c.json({
21105
22403
  success: false,
@@ -21107,9 +22405,27 @@ var init_web = __esm({
21107
22405
  }, 400);
21108
22406
  }
21109
22407
  });
22408
+ app.post("/api/gateway/restart", async (c) => {
22409
+ const rawBody = await c.req.json().catch(() => null);
22410
+ const confirmation = rawBody && typeof rawBody === "object" && !Array.isArray(rawBody) ? rawBody.confirmation : void 0;
22411
+ if (confirmation !== "RESTART") {
22412
+ return c.json({ error: "confirmation must be RESTART" }, 400);
22413
+ }
22414
+ if (restartScheduled) return c.json({ error: "restart already scheduled" }, 409);
22415
+ if (!canRestartCurrentGateway()) {
22416
+ 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);
22417
+ }
22418
+ restartScheduled = true;
22419
+ const previousPid = process.pid;
22420
+ setTimeout(() => {
22421
+ process.kill(previousPid, "SIGTERM");
22422
+ }, 500);
22423
+ return c.json({ success: true, previousPid }, 202);
22424
+ });
21110
22425
  app.post("/api/database/clear", async (c) => {
21111
- const body = await c.req.json().catch(() => ({}));
21112
- if (body.confirmation !== "CLEAR") {
22426
+ const rawBody = await c.req.json().catch(() => null);
22427
+ const confirmation = rawBody && typeof rawBody === "object" && !Array.isArray(rawBody) ? rawBody.confirmation : void 0;
22428
+ if (confirmation !== "CLEAR") {
21113
22429
  return c.json({ success: false, error: "confirmation must be CLEAR" }, 400);
21114
22430
  }
21115
22431
  try {
@@ -21142,6 +22458,8 @@ init_task_run_service();
21142
22458
  init_health();
21143
22459
  init_process_control();
21144
22460
  init_launch_protocol();
22461
+ init_task_working_directory();
22462
+ init_task_batch();
21145
22463
  import { spawn } from "child_process";
21146
22464
  import { fileURLToPath as fileURLToPath3 } from "url";
21147
22465
  import { existsSync as existsSync3 } from "fs";
@@ -21259,7 +22577,8 @@ var WorkerEngine = class {
21259
22577
  if (!task) break;
21260
22578
  if (this.stopped) break;
21261
22579
  if (!await TaskService.start(task.id)) continue;
21262
- if (task.batchId) this.activeBatchIds.add(task.batchId);
22580
+ const batchId = normalizeTaskBatchId(task.batchId);
22581
+ if (batchId) this.activeBatchIds.add(batchId);
21263
22582
  if (this.stopped) {
21264
22583
  await TaskService.resetRunningToPending([task.id]);
21265
22584
  this.releaseBatch(task);
@@ -21290,6 +22609,14 @@ var WorkerEngine = class {
21290
22609
  this.releaseBatch(task);
21291
22610
  continue;
21292
22611
  }
22612
+ try {
22613
+ validateTaskWorkingDirectory(task.cwd);
22614
+ } catch (error) {
22615
+ const message = error instanceof Error ? error.message : String(error);
22616
+ await TaskService.failRun(task.id, run.id, message, { setDeadLetter: true });
22617
+ this.releaseBatch(task);
22618
+ continue;
22619
+ }
21293
22620
  await this.spawnTask(task, run.id, launchIdentity);
21294
22621
  } catch (err) {
21295
22622
  this.releaseBatch(task);
@@ -21375,11 +22702,16 @@ var WorkerEngine = class {
21375
22702
  child.on("message", (message) => {
21376
22703
  if (isMatchingDrainProof(message, entry.launchIdentity)) {
21377
22704
  entry.guardianDrained = true;
22705
+ try {
22706
+ child.send(drainProofAckForIdentity(entry.launchIdentity));
22707
+ } catch (error) {
22708
+ this.logError("drain proof acknowledgment failed", error, task.id);
22709
+ }
21378
22710
  }
21379
22711
  });
21380
22712
  let spawnError = null;
21381
- const spawned = new Promise((resolve3, reject) => {
21382
- child.once("spawn", resolve3);
22713
+ const spawned = new Promise((resolve4, reject) => {
22714
+ child.once("spawn", resolve4);
21383
22715
  child.once("error", (error) => {
21384
22716
  spawnError = error;
21385
22717
  reject(error);
@@ -21427,10 +22759,10 @@ var WorkerEngine = class {
21427
22759
  return;
21428
22760
  }
21429
22761
  try {
21430
- await new Promise((resolve3, reject) => {
22762
+ await new Promise((resolve4, reject) => {
21431
22763
  child.stdin.end("START\n", (error) => {
21432
22764
  if (error) reject(error);
21433
- else resolve3();
22765
+ else resolve4();
21434
22766
  });
21435
22767
  });
21436
22768
  } catch (error) {
@@ -21654,7 +22986,8 @@ ${output}` : ""}` : output;
21654
22986
  );
21655
22987
  }
21656
22988
  releaseBatch(task) {
21657
- if (task.batchId) this.activeBatchIds.delete(task.batchId);
22989
+ const batchId = normalizeTaskBatchId(task.batchId);
22990
+ if (batchId) this.activeBatchIds.delete(batchId);
21658
22991
  }
21659
22992
  resolveModel(taskModel) {
21660
22993
  if (!taskModel || taskModel === "default") return null;
@@ -21994,7 +23327,7 @@ var Scheduler = class {
21994
23327
  const lastTemplate = dueTemplates.at(-1);
21995
23328
  for (const tmpl of dueTemplates) {
21996
23329
  try {
21997
- const task = await cloneTaskFromTemplate(tmpl.id);
23330
+ const task = await cloneTaskFromTemplate(tmpl.id, tmpl.nextRunAt);
21998
23331
  if (task) {
21999
23332
  console.log(JSON.stringify({
22000
23333
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -22060,29 +23393,7 @@ init_db2();
22060
23393
  init_task_service();
22061
23394
  init_health();
22062
23395
  init_process_control();
22063
-
22064
- // src/core/package-version.ts
22065
- import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
22066
- import { dirname as dirname4, join as join4 } from "path";
22067
- import { fileURLToPath as fileURLToPath4 } from "url";
22068
- function getPackageVersion() {
22069
- let directory = dirname4(fileURLToPath4(import.meta.url));
22070
- for (let depth = 0; depth < 5; depth += 1) {
22071
- const packagePath = join4(directory, "package.json");
22072
- if (existsSync4(packagePath)) {
22073
- try {
22074
- const pkg = JSON.parse(readFileSync2(packagePath, "utf8"));
22075
- if (typeof pkg.version === "string") return pkg.version;
22076
- } catch {
22077
- return "0.0.0";
22078
- }
22079
- }
22080
- directory = dirname4(directory);
22081
- }
22082
- return "0.0.0";
22083
- }
22084
-
22085
- // src/gateway/index.ts
23396
+ init_package_version();
22086
23397
  var STALE_THRESHOLD_MS = 3e4;
22087
23398
  async function runGatewayShutdownStep(failures, step, operation) {
22088
23399
  try {
@@ -22301,8 +23612,9 @@ async function main() {
22301
23612
  await scheduler.start();
22302
23613
  if (shuttingDown) return;
22303
23614
  if (cfg.dashboard.enabled) {
22304
- const { dashboardApp: dashboardApp2 } = await Promise.resolve().then(() => (init_web(), web_exports));
23615
+ const { dashboardApp: dashboardApp2, setDashboardRuntimeConfig: setDashboardRuntimeConfig2 } = await Promise.resolve().then(() => (init_web(), web_exports));
22305
23616
  if (shuttingDown) return;
23617
+ setDashboardRuntimeConfig2(cfg);
22306
23618
  dashboardServer = Bun.serve({
22307
23619
  hostname: "127.0.0.1",
22308
23620
  port: cfg.dashboard.port,