opencode-supertask 0.1.39 → 0.1.41

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.
@@ -6005,6 +6005,7 @@ var init_schema = __esm({
6005
6005
  name: text("name").notNull(),
6006
6006
  agent: text("agent").notNull(),
6007
6007
  model: text("model").default("default"),
6008
+ variant: text("variant"),
6008
6009
  prompt: text("prompt").notNull(),
6009
6010
  cwd: text("cwd"),
6010
6011
  // 分类与优先级
@@ -6049,6 +6050,7 @@ var init_schema = __esm({
6049
6050
  taskId: integer("task_id").notNull().references(() => tasks.id, { onDelete: "cascade" }),
6050
6051
  sessionId: text("session_id"),
6051
6052
  model: text("model"),
6053
+ variant: text("variant"),
6052
6054
  status: text("status").default("running"),
6053
6055
  startedAt: integer("started_at", { mode: "timestamp" }).$defaultFn(() => /* @__PURE__ */ new Date()),
6054
6056
  finishedAt: integer("finished_at", { mode: "timestamp" }),
@@ -6069,6 +6071,7 @@ var init_schema = __esm({
6069
6071
  name: text("name").notNull(),
6070
6072
  agent: text("agent").notNull(),
6071
6073
  model: text("model").default("default"),
6074
+ variant: text("variant"),
6072
6075
  prompt: text("prompt").notNull(),
6073
6076
  cwd: text("cwd"),
6074
6077
  category: text("category").default("general"),
@@ -6107,8 +6110,8 @@ import { homedir } from "os";
6107
6110
  import { join, dirname } from "path";
6108
6111
  import { fileURLToPath } from "url";
6109
6112
  function getMigrationsFolder() {
6110
- const __dirname2 = dirname(fileURLToPath(import.meta.url));
6111
- let dir = __dirname2;
6113
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6114
+ let dir = __dirname;
6112
6115
  for (let i = 0; i < 5; i++) {
6113
6116
  const candidate = join(dir, "drizzle", "meta", "_journal.json");
6114
6117
  if (existsSync(candidate)) {
@@ -6116,7 +6119,7 @@ function getMigrationsFolder() {
6116
6119
  }
6117
6120
  dir = dirname(dir);
6118
6121
  }
6119
- return join(__dirname2, "../../drizzle");
6122
+ return join(__dirname, "../../drizzle");
6120
6123
  }
6121
6124
  function ensureGatewayLock(sqliteDb) {
6122
6125
  sqliteDb.exec(`
@@ -6439,6 +6442,28 @@ var init_task_batch = __esm({
6439
6442
  }
6440
6443
  });
6441
6444
 
6445
+ // src/core/model-variant.ts
6446
+ function normalizeModelVariant(value) {
6447
+ if (value === void 0 || value === null) return value;
6448
+ const normalized = value.trim();
6449
+ if (!normalized) return null;
6450
+ if (normalized.length > MAX_MODEL_VARIANT_LENGTH) {
6451
+ throw new Error(`variant \u957F\u5EA6\u4E0D\u80FD\u8D85\u8FC7 ${MAX_MODEL_VARIANT_LENGTH} \u4E2A\u5B57\u7B26`);
6452
+ }
6453
+ if (CONTROL_CHARACTER_PATTERN.test(normalized)) {
6454
+ throw new Error("variant \u4E0D\u80FD\u5305\u542B\u63A7\u5236\u5B57\u7B26");
6455
+ }
6456
+ return normalized;
6457
+ }
6458
+ var MAX_MODEL_VARIANT_LENGTH, CONTROL_CHARACTER_PATTERN;
6459
+ var init_model_variant = __esm({
6460
+ "src/core/model-variant.ts"() {
6461
+ "use strict";
6462
+ MAX_MODEL_VARIANT_LENGTH = 128;
6463
+ CONTROL_CHARACTER_PATTERN = /[\u0000-\u001F\u007F]/;
6464
+ }
6465
+ });
6466
+
6442
6467
  // src/core/services/task.service.ts
6443
6468
  function hasNoExecutableDependents() {
6444
6469
  return sql`NOT EXISTS (
@@ -6494,6 +6519,7 @@ var init_task_service = __esm({
6494
6519
  init_backoff();
6495
6520
  init_task_working_directory();
6496
6521
  init_task_batch();
6522
+ init_model_variant();
6497
6523
  ({ tasks: tasks2, taskRuns: taskRuns2 } = schema_exports);
6498
6524
  cleanupInvocation = 0;
6499
6525
  TaskDeletionConflictError = class extends Error {
@@ -6513,7 +6539,8 @@ var init_task_service = __esm({
6513
6539
  static async add(data) {
6514
6540
  const normalizedData = {
6515
6541
  ...data,
6516
- batchId: normalizeTaskBatchId(data.batchId)
6542
+ batchId: normalizeTaskBatchId(data.batchId),
6543
+ variant: normalizeModelVariant(data.variant)
6517
6544
  };
6518
6545
  this.validateNewTask(normalizedData);
6519
6546
  return db.transaction((tx) => {
@@ -6541,7 +6568,11 @@ var init_task_service = __esm({
6541
6568
  }
6542
6569
  static async update(id, data, scope = {}) {
6543
6570
  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 };
6571
+ const normalizedData = {
6572
+ ...data,
6573
+ ...data.batchId === void 0 ? {} : { batchId: normalizeTaskBatchId(data.batchId) ?? null },
6574
+ ...data.variant === void 0 ? {} : { variant: normalizeModelVariant(data.variant) ?? null }
6575
+ };
6545
6576
  return db.transaction((tx) => {
6546
6577
  const task = tx.select().from(tasks2).where(and(
6547
6578
  eq(tasks2.id, id),
@@ -6553,6 +6584,7 @@ var init_task_service = __esm({
6553
6584
  name: normalizedData.name ?? task.name,
6554
6585
  agent: normalizedData.agent ?? task.agent,
6555
6586
  model: normalizedData.model ?? task.model,
6587
+ variant: normalizedData.variant === void 0 ? task.variant : normalizedData.variant,
6556
6588
  prompt: normalizedData.prompt ?? task.prompt,
6557
6589
  cwd: task.cwd,
6558
6590
  category: normalizedData.category ?? task.category,
@@ -6566,13 +6598,23 @@ var init_task_service = __esm({
6566
6598
  });
6567
6599
  const maxRetries = normalizedData.maxRetries ?? task.maxRetries ?? 3;
6568
6600
  const exhausted = task.status === "failed" && (task.retryCount ?? 0) > maxRetries;
6569
- return tx.update(tasks2).set({
6601
+ const updated = tx.update(tasks2).set({
6570
6602
  ...normalizedData,
6571
6603
  ...exhausted ? {
6572
6604
  status: "dead_letter",
6573
6605
  retryAfter: null
6574
6606
  } : {}
6575
6607
  }).where(eq(tasks2.id, id)).returning().get() ?? null;
6608
+ if (exhausted && updated) {
6609
+ const finishedAt = /* @__PURE__ */ new Date();
6610
+ tx.update(tasks2).set({
6611
+ status: "dead_letter",
6612
+ finishedAt,
6613
+ retryAfter: null,
6614
+ resultLog: `\u4F9D\u8D56\u4EFB\u52A1 #${id} \u5DF2\u8FDB\u5165\u4E0D\u53EF\u6062\u590D\u7EC8\u6001`
6615
+ }).where(blockedDependentsOf(id)).run();
6616
+ }
6617
+ return updated;
6576
6618
  }, { behavior: "immediate" });
6577
6619
  }
6578
6620
  static validateNewTask(data) {
@@ -6593,7 +6635,7 @@ var init_task_service = __esm({
6593
6635
  throw new Error(`${name} \u5FC5\u987B\u662F ${min} \u5230 ${max} \u4E4B\u95F4\u7684\u6574\u6570`);
6594
6636
  }
6595
6637
  }
6596
- static async next(scope = {}) {
6638
+ static buildRunnableTaskWhere(scope) {
6597
6639
  const baseConditions = [...this.buildScopeWhere(scope)];
6598
6640
  const nowMs = Date.now();
6599
6641
  const retryAfterFilter = or(
@@ -6628,40 +6670,43 @@ var init_task_service = __esm({
6628
6670
  if (batchFilter) {
6629
6671
  conditions.push(batchFilter);
6630
6672
  }
6631
- const result = await db.select().from(tasks2).where(and(
6673
+ return and(
6632
6674
  ...conditions,
6633
6675
  or(
6634
6676
  isNull(tasks2.dependsOn),
6635
6677
  sql`EXISTS (
6636
- SELECT 1 FROM tasks AS dependency_task
6637
- WHERE dependency_task.id = ${tasks2.dependsOn}
6638
- AND dependency_task.status = 'done'
6639
- AND dependency_task.cwd IS ${tasks2.cwd}
6640
- )`
6678
+ SELECT 1 FROM tasks AS dependency_task
6679
+ WHERE dependency_task.id = ${tasks2.dependsOn}
6680
+ AND dependency_task.status = 'done'
6681
+ AND dependency_task.cwd IS ${tasks2.cwd}
6682
+ )`
6641
6683
  ),
6642
6684
  or(
6643
6685
  isNull(tasks2.batchId),
6644
6686
  sql`trim(${tasks2.batchId}, ${TASK_BATCH_TRIM_CHARACTERS}) = ''`,
6645
6687
  sql`NOT EXISTS (
6646
- SELECT 1 FROM tasks AS running_batch_task
6647
- WHERE trim(running_batch_task.batch_id, ${TASK_BATCH_TRIM_CHARACTERS})
6648
- = trim(${tasks2.batchId}, ${TASK_BATCH_TRIM_CHARACTERS})
6649
- AND (
6650
- running_batch_task.status = 'running'
6651
- OR EXISTS (
6652
- SELECT 1 FROM task_runs AS running_batch_run
6653
- WHERE running_batch_run.task_id = running_batch_task.id
6654
- AND running_batch_run.status = 'running'
6655
- )
6688
+ SELECT 1 FROM tasks AS running_batch_task
6689
+ WHERE trim(running_batch_task.batch_id, ${TASK_BATCH_TRIM_CHARACTERS})
6690
+ = trim(${tasks2.batchId}, ${TASK_BATCH_TRIM_CHARACTERS})
6691
+ AND (
6692
+ running_batch_task.status = 'running'
6693
+ OR EXISTS (
6694
+ SELECT 1 FROM task_runs AS running_batch_run
6695
+ WHERE running_batch_run.task_id = running_batch_task.id
6696
+ AND running_batch_run.status = 'running'
6656
6697
  )
6657
- )`
6698
+ )
6699
+ )`
6658
6700
  ),
6659
6701
  sql`NOT EXISTS (
6660
- SELECT 1 FROM task_runs AS candidate_active_run
6661
- WHERE candidate_active_run.task_id = ${tasks2.id}
6662
- AND candidate_active_run.status = 'running'
6663
- )`
6664
- )).orderBy(
6702
+ SELECT 1 FROM task_runs AS candidate_active_run
6703
+ WHERE candidate_active_run.task_id = ${tasks2.id}
6704
+ AND candidate_active_run.status = 'running'
6705
+ )`
6706
+ );
6707
+ }
6708
+ static async next(scope = {}) {
6709
+ const result = await db.select().from(tasks2).where(this.buildRunnableTaskWhere(scope)).orderBy(
6665
6710
  desc(tasks2.urgency),
6666
6711
  desc(tasks2.importance),
6667
6712
  asc(tasks2.createdAt),
@@ -6669,6 +6714,22 @@ var init_task_service = __esm({
6669
6714
  ).limit(1);
6670
6715
  return result[0] ?? null;
6671
6716
  }
6717
+ static async claimNext(scope = {}) {
6718
+ return db.transaction((tx) => {
6719
+ const candidate = tx.select().from(tasks2).where(this.buildRunnableTaskWhere(scope)).orderBy(
6720
+ desc(tasks2.urgency),
6721
+ desc(tasks2.importance),
6722
+ asc(tasks2.createdAt),
6723
+ asc(tasks2.id)
6724
+ ).limit(1).get();
6725
+ if (!candidate) return null;
6726
+ return tx.update(tasks2).set({
6727
+ status: "running",
6728
+ startedAt: /* @__PURE__ */ new Date(),
6729
+ finishedAt: null
6730
+ }).where(eq(tasks2.id, candidate.id)).returning().get() ?? null;
6731
+ }, { behavior: "immediate" });
6732
+ }
6672
6733
  static async countRunning(scope = {}) {
6673
6734
  const scopeConditions = scope.legacyCwd ? [sql`${tasks2.cwd} IS NULL OR trim(${tasks2.cwd}) = ''`] : this.buildScopeWhere(scope);
6674
6735
  if (scope.batchId !== void 0) {
@@ -7329,9 +7390,34 @@ var init_launch_protocol = __esm({
7329
7390
  });
7330
7391
 
7331
7392
  // src/core/process-control.ts
7332
- import { spawnSync } from "child_process";
7333
7393
  import { fileURLToPath as fileURLToPath2 } from "url";
7334
7394
  import { basename, dirname as dirname2, resolve } from "path";
7395
+ function runBoundedOsCommand(command, args, captureOutput = true) {
7396
+ const result = Bun.spawnSync({
7397
+ cmd: [
7398
+ process.execPath,
7399
+ "-e",
7400
+ BOUNDED_OS_COMMAND_RUNNER,
7401
+ "supertask-os-command",
7402
+ command,
7403
+ ...args
7404
+ ],
7405
+ detached: process.platform !== "win32",
7406
+ maxBuffer: OS_COMMAND_MAX_OUTPUT_BYTES + 1024,
7407
+ stdin: "ignore",
7408
+ stdout: "pipe",
7409
+ stderr: "ignore"
7410
+ });
7411
+ const output = new TextDecoder().decode(result.stdout);
7412
+ const markerIndex = output.lastIndexOf(OS_COMMAND_RESULT_MARKER);
7413
+ if (markerIndex < 0) return { status: null, stdout: "" };
7414
+ const statusText2 = output.slice(markerIndex + OS_COMMAND_RESULT_MARKER.length);
7415
+ if (!/^\d+$/.test(statusText2)) return { status: null, stdout: "" };
7416
+ return {
7417
+ status: Number(statusText2),
7418
+ stdout: captureOutput ? output.slice(0, markerIndex) : ""
7419
+ };
7420
+ }
7335
7421
  function isSafePid(pid) {
7336
7422
  return Number.isInteger(pid) && pid > 1 && pid !== process.pid;
7337
7423
  }
@@ -7368,12 +7454,10 @@ function inspectSpawnedProcessTreePresence(pid) {
7368
7454
  }
7369
7455
  }
7370
7456
  function inspectUnixProcess(pid) {
7371
- const result = spawnSync("ps", ["-o", "pgid=", "-o", "command=", "-p", String(pid)], {
7372
- encoding: "utf8",
7373
- stdio: ["ignore", "pipe", "ignore"],
7374
- timeout: OS_COMMAND_TIMEOUT_MS,
7375
- killSignal: "SIGKILL"
7376
- });
7457
+ const result = runBoundedOsCommand(
7458
+ "ps",
7459
+ ["-o", "pgid=", "-o", "command=", "-p", String(pid)]
7460
+ );
7377
7461
  if (result.status !== 0) return null;
7378
7462
  const match2 = result.stdout.trim().match(/^(\d+)\s+(.+)$/s);
7379
7463
  if (!match2) return null;
@@ -7381,30 +7465,18 @@ function inspectUnixProcess(pid) {
7381
7465
  }
7382
7466
  function inspectWindowsCommand(pid) {
7383
7467
  const script = `(Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}").CommandLine`;
7384
- const result = spawnSync(
7468
+ const result = runBoundedOsCommand(
7385
7469
  "powershell.exe",
7386
- ["-NoProfile", "-NonInteractive", "-Command", script],
7387
- {
7388
- encoding: "utf8",
7389
- stdio: ["ignore", "pipe", "ignore"],
7390
- timeout: OS_COMMAND_TIMEOUT_MS,
7391
- killSignal: "SIGKILL"
7392
- }
7470
+ ["-NoProfile", "-NonInteractive", "-Command", script]
7393
7471
  );
7394
7472
  if (result.status !== 0) return null;
7395
7473
  return result.stdout.trim() || null;
7396
7474
  }
7397
7475
  function inspectWindowsProcessTree(rootPid) {
7398
7476
  const script = `$root=${rootPid}; $all=@(Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId); $ids=New-Object 'System.Collections.Generic.HashSet[int]'; [void]$ids.Add($root); do { $added=$false; foreach($p in $all) { if($ids.Contains([int]$p.ParentProcessId) -and $ids.Add([int]$p.ProcessId)) { $added=$true } } } while($added); $all | Where-Object { $ids.Contains([int]$_.ProcessId) } | Select-Object -ExpandProperty ProcessId`;
7399
- const result = spawnSync(
7477
+ const result = runBoundedOsCommand(
7400
7478
  "powershell.exe",
7401
- ["-NoProfile", "-NonInteractive", "-Command", script],
7402
- {
7403
- encoding: "utf8",
7404
- stdio: ["ignore", "pipe", "ignore"],
7405
- timeout: OS_COMMAND_TIMEOUT_MS,
7406
- killSignal: "SIGKILL"
7407
- }
7479
+ ["-NoProfile", "-NonInteractive", "-Command", script]
7408
7480
  );
7409
7481
  if (result.status !== 0) return null;
7410
7482
  return result.stdout.split(/\s+/).filter(Boolean).map(Number).filter((pid) => Number.isInteger(pid) && pid > 0);
@@ -7475,11 +7547,7 @@ function signalRecordedProcessTreeWithResult(pid, signal, expectedExecutable, la
7475
7547
  }
7476
7548
  const args = ["/PID", String(pid), "/T"];
7477
7549
  if (signal === "SIGKILL") args.push("/F");
7478
- const status = spawnSync("taskkill", args, {
7479
- stdio: "ignore",
7480
- timeout: OS_COMMAND_TIMEOUT_MS,
7481
- killSignal: "SIGKILL"
7482
- }).status;
7550
+ const status = runBoundedOsCommand("taskkill", args, false).status;
7483
7551
  if (status === 0) return "signalled";
7484
7552
  return isProcessAlive(pid) ? "signal-failed" : absentLeaderResult(pid);
7485
7553
  }
@@ -7508,12 +7576,64 @@ async function waitForSpawnedProcessTreeExit(pid, timeoutMs = 5e3) {
7508
7576
  }
7509
7577
  return inspectSpawnedProcessTreePresence(pid) === "not-running";
7510
7578
  }
7511
- var OS_COMMAND_TIMEOUT_MS;
7579
+ var OS_COMMAND_TIMEOUT_MS, OS_COMMAND_MAX_OUTPUT_BYTES, OS_COMMAND_RESULT_MARKER, BOUNDED_OS_COMMAND_RUNNER;
7512
7580
  var init_process_control = __esm({
7513
7581
  "src/core/process-control.ts"() {
7514
7582
  "use strict";
7515
7583
  init_launch_protocol();
7516
7584
  OS_COMMAND_TIMEOUT_MS = 2e3;
7585
+ OS_COMMAND_MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
7586
+ OS_COMMAND_RESULT_MARKER = "\n\0supertask-os-command-result:";
7587
+ BOUNDED_OS_COMMAND_RUNNER = `
7588
+ const { writeSync } = await import('fs');
7589
+ let child;
7590
+ try {
7591
+ child = Bun.spawn(process.argv.slice(2), {
7592
+ stdin: 'ignore',
7593
+ stdout: 'pipe',
7594
+ stderr: 'ignore',
7595
+ });
7596
+ } catch {
7597
+ process.exit(127);
7598
+ }
7599
+ const terminateGroup = () => {
7600
+ if (process.platform !== 'win32') {
7601
+ try {
7602
+ process.kill(-process.pid, 'SIGKILL');
7603
+ } catch {
7604
+ }
7605
+ }
7606
+ try {
7607
+ child.kill('SIGKILL');
7608
+ } catch {
7609
+ }
7610
+ process.exit(1);
7611
+ };
7612
+ const timer = setTimeout(terminateGroup, ${OS_COMMAND_TIMEOUT_MS});
7613
+ try {
7614
+ const chunks = [];
7615
+ let outputBytes = 0;
7616
+ const readOutput = async () => {
7617
+ const reader = child.stdout.getReader();
7618
+ while (true) {
7619
+ const { done, value } = await reader.read();
7620
+ if (done) return;
7621
+ outputBytes += value.byteLength;
7622
+ if (outputBytes > ${OS_COMMAND_MAX_OUTPUT_BYTES}) terminateGroup();
7623
+ chunks.push(value);
7624
+ }
7625
+ };
7626
+ const [exitCode] = await Promise.all([child.exited, readOutput()]);
7627
+ clearTimeout(timer);
7628
+ for (const chunk of chunks) writeSync(1, chunk);
7629
+ writeSync(1, ${JSON.stringify(OS_COMMAND_RESULT_MARKER)} + String(exitCode));
7630
+ if (process.platform !== 'win32') terminateGroup();
7631
+ process.exit(0);
7632
+ } catch {
7633
+ clearTimeout(timer);
7634
+ terminateGroup();
7635
+ }
7636
+ `;
7517
7637
  }
7518
7638
  });
7519
7639
 
@@ -7665,7 +7785,7 @@ var init_task_run_service = __esm({
7665
7785
  }
7666
7786
  if (current.childPid != null) {
7667
7787
  throw new LegacyRunAbandonConflictError(
7668
- `run #${runId} \u5DF2\u8BB0\u5F55 child PID ${current.childPid}\uFF0C\u5FC5\u987B\u7531 Worker/Watchdog \u786E\u8BA4\u8FDB\u7A0B\u6811\u9000\u51FA`
7788
+ `run #${runId} \u5DF2\u8BB0\u5F55 child PID ${current.childPid}\uFF0C\u5FC5\u987B\u7531 Worker/Watchdog \u786E\u8BA4\u53D7\u7BA1\u8FDB\u7A0B\u7EC4\u6392\u7A7A`
7669
7789
  );
7670
7790
  }
7671
7791
  if (current.workerPid != null && isProcessAlive(current.workerPid)) {
@@ -16909,8 +17029,8 @@ var require_CronFileParser = __commonJS({
16909
17029
  * @throws If file cannot be read
16910
17030
  */
16911
17031
  static parseFileSync(filePath) {
16912
- const { readFileSync: readFileSync5 } = __require("fs");
16913
- const data = readFileSync5(filePath, "utf8");
17032
+ const { readFileSync: readFileSync4 } = __require("fs");
17033
+ const data = readFileSync4(filePath, "utf8");
16914
17034
  return _CronFileParser.#parseContent(data);
16915
17035
  }
16916
17036
  /**
@@ -17045,12 +17165,14 @@ var init_task_template_service = __esm({
17045
17165
  init_cron_parser();
17046
17166
  init_task_working_directory();
17047
17167
  init_task_batch();
17168
+ init_model_variant();
17048
17169
  ({ taskTemplates: taskTemplates2 } = schema_exports);
17049
17170
  TaskTemplateService = class {
17050
17171
  static async create(data) {
17051
17172
  const normalizedData = {
17052
17173
  ...data,
17053
- batchId: normalizeTaskBatchId(data.batchId)
17174
+ batchId: normalizeTaskBatchId(data.batchId),
17175
+ variant: normalizeModelVariant(data.variant)
17054
17176
  };
17055
17177
  this.validate(normalizedData);
17056
17178
  const now = Date.now();
@@ -17116,7 +17238,8 @@ var init_task_template_service = __esm({
17116
17238
  static async update(id, data) {
17117
17239
  const normalizedData = {
17118
17240
  ...data,
17119
- batchId: normalizeTaskBatchId(data.batchId) ?? null
17241
+ batchId: normalizeTaskBatchId(data.batchId) ?? null,
17242
+ ...data.variant === void 0 ? {} : { variant: normalizeModelVariant(data.variant) ?? null }
17120
17243
  };
17121
17244
  this.validate(normalizedData);
17122
17245
  const now = Date.now();
@@ -17279,6 +17402,7 @@ function createTaskFromTemplate(templateId, options) {
17279
17402
  name: `${options.namePrefix ?? ""}${tmpl.name}`,
17280
17403
  agent: tmpl.agent,
17281
17404
  model: tmpl.model ?? "default",
17405
+ variant: tmpl.variant,
17282
17406
  prompt: tmpl.prompt,
17283
17407
  cwd: tmpl.cwd ?? null,
17284
17408
  category: tmpl.category ?? "general",
@@ -17351,32 +17475,6 @@ var init_job_templates = __esm({
17351
17475
  }
17352
17476
  });
17353
17477
 
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
-
17380
17478
  // node_modules/hono/dist/compose.js
17381
17479
  var compose;
17382
17480
  var init_compose = __esm({
@@ -19634,21 +19732,53 @@ function cleanOutput(value) {
19634
19732
  return value.replace(ANSI_PATTERN, "").replace(/\r/g, "");
19635
19733
  }
19636
19734
  function runOpenCode(executable, args, cwd, timeoutMs) {
19637
- return new Promise((resolve4, reject) => {
19735
+ return new Promise((resolve3, reject) => {
19638
19736
  const child = spawn2(executable, args, {
19639
19737
  cwd,
19640
19738
  env: process.env,
19641
- stdio: ["ignore", "pipe", "pipe"]
19739
+ stdio: ["ignore", "pipe", "pipe"],
19740
+ detached: process.platform !== "win32"
19642
19741
  });
19643
19742
  let stdout = "";
19644
19743
  let stderr = "";
19645
19744
  let failure = null;
19646
- let finished = false;
19745
+ let settled = false;
19746
+ let forceKillTimer = null;
19747
+ let finalRejectTimer = null;
19748
+ let timer;
19749
+ const signalProcessTree = (signal) => {
19750
+ if (process.platform !== "win32" && child.pid) {
19751
+ try {
19752
+ process.kill(-child.pid, signal);
19753
+ return;
19754
+ } catch {
19755
+ }
19756
+ }
19757
+ child.kill(signal);
19758
+ };
19759
+ const rejectOnce = (error) => {
19760
+ failure ??= error;
19761
+ if (settled) return;
19762
+ settled = true;
19763
+ clearTimeout(timer);
19764
+ if (forceKillTimer) clearTimeout(forceKillTimer);
19765
+ if (finalRejectTimer) clearTimeout(finalRejectTimer);
19766
+ reject(failure);
19767
+ };
19768
+ const terminate = (error) => {
19769
+ if (failure) return;
19770
+ failure = error;
19771
+ signalProcessTree("SIGTERM");
19772
+ forceKillTimer = setTimeout(() => {
19773
+ signalProcessTree("SIGKILL");
19774
+ rejectOnce(error);
19775
+ }, 1e3);
19776
+ finalRejectTimer = setTimeout(() => rejectOnce(error), 2e3);
19777
+ };
19647
19778
  const append = (current, chunk) => {
19648
19779
  const next = current + chunk.toString();
19649
19780
  if (Buffer.byteLength(next) > MAX_OUTPUT_BYTES && failure === null) {
19650
- failure = new Error(`OpenCode \u8F93\u51FA\u8D85\u8FC7 ${MAX_OUTPUT_BYTES} bytes`);
19651
- child.kill("SIGTERM");
19781
+ terminate(new Error(`OpenCode \u8F93\u51FA\u8D85\u8FC7 ${MAX_OUTPUT_BYTES} bytes`));
19652
19782
  }
19653
19783
  return next.slice(-MAX_OUTPUT_BYTES);
19654
19784
  };
@@ -19659,32 +19789,80 @@ function runOpenCode(executable, args, cwd, timeoutMs) {
19659
19789
  stderr = append(stderr, chunk);
19660
19790
  });
19661
19791
  child.once("error", (error) => {
19662
- failure ??= error;
19792
+ if (forceKillTimer) return;
19793
+ rejectOnce(error);
19663
19794
  });
19664
- const timer = setTimeout(() => {
19665
- failure ??= new Error(`OpenCode \u547D\u4EE4\u8D85\u8FC7 ${timeoutMs}ms \u672A\u5B8C\u6210`);
19666
- child.kill("SIGTERM");
19795
+ timer = setTimeout(() => {
19796
+ terminate(new Error(`OpenCode \u547D\u4EE4\u8D85\u8FC7 ${timeoutMs}ms \u672A\u5B8C\u6210`));
19667
19797
  }, timeoutMs);
19668
19798
  child.once("close", (code) => {
19669
- if (finished) return;
19670
- finished = true;
19671
19799
  clearTimeout(timer);
19800
+ if (forceKillTimer) return;
19801
+ if (finalRejectTimer) clearTimeout(finalRejectTimer);
19802
+ if (settled) return;
19803
+ settled = true;
19672
19804
  if (failure) {
19673
19805
  reject(failure);
19674
19806
  return;
19675
19807
  }
19676
19808
  if (code !== 0) {
19677
19809
  const detail = cleanOutput(stderr).trim() || `\u9000\u51FA\u7801 ${code ?? "null"}`;
19678
- reject(new Error(`OpenCode ${args.join(" ")} \u5931\u8D25\uFF1A${detail}`));
19810
+ reject(new OpenCodeCommandExitError(`OpenCode ${args.join(" ")} \u5931\u8D25\uFF1A${detail}`));
19679
19811
  return;
19680
19812
  }
19681
- resolve4(cleanOutput(stdout));
19813
+ resolve3(cleanOutput(stdout));
19682
19814
  });
19683
19815
  });
19684
19816
  }
19685
19817
  function parseOpenCodeModels(output) {
19686
19818
  return [...new Set(cleanOutput(output).split("\n").map((line) => line.trim()).filter((line) => /^[^\s/]+\/.+/.test(line)))].sort((left, right) => left.localeCompare(right));
19687
19819
  }
19820
+ function isRecord(value) {
19821
+ return typeof value === "object" && value !== null && !Array.isArray(value);
19822
+ }
19823
+ function findJsonObjectEnd(value, start) {
19824
+ let depth = 0;
19825
+ let inString = false;
19826
+ let escaped = false;
19827
+ for (let index2 = start; index2 < value.length; index2 += 1) {
19828
+ const character = value[index2];
19829
+ if (inString) {
19830
+ if (escaped) escaped = false;
19831
+ else if (character === "\\") escaped = true;
19832
+ else if (character === '"') inString = false;
19833
+ continue;
19834
+ }
19835
+ if (character === '"') inString = true;
19836
+ else if (character === "{") depth += 1;
19837
+ else if (character === "}" && --depth === 0) return index2 + 1;
19838
+ }
19839
+ return null;
19840
+ }
19841
+ function parseOpenCodeModelMetadata(output) {
19842
+ const cleaned = cleanOutput(output);
19843
+ const models = /* @__PURE__ */ new Set();
19844
+ const variantsByModel = {};
19845
+ const headingPattern = /^([^\s/]+\/[^\r\n]+)\r?\n\s*(?=\{)/gm;
19846
+ for (const match2 of cleaned.matchAll(headingPattern)) {
19847
+ const model = match2[1].trim();
19848
+ models.add(model);
19849
+ let objectStart = (match2.index ?? 0) + match2[0].length;
19850
+ while (/\s/.test(cleaned[objectStart] ?? "")) objectStart += 1;
19851
+ if (cleaned[objectStart] !== "{") continue;
19852
+ const objectEnd = findJsonObjectEnd(cleaned, objectStart);
19853
+ if (objectEnd === null) continue;
19854
+ try {
19855
+ const metadata = JSON.parse(cleaned.slice(objectStart, objectEnd));
19856
+ if (!isRecord(metadata) || !isRecord(metadata.variants)) continue;
19857
+ variantsByModel[model] = Object.keys(metadata.variants).filter((variant) => variant.trim().length > 0).sort((left, right) => left.localeCompare(right));
19858
+ } catch {
19859
+ }
19860
+ }
19861
+ return {
19862
+ models: [...models].sort((left, right) => left.localeCompare(right)),
19863
+ variantsByModel
19864
+ };
19865
+ }
19688
19866
  function parseOpenCodeAgents(output) {
19689
19867
  const agents = /* @__PURE__ */ new Map();
19690
19868
  for (const line of cleanOutput(output).split("\n")) {
@@ -19703,19 +19881,23 @@ async function loadOpenCodeCatalog(cwd, options = {}) {
19703
19881
  const executable = options.executable ?? process.env.SUPERTASK_OPENCODE_BIN ?? "opencode";
19704
19882
  const timeoutMs = options.timeoutMs ?? COMMAND_TIMEOUT_MS;
19705
19883
  const cacheKey = `${executable}\0${cwd}`;
19706
- const cached = catalogCache.get(cacheKey);
19707
- if (options.useCache !== false && cached && cached.expiresAt > Date.now()) {
19708
- return cached.result;
19884
+ const cached2 = catalogCache.get(cacheKey);
19885
+ if (options.useCache !== false && cached2 && cached2.expiresAt > Date.now()) {
19886
+ return cached2.result;
19709
19887
  }
19710
19888
  const result = Promise.all([
19711
- runOpenCode(executable, ["models"], cwd, timeoutMs),
19889
+ runOpenCode(executable, ["models", "--verbose"], cwd, timeoutMs).catch((error) => {
19890
+ if (!(error instanceof OpenCodeCommandExitError)) throw error;
19891
+ return runOpenCode(executable, ["models"], cwd, timeoutMs);
19892
+ }),
19712
19893
  runOpenCode(executable, ["agent", "list"], cwd, timeoutMs)
19713
19894
  ]).then(([modelsOutput, agentsOutput]) => {
19714
- const models = parseOpenCodeModels(modelsOutput);
19895
+ const metadata = parseOpenCodeModelMetadata(modelsOutput);
19896
+ const models = metadata.models.length > 0 ? metadata.models : parseOpenCodeModels(modelsOutput);
19715
19897
  const agents = parseOpenCodeAgents(agentsOutput).filter((agent) => agent.mode !== "subagent");
19716
19898
  if (models.length === 0) throw new Error("OpenCode \u6CA1\u6709\u8FD4\u56DE\u53EF\u7528\u6A21\u578B");
19717
19899
  if (agents.length === 0) throw new Error("OpenCode \u6CA1\u6709\u8FD4\u56DE\u53EF\u76F4\u63A5\u8FD0\u884C\u7684\u4E3B Agent");
19718
- return { cwd, models, agents };
19900
+ return { cwd, models, variantsByModel: metadata.variantsByModel, agents };
19719
19901
  });
19720
19902
  if (options.useCache !== false) {
19721
19903
  catalogCache.set(cacheKey, { expiresAt: Date.now() + CATALOG_CACHE_MS, result });
@@ -19725,7 +19907,7 @@ async function loadOpenCodeCatalog(cwd, options = {}) {
19725
19907
  }
19726
19908
  return result;
19727
19909
  }
19728
- var CATALOG_CACHE_MS, COMMAND_TIMEOUT_MS, MAX_OUTPUT_BYTES, ANSI_PATTERN, catalogCache;
19910
+ var CATALOG_CACHE_MS, COMMAND_TIMEOUT_MS, MAX_OUTPUT_BYTES, ANSI_PATTERN, OpenCodeCommandExitError, catalogCache;
19729
19911
  var init_opencode_catalog = __esm({
19730
19912
  "src/core/opencode-catalog.ts"() {
19731
19913
  "use strict";
@@ -19734,6 +19916,8 @@ var init_opencode_catalog = __esm({
19734
19916
  COMMAND_TIMEOUT_MS = 2e4;
19735
19917
  MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
19736
19918
  ANSI_PATTERN = /\x1B(?:[@-_][0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\))/g;
19919
+ OpenCodeCommandExitError = class extends Error {
19920
+ };
19737
19921
  catalogCache = /* @__PURE__ */ new Map();
19738
19922
  }
19739
19923
  });
@@ -20174,464 +20358,106 @@ var init_database_maintenance_service = __esm({
20174
20358
  }
20175
20359
  });
20176
20360
 
20177
- // src/daemon/management-lock.ts
20178
- import { Database as Database4 } from "bun:sqlite";
20179
- var init_management_lock = __esm({
20180
- "src/daemon/management-lock.ts"() {
20181
- "use strict";
20182
- }
20183
- });
20184
-
20185
- // src/daemon/pm2.ts
20186
- import { execSync, spawnSync as spawnSync2 } from "child_process";
20187
- import {
20188
- accessSync,
20189
- chmodSync as chmodSync2,
20190
- constants,
20191
- existsSync as existsSync6,
20192
- mkdirSync as mkdirSync3,
20193
- readFileSync as readFileSync3,
20194
- rmSync,
20195
- statSync as statSync3,
20196
- writeFileSync as writeFileSync2
20197
- } from "fs";
20198
- import { homedir as homedir3, userInfo } from "os";
20199
- import { delimiter, dirname as dirname6, isAbsolute as isAbsolute2, join as join5, resolve as resolve3 } from "path";
20361
+ // src/web/gateway-diagnostic.ts
20362
+ import { spawn as spawn3 } from "child_process";
20363
+ import { existsSync as existsSync6 } from "fs";
20364
+ import { dirname as dirname6, join as join5 } from "path";
20200
20365
  import { fileURLToPath as fileURLToPath5 } from "url";
20201
- import { Database as Database5 } from "bun:sqlite";
20202
- function runtimeHome(env) {
20203
- return resolve3(env.HOME || homedir3());
20204
- }
20205
- function runtimePath(value, cwd) {
20206
- return resolve3(cwd, value);
20207
- }
20208
- function versionFile(env = process.env, cwd = process.cwd()) {
20209
- return env.SUPERTASK_VERSION_FILE ? runtimePath(env.SUPERTASK_VERSION_FILE, cwd) : join5(runtimeHome(env), ".local/share/opencode/supertask-gateway-version");
20210
- }
20211
- function getRunningVersion(env = process.env, cwd = process.cwd()) {
20212
- try {
20213
- const path = versionFile(env, cwd);
20214
- if (!existsSync6(path)) return null;
20215
- return readFileSync3(path, "utf-8").trim() || null;
20216
- } catch {
20217
- return null;
20218
- }
20219
- }
20220
- function packageVersionFromGatewayEntry(gatewayEntry) {
20221
- if (gatewayEntry === null) return null;
20222
- let directory = dirname6(gatewayEntry);
20223
- for (let depth = 0; depth < 6; depth += 1) {
20224
- const packagePath = join5(directory, "package.json");
20225
- if (existsSync6(packagePath)) {
20226
- try {
20227
- const pkg = JSON.parse(readFileSync3(packagePath, "utf8"));
20228
- if (pkg.name === "opencode-supertask" && typeof pkg.version === "string") {
20229
- return pkg.version;
20230
- }
20231
- } catch {
20232
- }
20233
- }
20234
- const parent = dirname6(directory);
20235
- if (parent === directory) break;
20236
- directory = parent;
20237
- }
20238
- return null;
20239
- }
20240
- function resolveRuntimeExecutable(command, env, cwd) {
20241
- if (isAbsolute2(command) || command.includes("/")) return runtimePath(command, cwd);
20242
- for (const entry of (env.PATH ?? "").split(delimiter)) {
20243
- if (!entry) continue;
20244
- const candidate = resolve3(cwd, entry, command);
20366
+ function killDiagnosticGroup(pid) {
20367
+ if (process.platform !== "win32") {
20245
20368
  try {
20246
- accessSync(candidate, constants.X_OK);
20247
- return candidate;
20369
+ process.kill(-pid, "SIGKILL");
20370
+ return;
20248
20371
  } catch {
20249
20372
  }
20250
20373
  }
20251
- return command;
20252
- }
20253
- function runtimeScope(runtime) {
20254
- const { cwd, env } = runtime;
20255
- const home = runtimeHome(env);
20256
- return {
20257
- cwd: resolve3(cwd),
20258
- databasePath: env.SUPERTASK_DB_PATH ? runtimePath(env.SUPERTASK_DB_PATH, cwd) : join5(home, ".local/share/opencode/tasks.db"),
20259
- configPath: env.SUPERTASK_CONFIG_PATH ? runtimePath(env.SUPERTASK_CONFIG_PATH, cwd) : join5(home, ".config/opencode/supertask.json"),
20260
- opencodePath: resolveRuntimeExecutable(env.SUPERTASK_OPENCODE_BIN ?? "opencode", env, cwd),
20261
- home,
20262
- pm2Home: env.PM2_HOME ? runtimePath(env.PM2_HOME, cwd) : join5(home, ".pm2"),
20263
- managementLockPath: canonicalManagementLockPath(env, cwd)
20264
- };
20265
- }
20266
- function scopesMatch(left, right) {
20267
- 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;
20268
- }
20269
- function currentScope() {
20270
- return runtimeScope({ cwd: process.cwd(), env: process.env });
20271
- }
20272
- function diagnoseOpenCodeRuntime(runtime = {
20273
- cwd: process.cwd(),
20274
- env: process.env
20275
- }) {
20276
- const executable = runtimeScope(runtime).opencodePath;
20277
- const result = spawnSync2(executable, ["--version"], {
20278
- cwd: runtime.cwd,
20279
- env: runtime.env,
20280
- encoding: "utf8",
20281
- timeout: 1e4,
20282
- killSignal: "SIGKILL"
20283
- });
20284
- const ok = result.status === 0 && result.error === void 0;
20285
- return {
20286
- ok,
20287
- executable,
20288
- version: ok ? result.stdout.trim() || result.stderr.trim() || null : null,
20289
- error: ok ? null : result.error?.message || result.stderr.trim() || result.stdout.trim() || `exit code ${result.status}`
20290
- };
20291
- }
20292
- function getGatewayDiagnostic(options = {}) {
20293
- const producerScope = currentScope();
20294
- if (!isPm2Installed()) {
20295
- return {
20296
- pm2Installed: false,
20297
- processFound: false,
20298
- status: null,
20299
- pid: null,
20300
- ready: false,
20301
- runningVersion: getRunningVersion(),
20302
- gatewayEntry: null,
20303
- gatewayPackageVersion: null,
20304
- logRotationInstalled: false,
20305
- startupConfigured: process.platform === "darwin" || process.platform === "linux" ? false : null,
20306
- currentScope: producerScope,
20307
- gatewayScope: null,
20308
- scopeMatches: false,
20309
- gatewayOpenCode: null
20310
- };
20311
- }
20312
- const processes = pm2JsonList();
20313
- const gateway = processes.find((item) => item.name === PROCESS_NAME);
20314
- const runtime = gatewayRuntimeFromProcess(gateway);
20315
- const gatewayEnv = runtime?.env ?? process.env;
20316
- const managedScope = runtime ? runtimeScope(runtime) : null;
20317
- const pid = typeof gateway?.pid === "number" ? gateway.pid : null;
20318
- const readyPath = managedScope?.databasePath ?? databasePath();
20319
- const lockedVersion = pid == null ? void 0 : gatewayVersionFromLock(pid, readyPath);
20320
- const gatewayEntry = runtime?.gatewayEntry ?? null;
20321
- return {
20322
- pm2Installed: true,
20323
- processFound: gateway != null,
20324
- status: gateway?.pm2_env?.status ?? null,
20325
- pid,
20326
- ready: pid != null && isGatewayReady(pid, readyPath),
20327
- runningVersion: lockedVersion === void 0 ? getRunningVersion(gatewayEnv, runtime?.cwd) : lockedVersion,
20328
- gatewayEntry,
20329
- gatewayPackageVersion: packageVersionFromGatewayEntry(gatewayEntry),
20330
- logRotationInstalled: processes.some((item) => item.name === "pm2-logrotate" && item.pm2_env?.status === "online"),
20331
- startupConfigured: process.platform === "darwin" ? isMacLaunchAgentConfigured(
20332
- gatewayEnv.PM2_HOME ?? join5(runtimeHome(gatewayEnv), ".pm2"),
20333
- runtime ?? void 0
20334
- ) : process.platform === "linux" ? isLinuxStartupConfigured(gatewayEnv, runtime?.cwd, runtime ?? void 0) : null,
20335
- currentScope: producerScope,
20336
- gatewayScope: managedScope,
20337
- scopeMatches: managedScope != null && scopesMatch(producerScope, managedScope),
20338
- gatewayOpenCode: runtime === null || !options.probeOpenCode ? null : diagnoseOpenCodeRuntime(runtime)
20339
- };
20340
- }
20341
- function pm2Bin(env = process.env) {
20342
- return env.SUPERTASK_PM2_BIN ?? (process.platform === "win32" ? "pm2.cmd" : "pm2");
20343
- }
20344
- function pm2CommandTimeoutMs(env = process.env) {
20345
- const configured = Number(env.SUPERTASK_PM2_COMMAND_TIMEOUT_MS ?? 15e3);
20346
- return Number.isFinite(configured) && configured > 0 ? configured : 15e3;
20347
- }
20348
- function resolvePm2Bin() {
20349
- const configured = pm2Bin();
20350
- if (isAbsolute2(configured)) return configured;
20351
- const result = spawnSync2("which", [configured], { encoding: "utf8" });
20352
- const resolved = result.status === 0 ? result.stdout.trim().split("\n")[0] : "";
20353
- if (!resolved) throw new Error(`[supertask] \u65E0\u6CD5\u89E3\u6790 pm2 \u53EF\u6267\u884C\u6587\u4EF6: ${configured}`);
20354
- return resolved;
20355
- }
20356
- function launchAgentPath() {
20357
- return process.env.SUPERTASK_LAUNCH_AGENT_PATH ?? join5(runtimeHome(process.env), "Library/LaunchAgents", `${MAC_LAUNCH_AGENT_LABEL}.plist`);
20358
- }
20359
- function launchctlBin() {
20360
- return process.env.SUPERTASK_LAUNCHCTL_BIN ?? "launchctl";
20361
- }
20362
- function xmlUnescape(value) {
20363
- return value.replaceAll("&apos;", "'").replaceAll("&quot;", '"').replaceAll("&gt;", ">").replaceAll("&lt;", "<").replaceAll("&amp;", "&");
20364
- }
20365
- function plistValue(contents, key) {
20366
- const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
20367
- const match2 = contents.match(new RegExp(`<key>\\s*${escapedKey}\\s*</key>\\s*<string>([^<]*)</string>`));
20368
- return match2?.[1] ? xmlUnescape(match2[1]) : null;
20369
- }
20370
- function isMacLaunchAgentConfigured(expectedPm2Home, expectedRuntime) {
20371
- if (typeof process.getuid !== "function") return false;
20372
- const plistPath = launchAgentPath();
20373
- if (!existsSync6(plistPath)) return false;
20374
20374
  try {
20375
- const contents = readFileSync3(plistPath, "utf8");
20376
- const argumentsBlock = contents.match(
20377
- /<key>\s*ProgramArguments\s*<\/key>\s*<array>([\s\S]*?)<\/array>/
20378
- )?.[1];
20379
- const programArguments = argumentsBlock ? [...argumentsBlock.matchAll(/<string>([^<]*)<\/string>/g)].map((match2) => xmlUnescape(match2[1])) : [];
20380
- const bunPath = programArguments[0];
20381
- const supervisorEntry = programArguments[1];
20382
- const pm2Path = programArguments[2];
20383
- const pm2Home = plistValue(contents, "PM2_HOME");
20384
- const managementLock = plistValue(contents, "SUPERTASK_PM2_MANAGEMENT_LOCK");
20385
- if (!bunPath || !supervisorEntry || !pm2Path || !pm2Home || !managementLock) return false;
20386
- if (expectedPm2Home && pm2Home !== expectedPm2Home) return false;
20387
- const expectedManagementLock = expectedRuntime ? managementLockPath(expectedRuntime.env, expectedRuntime.cwd) : process.env.SUPERTASK_PM2_MANAGEMENT_LOCK ? managementLockPath() : join5(expectedPm2Home ?? currentScope().pm2Home, "supertask-gateway.manage.sqlite");
20388
- if (managementLock !== expectedManagementLock) return false;
20389
- accessSync(bunPath, constants.X_OK);
20390
- accessSync(supervisorEntry, constants.R_OK);
20391
- accessSync(pm2Path, constants.X_OK);
20392
- if (spawnSync2(bunPath, ["--version"], {
20393
- stdio: "ignore",
20394
- timeout: pm2CommandTimeoutMs(),
20395
- killSignal: "SIGKILL"
20396
- }).status !== 0) return false;
20397
- if (spawnSync2(pm2Path, ["--version"], {
20398
- stdio: "ignore",
20399
- env: { ...process.env, PM2_HOME: pm2Home },
20400
- timeout: pm2CommandTimeoutMs(),
20401
- killSignal: "SIGKILL"
20402
- }).status !== 0) return false;
20403
- const loaded = spawnSync2(
20404
- launchctlBin(),
20405
- ["print", `gui/${process.getuid()}/${MAC_LAUNCH_AGENT_LABEL}`],
20406
- {
20407
- encoding: "utf8",
20408
- stdio: ["ignore", "pipe", "ignore"],
20409
- timeout: pm2CommandTimeoutMs(),
20410
- killSignal: "SIGKILL"
20411
- }
20412
- );
20413
- if (loaded.status !== 0) return false;
20414
- if (!loaded.stdout.includes("state = running")) return false;
20415
- if (!loaded.stdout.includes(`path = ${plistPath}`)) return false;
20416
- if (!loaded.stdout.includes(`program = ${bunPath}`)) return false;
20417
- const dumpPath = join5(pm2Home, "dump.pm2");
20418
- const dump = JSON.parse(readFileSync3(dumpPath, "utf8"));
20419
- if (!Array.isArray(dump)) return false;
20420
- const gateway = dump.find((item) => item.name === PROCESS_NAME);
20421
- const dumpRuntime = gatewayRuntimeFromProcess(gateway);
20422
- if (!dumpRuntime || !hasRestorableSavedGatewayRuntime(gateway)) return false;
20423
- return expectedRuntime === void 0 || dumpRuntime.gatewayEntry === expectedRuntime.gatewayEntry && dumpRuntime.bunPath === expectedRuntime.bunPath && dumpRuntime.cwd === expectedRuntime.cwd && scopesMatch(runtimeScope(dumpRuntime), runtimeScope(expectedRuntime));
20375
+ process.kill(pid, "SIGKILL");
20424
20376
  } catch {
20425
- return false;
20426
20377
  }
20427
20378
  }
20428
- function systemctlBin(env = process.env) {
20429
- return env.SUPERTASK_SYSTEMCTL_BIN ?? "systemctl";
20379
+ function diagnosticEntry() {
20380
+ const extension = import.meta.url.endsWith(".ts") ? "ts" : "js";
20381
+ const moduleDir = dirname6(fileURLToPath5(import.meta.url));
20382
+ const candidates = [
20383
+ join5(moduleDir, `../daemon/gateway-diagnostic-runner.${extension}`),
20384
+ join5(moduleDir, `../../src/daemon/gateway-diagnostic-runner.${extension}`)
20385
+ ];
20386
+ const entry = candidates.find((candidate) => existsSync6(candidate));
20387
+ if (!entry) throw new Error(`Gateway diagnostic runner \u4E0D\u5B58\u5728\uFF1A${candidates.join(", ")}`);
20388
+ return entry;
20430
20389
  }
20431
- function isLinuxStartupConfigured(env = process.env, cwd = process.cwd(), expectedRuntime) {
20432
- const unit = env.SUPERTASK_PM2_SYSTEMD_UNIT ?? `pm2-${userInfo().username}.service`;
20433
- const enabled = spawnSync2(systemctlBin(env), ["is-enabled", unit], {
20434
- encoding: "utf8",
20435
- env,
20436
- stdio: ["ignore", "pipe", "ignore"],
20437
- timeout: pm2CommandTimeoutMs(env),
20438
- killSignal: "SIGKILL"
20439
- });
20440
- if (enabled.status !== 0 || enabled.stdout.trim() !== "enabled") return false;
20441
- const contents = spawnSync2(systemctlBin(env), ["cat", unit], {
20442
- encoding: "utf8",
20443
- env,
20444
- stdio: ["ignore", "pipe", "ignore"],
20445
- timeout: pm2CommandTimeoutMs(env),
20446
- killSignal: "SIGKILL"
20390
+ async function runGatewayDiagnostic() {
20391
+ return new Promise((resolve3, reject) => {
20392
+ const child = spawn3(process.execPath, [diagnosticEntry()], {
20393
+ cwd: process.cwd(),
20394
+ env: process.env,
20395
+ detached: process.platform !== "win32",
20396
+ stdio: ["ignore", "pipe", "pipe"]
20397
+ });
20398
+ let stdout = "";
20399
+ let stderr = "";
20400
+ let timedOut = false;
20401
+ if (child.pid) activeProcessGroups.add(child.pid);
20402
+ child.stdout?.on("data", (chunk) => {
20403
+ stdout += chunk.toString();
20404
+ });
20405
+ child.stderr?.on("data", (chunk) => {
20406
+ stderr += chunk.toString();
20407
+ });
20408
+ child.once("error", reject);
20409
+ child.once("close", (exitCode) => {
20410
+ clearTimeout(timer);
20411
+ if (child.pid) {
20412
+ killDiagnosticGroup(child.pid);
20413
+ activeProcessGroups.delete(child.pid);
20414
+ }
20415
+ if (timedOut) {
20416
+ reject(new Error(`Gateway diagnostic runner \u8D85\u8FC7 ${DIAGNOSTIC_TIMEOUT_MS}ms \u672A\u5B8C\u6210`));
20417
+ return;
20418
+ }
20419
+ if (exitCode !== 0) {
20420
+ reject(new Error(stderr.trim() || `Gateway diagnostic runner \u9000\u51FA\u7801 ${exitCode}`));
20421
+ return;
20422
+ }
20423
+ try {
20424
+ resolve3(JSON.parse(stdout));
20425
+ } catch (error) {
20426
+ reject(error);
20427
+ }
20428
+ });
20429
+ const timer = setTimeout(() => {
20430
+ timedOut = true;
20431
+ if (child.pid) killDiagnosticGroup(child.pid);
20432
+ else child.kill("SIGKILL");
20433
+ }, DIAGNOSTIC_TIMEOUT_MS);
20447
20434
  });
20448
- if (contents.status !== 0) return false;
20449
- const expectedPm2Home = env.PM2_HOME ?? join5(runtimeHome(env), ".pm2");
20450
- if (!contents.stdout.includes("resurrect") || !contents.stdout.includes(expectedPm2Home)) {
20451
- return false;
20452
- }
20453
- try {
20454
- const dump = JSON.parse(readFileSync3(join5(expectedPm2Home, "dump.pm2"), "utf8"));
20455
- if (!Array.isArray(dump)) return false;
20456
- const gateway = dump.find((item) => item.name === PROCESS_NAME);
20457
- const runtime = gatewayRuntimeFromProcess(gateway);
20458
- if (!runtime || !hasRestorableSavedGatewayRuntime(gateway)) return false;
20459
- return runtime.cwd === resolve3(cwd) && scopesMatch(runtimeScope(runtime), runtimeScope({ cwd, env })) && (expectedRuntime === void 0 || runtime.gatewayEntry === expectedRuntime.gatewayEntry && runtime.bunPath === expectedRuntime.bunPath);
20460
- } catch {
20461
- return false;
20462
- }
20463
20435
  }
20464
- function isPm2Installed(env = process.env) {
20465
- const result = spawnSync2(pm2Bin(env), ["--version"], {
20466
- stdio: "ignore",
20467
- env,
20468
- shell: process.platform === "win32",
20469
- timeout: pm2CommandTimeoutMs(env),
20470
- killSignal: "SIGKILL"
20436
+ async function getDashboardGatewayDiagnostic(options = {}) {
20437
+ if (!options.fresh && cached && cached.expiresAt > Date.now()) return cached.diagnostic;
20438
+ if (!options.fresh && pending) return pending;
20439
+ const operation = runGatewayDiagnostic().then((diagnostic) => {
20440
+ cached = { expiresAt: Date.now() + CACHE_TTL_MS, diagnostic };
20441
+ return diagnostic;
20471
20442
  });
20472
- return result.status === 0;
20473
- }
20474
- function resolveCommand(command) {
20475
- const lookup = process.platform === "win32" ? "where" : "which";
20476
- const result = spawnSync2(lookup, [command], { encoding: "utf8" });
20477
- return result.status === 0 ? result.stdout.trim().split("\n")[0] || null : null;
20478
- }
20479
- function npmPreferredEnvironment() {
20480
- if (process.platform === "win32") return null;
20481
- const npm = resolveCommand("npm");
20482
- const node = resolveCommand("node");
20483
- if (!npm || !node) return null;
20484
- const command = resolvePm2Bin();
20485
- const path = [.../* @__PURE__ */ new Set([
20486
- dirname6(command),
20487
- dirname6(npm),
20488
- dirname6(node),
20489
- "/usr/local/bin",
20490
- "/usr/bin",
20491
- "/bin"
20492
- ])].join(delimiter);
20493
- return { command, env: { ...process.env, PATH: path } };
20494
- }
20495
- function pm2Exec(args, options = {}) {
20496
- const npmEnvironment = options.preferNpm ? npmPreferredEnvironment() : null;
20497
- const effectiveEnv = options.env ?? npmEnvironment?.env ?? process.env;
20498
- const result = spawnSync2(npmEnvironment?.command ?? pm2Bin(effectiveEnv), args, {
20499
- stdio: ["pipe", "pipe", "pipe"],
20500
- encoding: "utf-8",
20501
- env: effectiveEnv,
20502
- shell: process.platform === "win32",
20503
- timeout: options.timeoutMs ?? pm2CommandTimeoutMs(effectiveEnv),
20504
- killSignal: "SIGKILL"
20443
+ if (options.fresh) return operation;
20444
+ pending = operation.finally(() => {
20445
+ pending = null;
20505
20446
  });
20506
- const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
20507
- if (result.error) return { ok: false, output: result.error.message };
20508
- return { ok: result.status === 0, output };
20509
- }
20510
- function requirePm2(args, action, options = {}) {
20511
- const result = pm2Exec(args, options);
20512
- if (!result.ok) throw new Error(`[supertask] ${action} failed: ${result.output || "unknown pm2 error"}`);
20513
- return result.output;
20514
- }
20515
- function pm2JsonList(env = process.env) {
20516
- const output = requirePm2(["jlist"], "pm2 jlist", { env });
20517
- try {
20518
- const parsed = JSON.parse(output);
20519
- if (!Array.isArray(parsed)) throw new Error("result is not an array");
20520
- return parsed;
20521
- } catch (error) {
20522
- const message = error instanceof Error ? error.message : String(error);
20523
- throw new Error(`[supertask] Invalid pm2 jlist output: ${message}`);
20524
- }
20447
+ return pending;
20525
20448
  }
20526
- function gatewayEntryFromProcess(processInfo) {
20527
- const args = processInfo?.pm2_env?.args ?? processInfo?.args;
20528
- const candidates = Array.isArray(args) ? [...args].reverse() : typeof args === "string" ? [args] : [];
20529
- const savedCwd = processInfo?.pm2_env?.pm_cwd ?? processInfo?.pm_cwd;
20530
- for (const candidate of candidates) {
20531
- const path = typeof savedCwd === "string" ? runtimePath(candidate, savedCwd) : candidate;
20532
- if (existsSync6(path)) return resolve3(path);
20533
- }
20534
- return null;
20535
- }
20536
- function gatewayEnvironmentFromProcess(processInfo) {
20537
- const saved = processInfo?.pm2_env?.env ?? processInfo?.env;
20538
- if (!saved) return { ...process.env };
20539
- const env = {};
20540
- for (const [key, value] of Object.entries(saved)) {
20541
- if (typeof value === "string") env[key] = value;
20542
- }
20543
- return env;
20544
- }
20545
- function gatewayRuntimeFromProcess(processInfo) {
20546
- const gatewayEntry = gatewayEntryFromProcess(processInfo);
20547
- if (!gatewayEntry) return null;
20548
- const savedBunPath = processInfo?.pm2_env?.pm_exec_path ?? processInfo?.pm_exec_path;
20549
- const savedCwd = processInfo?.pm2_env?.pm_cwd ?? processInfo?.pm_cwd;
20550
- const savedEnv = gatewayEnvironmentFromProcess(processInfo);
20551
- if (typeof savedBunPath !== "string" || typeof savedCwd !== "string") return null;
20552
- try {
20553
- accessSync(savedBunPath, constants.X_OK);
20554
- if (!statSync3(savedCwd).isDirectory()) return null;
20555
- if (spawnSync2(savedBunPath, ["--version"], {
20556
- stdio: "ignore",
20557
- env: savedEnv,
20558
- timeout: pm2CommandTimeoutMs(savedEnv),
20559
- killSignal: "SIGKILL"
20560
- }).status !== 0) return null;
20561
- } catch {
20562
- return null;
20563
- }
20564
- const killTimeout = processInfo?.pm2_env?.kill_timeout ?? processInfo?.kill_timeout;
20565
- return {
20566
- gatewayEntry,
20567
- bunPath: savedBunPath,
20568
- cwd: resolve3(savedCwd),
20569
- env: savedEnv,
20570
- killTimeoutMs: Number.isInteger(killTimeout) && killTimeout >= 5e3 ? killTimeout : void 0
20571
- };
20572
- }
20573
- function hasRestorableSavedGatewayRuntime(processInfo) {
20574
- return gatewayRuntimeFromProcess(processInfo) !== null;
20575
- }
20576
- function databasePath(env = process.env, cwd = process.cwd()) {
20577
- return env.SUPERTASK_DB_PATH ? runtimePath(env.SUPERTASK_DB_PATH, cwd) : join5(runtimeHome(env), ".local/share/opencode/tasks.db");
20578
- }
20579
- function isGatewayReady(expectedPid, path = databasePath()) {
20580
- if (!existsSync6(path)) return false;
20581
- let database = null;
20582
- try {
20583
- database = new Database5(path, { readonly: true });
20584
- const row = database.query(
20585
- "SELECT pid, heartbeat_at, ready_at FROM gateway_lock WHERE id = 1"
20586
- ).get();
20587
- if (!row || row.ready_at == null) return false;
20588
- if (expectedPid !== void 0 && row.pid !== expectedPid) return false;
20589
- const ageMs = Date.now() - row.heartbeat_at;
20590
- return ageMs >= -5e3 && ageMs < GATEWAY_LOCK_STALE_MS;
20591
- } catch {
20592
- return false;
20593
- } finally {
20594
- database?.close();
20595
- }
20596
- }
20597
- function gatewayVersionFromLock(expectedPid, path) {
20598
- if (!existsSync6(path)) return void 0;
20599
- let database = null;
20600
- try {
20601
- database = new Database5(path, { readonly: true });
20602
- const row = database.query(
20603
- "SELECT pid, version FROM gateway_lock WHERE id = 1"
20604
- ).get();
20605
- if (!row || row.pid !== expectedPid) return void 0;
20606
- return row.version;
20607
- } catch {
20608
- return void 0;
20609
- } finally {
20610
- database?.close();
20611
- }
20612
- }
20613
- function managementLockPath(env = process.env, cwd = process.cwd()) {
20614
- const override = env.SUPERTASK_PM2_MANAGEMENT_LOCK;
20615
- if (override) return runtimePath(override, cwd);
20616
- return join5(runtimeScope({ env, cwd }).pm2Home, "supertask-gateway.manage.sqlite");
20617
- }
20618
- function canonicalManagementLockPath(env = process.env, cwd = process.cwd()) {
20619
- const home = runtimeHome(env);
20620
- const pm2Home = env.PM2_HOME ? runtimePath(env.PM2_HOME, cwd) : join5(home, ".pm2");
20621
- return join5(pm2Home, "supertask-gateway.manage.sqlite");
20622
- }
20623
- var __dirname, PROCESS_NAME, MAC_LAUNCH_AGENT_LABEL, GATEWAY_LOCK_STALE_MS;
20624
- var init_pm2 = __esm({
20625
- "src/daemon/pm2.ts"() {
20449
+ var CACHE_TTL_MS, DIAGNOSTIC_TIMEOUT_MS, cached, pending, activeProcessGroups;
20450
+ var init_gateway_diagnostic = __esm({
20451
+ "src/web/gateway-diagnostic.ts"() {
20626
20452
  "use strict";
20627
- init_config();
20628
- init_package_version();
20629
- init_management_lock();
20630
- init_package_version();
20631
- __dirname = dirname6(fileURLToPath5(import.meta.url));
20632
- PROCESS_NAME = "supertask-gateway";
20633
- MAC_LAUNCH_AGENT_LABEL = "com.supertask.pm2-resurrect";
20634
- GATEWAY_LOCK_STALE_MS = 3e4;
20453
+ CACHE_TTL_MS = 5e3;
20454
+ DIAGNOSTIC_TIMEOUT_MS = 2e4;
20455
+ cached = null;
20456
+ pending = null;
20457
+ activeProcessGroups = /* @__PURE__ */ new Set();
20458
+ process.once("exit", () => {
20459
+ for (const pid of activeProcessGroups) killDiagnosticGroup(pid);
20460
+ });
20635
20461
  }
20636
20462
  });
20637
20463
 
@@ -20721,6 +20547,7 @@ function clientMessages(locale) {
20721
20547
  "details.copySuccess",
20722
20548
  "details.id",
20723
20549
  "details.project",
20550
+ "details.variant",
20724
20551
  "details.prompt",
20725
20552
  "details.result",
20726
20553
  "details.category",
@@ -20821,6 +20648,7 @@ function clientMessages(locale) {
20821
20648
  "catalog.chooseProject",
20822
20649
  "catalog.defaultModel",
20823
20650
  "catalog.defaultProvider",
20651
+ "catalog.defaultVariant",
20824
20652
  "catalog.loading",
20825
20653
  "catalog.loaded",
20826
20654
  "catalog.failed",
@@ -20945,23 +20773,23 @@ function renderLayout(options) {
20945
20773
  function detailSession(value){if(!value)return text('details.none');const session=String(value);return session.length<=10?session:session.slice(0,6)+'***'+session.slice(-4)}
20946
20774
  function detailTaskResult(data){const presentation=data._resultPresentation;if(!presentation)return text('details.none');const parts=[];if(Array.isArray(presentation.errors)&&presentation.errors.length)parts.push(presentation.errors.join('\\n'));if(presentation.text)parts.push(presentation.text);return parts.join('\\n\\n')||text('details.none')}
20947
20775
  function detailField(label,value,options={}){const item=document.createElement('div');item.className='detail-item'+(options.wide?' wide':'');const name=document.createElement('div');name.className='detail-label';name.textContent=label;const content=options.long?document.createElement('pre'):document.createElement('div');content.className='detail-value'+(options.mono?' mono':'')+(options.long?' long':'');content.textContent=value===null||value===undefined||value===''?text('details.none'):String(value);item.append(name,content);return item}
20948
- function renderDetailHistory(runs){const section=document.createElement('section');section.className='detail-history';const title=document.createElement('h3');title.textContent=text('details.history');section.appendChild(title);if(!Array.isArray(runs)||runs.length===0){const empty=document.createElement('div');empty.className='muted small';empty.textContent=text('details.noHistory');section.appendChild(empty);return section}const list=document.createElement('div');list.className='detail-history-list';for(const run of runs){const item=document.createElement('div');item.className='detail-history-item';const primary=document.createElement('strong');primary.textContent='Run #'+run.id+' \xB7 '+detailStatus('run',run.status);const secondary=document.createElement('span');secondary.className='muted';secondary.textContent=detailDate(run.startedAt)+(run.model?' \xB7 '+detailModel(run.model):'');item.append(primary,secondary);list.appendChild(item)}section.appendChild(list);return section}
20776
+ function renderDetailHistory(runs){const section=document.createElement('section');section.className='detail-history';const title=document.createElement('h3');title.textContent=text('details.history');section.appendChild(title);if(!Array.isArray(runs)||runs.length===0){const empty=document.createElement('div');empty.className='muted small';empty.textContent=text('details.noHistory');section.appendChild(empty);return section}const list=document.createElement('div');list.className='detail-history-list';for(const run of runs){const item=document.createElement('div');item.className='detail-history-item';const primary=document.createElement('strong');primary.textContent='Run #'+run.id+' \xB7 '+detailStatus('run',run.status);const secondary=document.createElement('span');secondary.className='muted';secondary.textContent=detailDate(run.startedAt)+(run.model?' \xB7 '+detailModel(run.model):'')+(run.variant?' \xB7 '+run.variant:'');item.append(primary,secondary);list.appendChild(item)}section.appendChild(list);return section}
20949
20777
  function detailFields(type,data){if(type==='task')return [
20950
20778
  [text('details.id'),'#'+data.id],[text('table.name'),data.name],[text('table.status'),detailStatus('task',data.status)],[text('details.project'),data.cwd,{wide:true,mono:true}],
20951
- [text('table.agent'),data.agent],[text('table.model'),detailModel(data.model)],[text('details.prompt'),data.prompt,{wide:true,long:true}],
20779
+ [text('table.agent'),data.agent],[text('table.model'),detailModel(data.model)],[text('details.variant'),data.variant||text('details.default')],[text('details.prompt'),data.prompt,{wide:true,long:true}],
20952
20780
  [text('details.category'),data.category],[text('details.batch'),data.batchId],[text('details.dependency'),data.dependsOn?'#'+data.dependsOn:text('details.none')],
20953
20781
  [text('details.importance'),data.importance],[text('details.urgency'),data.urgency],[text('details.retryCount'),String(data.retryCount??0)+' / '+String(data.maxRetries??0)],
20954
20782
  [text('details.retryBackoff'),detailDuration(data.retryBackoffMs)],[text('details.timeout'),detailDuration(data.timeoutMs)],[text('details.scheduledAt'),detailDate(data.scheduledAt)],
20955
20783
  [text('details.createdAt'),detailDate(data.createdAt)],[text('details.startedAt'),detailDate(data.startedAt)],[text('details.finishedAt'),detailDate(data.finishedAt)],
20956
20784
  [text('details.result'),detailTaskResult(data),{wide:true,long:true}]
20957
20785
  ];if(type==='run'){const started=data.startedAt?new Date(data.startedAt).getTime():null;const finished=data.finishedAt?new Date(data.finishedAt).getTime():null;return [
20958
- [text('details.id'),'Run #'+data.id],[text('details.taskId'),'#'+data.taskId],[text('table.status'),detailStatus('run',data.status)],[text('table.model'),detailModel(data.model)],
20786
+ [text('details.id'),'Run #'+data.id],[text('details.taskId'),'#'+data.taskId],[text('table.status'),detailStatus('run',data.status)],[text('table.model'),detailModel(data.model)],[text('details.variant'),data.variant||text('details.default')],
20959
20787
  [text('details.session'),detailSession(data.sessionId)],[text('details.startedAt'),detailDate(data.startedAt)],[text('details.finishedAt'),detailDate(data.finishedAt)],
20960
20788
  [text('table.duration'),started!==null?detailDuration((finished??Date.now())-started):text('details.none')],[text('details.heartbeat'),detailDate(data.heartbeatAt)],
20961
20789
  [text('details.process'),'Worker PID '+String(data.workerPid??'\u2014')+' \xB7 OpenCode PID '+String(data.childPid??'\u2014'),{wide:true,mono:true}]
20962
20790
  ];}const scheduleRule=data.scheduleType==='cron'?data.cronExpr:data.scheduleType==='recurring'?detailDuration(data.intervalMs):detailDate(data.runAt);return [
20963
20791
  [text('details.id'),'#'+data.id],[text('table.name'),data.name],[text('details.enabled'),data.enabled?text('details.enabledYes'):text('details.enabledNo')],[text('details.project'),data.cwd,{wide:true,mono:true}],
20964
- [text('table.agent'),data.agent],[text('table.model'),detailModel(data.model)],[text('details.prompt'),data.prompt,{wide:true,long:true}],
20792
+ [text('table.agent'),data.agent],[text('table.model'),detailModel(data.model)],[text('details.variant'),data.variant||text('details.default')],[text('details.prompt'),data.prompt,{wide:true,long:true}],
20965
20793
  [text('template.scheduleType'),detailScheduleType(data.scheduleType)],[text('details.scheduleRule'),scheduleRule],[text('details.category'),data.category],[text('details.batch'),data.batchId],
20966
20794
  [text('details.importance'),data.importance],[text('details.urgency'),data.urgency],[text('details.maxInstances'),data.maxInstances],[text('details.maxRetries'),data.maxRetries??0],
20967
20795
  [text('details.retryBackoff'),detailDuration(data.retryBackoffMs)],[text('details.timeout'),detailDuration(data.timeoutMs)],[text('details.lastRun'),detailDate(data.lastRunAt)],[text('details.nextRun'),detailDate(data.nextRunAt)],
@@ -20978,12 +20806,20 @@ function renderLayout(options) {
20978
20806
  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')}
20979
20807
  const catalogTimers={};const catalogRequests={};const catalogModels={};
20980
20808
  function catalogField(prefix,name){return document.getElementById(prefix+'-'+name)}
20981
- function resetCatalog(prefix){const agent=catalogField(prefix,'agent');const provider=catalogField(prefix,'model-provider');const model=catalogField(prefix,'model');if(!agent||!provider||!model)return;catalogModels[prefix]=[];agent.replaceChildren(new Option(text('catalog.chooseProject'),''));provider.replaceChildren(new Option(text('catalog.defaultProvider'),''));model.replaceChildren(new Option(text('catalog.defaultModel'),'default'));agent.disabled=false;provider.disabled=true;model.disabled=true;catalogField(prefix,'catalog-status').textContent='';}
20809
+ function resetCatalog(prefix){const agent=catalogField(prefix,'agent');const provider=catalogField(prefix,'model-provider');const model=catalogField(prefix,'model');const variant=catalogField(prefix,'variant');if(!agent||!provider||!model||!variant)return;const preferredVariant=variant.dataset.preferred||'';catalogModels[prefix]=[];agent.replaceChildren(new Option(text('catalog.chooseProject'),''));provider.replaceChildren(new Option(text('catalog.defaultProvider'),''));model.replaceChildren(new Option(text('catalog.defaultModel'),'default'));variant.replaceChildren(new Option(text('catalog.defaultVariant'),''));if(preferredVariant)appendCurrentOption(variant,preferredVariant);variant.value=preferredVariant;agent.disabled=false;provider.disabled=true;model.disabled=true;variant.disabled=!preferredVariant;catalogField(prefix,'catalog-status').textContent='';}
20982
20810
  function appendCurrentOption(select,value){if(!value||[...select.options].some(option=>option.value===value))return;select.appendChild(new Option(value,value));}
20811
+ function setPreferredVariant(prefix,modelValue,variantValue){const variant=catalogField(prefix,'variant');if(!variant)return;const preferred=variantValue||'';variant.dataset.preferred=preferred;variant.dataset.preferredModel=modelValue||'default';variant.replaceChildren(new Option(text('catalog.defaultVariant'),''));if(preferred)appendCurrentOption(variant,preferred);variant.value=preferred;variant.disabled=!preferred}
20812
+ function invalidateVariantRequest(prefix){const key=prefix+'-variant';catalogRequests[key]=(catalogRequests[key]||0)+1}
20813
+ function invalidateCatalogRequests(prefix){clearTimeout(catalogTimers[prefix]);catalogRequests[prefix]=(catalogRequests[prefix]||0)+1;invalidateVariantRequest(prefix)}
20814
+ function clearVariantPreference(prefix){const variant=catalogField(prefix,'variant');if(!variant)return;invalidateVariantRequest(prefix);variant.dataset.preferred='';variant.dataset.preferredModel='';variant.replaceChildren(new Option(text('catalog.defaultVariant'),''));variant.value='';variant.disabled=true}
20815
+ function handleProviderChange(prefix){clearVariantPreference(prefix);populateModelOptions(prefix)}
20816
+ function handleModelChange(prefix){clearVariantPreference(prefix);populateVariantOptions(prefix)}
20817
+ function handleVariantChange(prefix){const variant=catalogField(prefix,'variant');if(!variant)return;invalidateVariantRequest(prefix);variant.dataset.preferred='';variant.dataset.preferredModel=''}
20983
20818
  function modelProvider(value){const slash=value.indexOf('/');return slash>0?value.slice(0,slash):''}
20984
- function populateModelOptions(prefix,preferredModel=''){const provider=catalogField(prefix,'model-provider');const model=catalogField(prefix,'model');if(!provider||!model)return;const selectedProvider=provider.value;model.replaceChildren();if(!selectedProvider){model.appendChild(new Option(text('catalog.defaultModel'),'default'));model.disabled=true;return}const available=(catalogModels[prefix]||[]).filter(value=>modelProvider(value)===selectedProvider);for(const value of available)model.appendChild(new Option(value,value));if(preferredModel&&available.includes(preferredModel))model.value=preferredModel;model.disabled=available.length===0}
20819
+ function populateModelOptions(prefix,preferredModel=''){const provider=catalogField(prefix,'model-provider');const model=catalogField(prefix,'model');if(!provider||!model)return;const selectedProvider=provider.value;model.replaceChildren();if(!selectedProvider){model.appendChild(new Option(text('catalog.defaultModel'),'default'));model.disabled=true;populateVariantOptions(prefix);return}const available=(catalogModels[prefix]||[]).filter(value=>modelProvider(value)===selectedProvider);for(const value of available)model.appendChild(new Option(value,value));if(preferredModel&&available.includes(preferredModel))model.value=preferredModel;model.disabled=available.length===0;populateVariantOptions(prefix)}
20820
+ async function populateVariantOptions(prefix,preferredVariant=''){const cwd=catalogField(prefix,'cwd')?.value.trim()||'';const model=catalogField(prefix,'model');const variant=catalogField(prefix,'variant');if(!model||!variant)return;const selectedModel=model.value;const preferred=preferredVariant||(variant.dataset.preferredModel===selectedModel?variant.dataset.preferred||'':'');if(!cwd||!selectedModel||selectedModel==='default'){variant.replaceChildren(new Option(text('catalog.defaultVariant'),''));if(preferred)appendCurrentOption(variant,preferred);variant.value=preferred;variant.dataset.preferred='';variant.dataset.preferredModel='';variant.disabled=!preferred;return}if(!preferred){variant.replaceChildren(new Option(text('catalog.defaultVariant'),''));variant.value=''}const requestKey=prefix+'-variant';const request=(catalogRequests[requestKey]||0)+1;catalogRequests[requestKey]=request;variant.disabled=true;try{const data=await readJson(await fetch('/api/opencode/catalog?cwd='+encodeURIComponent(cwd)));if(catalogRequests[requestKey]!==request||catalogField(prefix,'cwd')?.value.trim()!==cwd||model.value!==selectedModel)return;const available=Array.isArray(data.variantsByModel?.[selectedModel])?data.variantsByModel[selectedModel]:[];variant.replaceChildren(new Option(text('catalog.defaultVariant'),''));for(const value of available)variant.appendChild(new Option(value,value));if(preferred)appendCurrentOption(variant,preferred);variant.value=[...variant.options].some(option=>option.value===preferred)?preferred:'';variant.dataset.preferred='';variant.dataset.preferredModel='';variant.disabled=available.length===0&&!preferred}catch(error){if(catalogRequests[requestKey]!==request||catalogField(prefix,'cwd')?.value.trim()!==cwd||model.value!==selectedModel)return;if(preferred)appendCurrentOption(variant,preferred);variant.value=preferred;variant.dataset.preferred=preferred;variant.dataset.preferredModel=selectedModel;variant.disabled=!preferred}}
20985
20821
  async function loadCatalog(prefix,preferredAgent='',preferredModel='default',preserveUnavailable=false){const cwd=catalogField(prefix,'cwd')?.value.trim()||'';const status=catalogField(prefix,'catalog-status');const agent=catalogField(prefix,'agent');const provider=catalogField(prefix,'model-provider');const model=catalogField(prefix,'model');if(!cwd||!status||!agent||!provider||!model){if(!cwd)resetCatalog(prefix);return}const request=(catalogRequests[prefix]||0)+1;catalogRequests[prefix]=request;status.dataset.state='loading';status.textContent=text('catalog.loading');agent.disabled=true;provider.disabled=true;model.disabled=true;try{const data=await readJson(await fetch('/api/opencode/catalog?cwd='+encodeURIComponent(cwd)));if(catalogRequests[prefix]!==request||catalogField(prefix,'cwd').value.trim()!==cwd)return;agent.replaceChildren();for(const item of data.agents){const label=item.name+' \u2014 '+text('catalog.'+item.mode);agent.appendChild(new Option(label,item.name))}if(preserveUnavailable)appendCurrentOption(agent,preferredAgent);const defaultAgent=preferredAgent||data.agents.find(item=>item.name==='build')?.name||data.agents.find(item=>item.mode==='primary')?.name||data.agents[0]?.name||'';agent.value=[...agent.options].some(option=>option.value===defaultAgent)?defaultAgent:(agent.options[0]?.value||'');catalogModels[prefix]=[...data.models];if(preserveUnavailable&&preferredModel&&preferredModel!=='default'&&!catalogModels[prefix].includes(preferredModel))catalogModels[prefix].push(preferredModel);provider.replaceChildren(new Option(text('catalog.defaultProvider'),''));for(const name of [...new Set(catalogModels[prefix].map(modelProvider).filter(Boolean))].sort())provider.appendChild(new Option(name,name));const preferredProvider=preferredModel==='default'?'':modelProvider(preferredModel);provider.value=[...provider.options].some(option=>option.value===preferredProvider)?preferredProvider:'';populateModelOptions(prefix,preferredModel);status.dataset.state='ready';status.textContent=text('catalog.loaded',{agents:data.agents.length,models:data.models.length})}catch(error){if(catalogRequests[prefix]!==request)return;resetCatalog(prefix);if(preserveUnavailable){appendCurrentOption(agent,preferredAgent);if(preferredModel&&preferredModel!=='default'){catalogModels[prefix]=[preferredModel];appendCurrentOption(provider,modelProvider(preferredModel));provider.value=modelProvider(preferredModel);populateModelOptions(prefix,preferredModel)}}status.dataset.state='error';status.textContent=text('catalog.failed',{error:error.message})}finally{if(catalogRequests[prefix]===request){agent.disabled=false;provider.disabled=false;if(provider.value)model.disabled=false}}}
20986
- function scheduleCatalogLoad(prefix){clearTimeout(catalogTimers[prefix]);catalogTimers[prefix]=setTimeout(()=>loadCatalog(prefix),450)}
20822
+ function scheduleCatalogLoad(prefix){invalidateCatalogRequests(prefix);resetCatalog(prefix);catalogTimers[prefix]=setTimeout(()=>loadCatalog(prefix),450)}
20987
20823
  let directoryTargetId='';let directoryCurrent='';let directoryEntries=[];let directoryShowHidden=false;
20988
20824
  function renderDirectoryEntries(){const list=document.getElementById('directory-list');list.replaceChildren();const entries=directoryEntries.filter(entry=>directoryShowHidden||!entry.hidden);const hidden=document.getElementById('directory-hidden');hidden.textContent=text(directoryShowHidden?'action.hideHidden':'action.showHidden');if(entries.length===0){const empty=document.createElement('div');empty.className='directory-empty';empty.textContent=text('directory.empty');list.appendChild(empty);return}for(const entry of entries){const button=document.createElement('button');button.type='button';button.className='directory-item';button.innerHTML='${icon("folder")}<span></span>';button.querySelector('span').textContent=entry.name;button.onclick=()=>browseDirectory(entry.path);list.appendChild(button)}}
20989
20825
  async function browseDirectory(path=''){const choose=document.getElementById('directory-choose');choose.disabled=true;try{const suffix=path?'?path='+encodeURIComponent(path):'';const data=await readJson(await fetch('/api/filesystem/directories'+suffix));directoryCurrent=data.path;directoryEntries=data.directories;document.getElementById('directory-path').textContent=data.path;document.getElementById('directory-up').disabled=data.parent===data.path;document.getElementById('directory-up').onclick=()=>browseDirectory(data.parent);document.getElementById('directory-home').onclick=()=>browseDirectory(data.home);renderDirectoryEntries();choose.disabled=false;return true}catch(error){directoryCurrent='';directoryEntries=[];document.getElementById('directory-path').textContent='';renderDirectoryEntries();showToast(error.message,'error');return false}}
@@ -20993,16 +20829,16 @@ function renderLayout(options) {
20993
20829
  function updateDurationControl(id){const preset=document.getElementById(id+'-preset');const custom=document.getElementById(id+'-custom');const input=document.getElementById(id+'-value');const visible=preset.value==='custom';custom.hidden=!visible;input.required=visible;if(visible&&!input.value)input.value='1'}
20994
20830
  function readDuration(id){const preset=document.getElementById(id+'-preset').value;if(preset==='')return '';if(preset!=='custom')return preset==='0'?'0':preset+'ms';const value=document.getElementById(id+'-value').value.trim();return value===''?'':value+document.getElementById(id+'-unit').value}
20995
20831
  function setDuration(id,milliseconds){const preset=document.getElementById(id+'-preset');const exact=milliseconds==null?'':String(milliseconds);if([...preset.options].some(option=>option.value===exact)){preset.value=exact;updateDurationControl(id);return}preset.value='custom';const input=document.getElementById(id+'-value');const unit=document.getElementById(id+'-unit');const units=[['d',86400000],['h',3600000],['min',60000],['s',1000]];let matched=false;for(const [name,factor] of units){if(milliseconds!=null&&(milliseconds===0||milliseconds%factor===0)){input.value=String(milliseconds/factor);unit.value=name;matched=true;break}}if(!matched){input.value=String((milliseconds??60000)/1000);unit.value='s'}updateDurationControl(id)}
20996
- function openTaskCreator(){const form=document.getElementById('task-form');form.reset();taskField('id').value='';taskField('cwd').readOnly=false;taskField('cwd-picker').hidden=false;taskField('cwd').value=form.dataset.defaultCwd||'';setDuration('task-retry-backoff',30000);setDuration('task-timeout',null);taskField('dialog-title').textContent=text('task.createTitle');taskField('save').textContent=text('action.saveTask');updateTaskProjectStatus();resetCatalog('task');document.getElementById('task-dialog').showModal();if(taskField('cwd').value)loadCatalog('task');setTimeout(()=>taskField('name').focus(),50)}
20997
- async function openTaskEditor(id){try{const data=await readJson(await fetch('/api/tasks/'+id));taskField('id').value=String(id);taskField('cwd').value=data.cwd||'';taskField('cwd').readOnly=true;taskField('cwd-picker').hidden=true;taskField('name').value=data.name||'';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);setDuration('task-retry-backoff',data.retryBackoffMs??30000);setDuration('task-timeout',data.timeoutMs);taskField('dialog-title').textContent=text('task.editTitle');taskField('save').textContent=text('action.updateTask');updateTaskProjectStatus();resetCatalog('task');document.getElementById('task-dialog').showModal();loadCatalog('task',data.agent||'',data.model||'default',true);setTimeout(()=>taskField('name').focus(),50)}catch(error){showToast(error.message,'error')}}
20998
- 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:readDuration('task-retry-backoff'),timeout:readDuration('task-timeout')};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}}
20832
+ function openTaskCreator(){invalidateCatalogRequests('task');const form=document.getElementById('task-form');form.reset();taskField('id').value='';setPreferredVariant('task','default','');taskField('cwd').readOnly=false;taskField('cwd-picker').hidden=false;taskField('cwd').value=form.dataset.defaultCwd||'';setDuration('task-retry-backoff',30000);setDuration('task-timeout',null);taskField('dialog-title').textContent=text('task.createTitle');taskField('save').textContent=text('action.saveTask');updateTaskProjectStatus();resetCatalog('task');document.getElementById('task-dialog').showModal();if(taskField('cwd').value)loadCatalog('task');setTimeout(()=>taskField('name').focus(),50)}
20833
+ async function openTaskEditor(id){invalidateCatalogRequests('task');try{const data=await readJson(await fetch('/api/tasks/'+id));document.getElementById('task-form').reset();taskField('id').value=String(id);taskField('cwd').value=data.cwd||'';taskField('cwd').readOnly=true;taskField('cwd-picker').hidden=true;taskField('name').value=data.name||'';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);setDuration('task-retry-backoff',data.retryBackoffMs??30000);setDuration('task-timeout',data.timeoutMs);taskField('dialog-title').textContent=text('task.editTitle');taskField('save').textContent=text('action.updateTask');updateTaskProjectStatus();setPreferredVariant('task',data.model||'default',data.variant||'');resetCatalog('task');document.getElementById('task-dialog').showModal();loadCatalog('task',data.agent||'',data.model||'default',true);setTimeout(()=>taskField('name').focus(),50)}catch(error){showToast(error.message,'error')}}
20834
+ 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,variant:taskField('variant').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:readDuration('task-retry-backoff'),timeout:readDuration('task-timeout')};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}}
20999
20835
  function localDateTime(milliseconds){const date=new Date(milliseconds);const local=new Date(milliseconds-date.getTimezoneOffset()*60000);return local.toISOString().slice(0,23)}
21000
20836
  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;for(const control of node.querySelectorAll('input,select'))control.required=false;const required=name==='recurring'?node.querySelector('[id$="-preset"]'):node.querySelector('input');if(required)required.required=name===type;if(name==='recurring'&&name===type)updateDurationControl('template-interval')}}
21001
20837
  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}
21002
20838
  function selectedRunAt(){const input=templateField('run-at');return resolveEditedRunAt(input.dataset.originalEpoch?Number(input.dataset.originalEpoch):null,input.dataset.originalLocal||'',input.value)}
21003
- function openTemplateCreator(){const form=document.getElementById('template-form');form.reset();templateField('id').value='';templateField('dialog-title').textContent=text('template.createTitle');setDuration('template-interval',3600000);setDuration('template-retry-backoff',30000);setDuration('template-timeout',null);setOriginalRunAt(null);templateField('run-at').value=localDateTime(Date.now()+3600000);updateTemplateScheduleFields();resetCatalog('template');document.getElementById('template-dialog').showModal();if(templateField('cwd').value)loadCatalog('template');setTimeout(()=>templateField('name').focus(),50)}
21004
- 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('prompt').value=data.prompt||'';templateField('schedule-type').value=data.scheduleType;templateField('cron').value=data.cronExpr||'';setDuration('template-interval',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);setDuration('template-retry-backoff',data.retryBackoffMs??30000);setDuration('template-timeout',data.timeoutMs);updateTemplateScheduleFields();resetCatalog('template');document.getElementById('template-dialog').showModal();loadCatalog('template',data.agent||'',data.model||'default',true);setTimeout(()=>templateField('name').focus(),50)}catch(error){showToast(error.message,'error')}}
21005
- 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:readDuration('template-interval'),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:readDuration('template-retry-backoff'),timeout:readDuration('template-timeout')};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}}
20839
+ function openTemplateCreator(){invalidateCatalogRequests('template');const form=document.getElementById('template-form');form.reset();templateField('id').value='';setPreferredVariant('template','default','');templateField('dialog-title').textContent=text('template.createTitle');setDuration('template-interval',3600000);setDuration('template-retry-backoff',30000);setDuration('template-timeout',null);setOriginalRunAt(null);templateField('run-at').value=localDateTime(Date.now()+3600000);updateTemplateScheduleFields();resetCatalog('template');document.getElementById('template-dialog').showModal();if(templateField('cwd').value)loadCatalog('template');setTimeout(()=>templateField('name').focus(),50)}
20840
+ async function openTemplateEditor(id){invalidateCatalogRequests('template');try{const data=await readJson(await fetch('/api/templates/'+id));document.getElementById('template-form').reset();templateField('id').value=String(id);templateField('dialog-title').textContent=text('template.editTitle');templateField('name').value=data.name||'';templateField('cwd').value=data.cwd||'';templateField('prompt').value=data.prompt||'';templateField('schedule-type').value=data.scheduleType;templateField('cron').value=data.cronExpr||'';setDuration('template-interval',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);setDuration('template-retry-backoff',data.retryBackoffMs??30000);setDuration('template-timeout',data.timeoutMs);updateTemplateScheduleFields();setPreferredVariant('template',data.model||'default',data.variant||'');resetCatalog('template');document.getElementById('template-dialog').showModal();loadCatalog('template',data.agent||'',data.model||'default',true);setTimeout(()=>templateField('name').focus(),50)}catch(error){showToast(error.message,'error')}}
20841
+ 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,variant:templateField('variant').value,prompt:templateField('prompt').value,scheduleType:type,cronExpr:templateField('cron').value,interval:readDuration('template-interval'),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:readDuration('template-retry-backoff'),timeout:readDuration('template-timeout')};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}}
21006
20842
  async function enableTmpl(id){try{await readJson(await fetch('/api/templates/'+id+'/enable',{method:'POST'}));location.reload()}catch(error){showToast(error.message,'error')}}
21007
20843
  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')}}
21008
20844
  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')}}
@@ -21184,6 +21020,7 @@ var init_ui = __esm({
21184
21020
  "template.cwdHint": "OpenCode \u4F1A\u5728\u8FD9\u4E2A\u76EE\u5F55\u4E2D\u6267\u884C\u4EFB\u52A1\u3002",
21185
21021
  "template.agent": "Agent",
21186
21022
  "template.model": "\u6A21\u578B",
21023
+ "template.variant": "\u6A21\u578B Variant",
21187
21024
  "template.prompt": "\u63D0\u793A\u8BCD",
21188
21025
  "template.scheduleType": "\u6267\u884C\u65B9\u5F0F",
21189
21026
  "template.cronExpr": "Cron \u8868\u8FBE\u5F0F",
@@ -21221,9 +21058,11 @@ var init_ui = __esm({
21221
21058
  "catalog.chooseProject": "\u8BF7\u5148\u9009\u62E9\u9879\u76EE\u76EE\u5F55",
21222
21059
  "catalog.defaultModel": "\u8DDF\u968F Agent / OpenCode \u9ED8\u8BA4\u6A21\u578B",
21223
21060
  "catalog.defaultProvider": "\u9ED8\u8BA4\u6A21\u578B",
21061
+ "catalog.defaultVariant": "\u8DDF\u968F Agent / \u6A21\u578B\u9ED8\u8BA4\u8BBE\u7F6E",
21224
21062
  "catalog.provider": "\u6A21\u578B\u63D0\u4F9B\u5546",
21225
21063
  "catalog.model": "\u5177\u4F53\u6A21\u578B",
21226
- "catalog.modelHint": "\u5148\u9009\u63D0\u4F9B\u5546\uFF0C\u518D\u9009\u672C\u9879\u76EE opencode models \u8FD4\u56DE\u7684\u6A21\u578B\uFF1B\u9ED8\u8BA4\u9009\u9879\u4E0D\u4F1A\u4F20\u5165 -m\u3002",
21064
+ "catalog.modelHint": "\u5148\u9009\u63D0\u4F9B\u5546\uFF0C\u518D\u9009\u672C\u9879\u76EE opencode models --verbose \u8FD4\u56DE\u7684\u6A21\u578B\uFF1B\u9ED8\u8BA4\u9009\u9879\u4E0D\u4F1A\u4F20\u5165 -m\u3002",
21065
+ "catalog.variantHint": "\u4EC5\u663E\u793A\u6240\u9009\u6A21\u578B\u58F0\u660E\u652F\u6301\u7684 variants\uFF1B\u9ED8\u8BA4\u9009\u9879\u4E0D\u4F1A\u4F20\u5165 --variant\u3002",
21227
21066
  "catalog.agentHint": "\u6765\u81EA\u5F53\u524D\u9879\u76EE\u7684 opencode agent list\u3002",
21228
21067
  "catalog.loading": "\u6B63\u5728\u8BFB\u53D6\u6B64\u9879\u76EE\u53EF\u7528\u7684 Agent \u548C\u6A21\u578B\u2026",
21229
21068
  "catalog.loaded": "\u5DF2\u4ECE\u672C\u673A OpenCode \u8BFB\u53D6 {agents} \u4E2A Agent\u3001{models} \u4E2A\u6A21\u578B\u3002",
@@ -21254,6 +21093,7 @@ var init_ui = __esm({
21254
21093
  "details.copySuccess": "\u539F\u59CB\u6570\u636E\u5DF2\u590D\u5236",
21255
21094
  "details.id": "\u7F16\u53F7",
21256
21095
  "details.project": "\u9879\u76EE\u76EE\u5F55",
21096
+ "details.variant": "\u6A21\u578B Variant",
21257
21097
  "details.prompt": "\u63D0\u793A\u8BCD",
21258
21098
  "details.result": "\u6267\u884C\u7ED3\u679C / \u5931\u8D25\u539F\u56E0",
21259
21099
  "details.category": "\u5206\u7C7B",
@@ -21286,7 +21126,7 @@ var init_ui = __esm({
21286
21126
  "details.enabledYes": "\u5DF2\u542F\u7528",
21287
21127
  "details.enabledNo": "\u5DF2\u505C\u7528",
21288
21128
  "dialog.cancelTask": "\u53D6\u6D88\u4EFB\u52A1 #{id}\uFF1F",
21289
- "dialog.cancelTaskBody": "\u8FD0\u884C\u4E2D\u7684\u4EFB\u52A1\u4F1A\u5728\u4E0B\u4E00\u4E2A\u8F6E\u8BE2\u5468\u671F\u7EC8\u6B62\u5BF9\u5E94\u8FDB\u7A0B\u6811\u3002",
21129
+ "dialog.cancelTaskBody": "\u8FD0\u884C\u4E2D\u7684\u4EFB\u52A1\u4F1A\u5728\u4E0B\u4E00\u4E2A\u8F6E\u8BE2\u5468\u671F\u7EC8\u6B62\u5BF9\u5E94\u7684\u53D7\u7BA1\u8FDB\u7A0B\u7EC4\u3002",
21290
21130
  "dialog.retryTask": "\u91CD\u8BD5\u4EFB\u52A1 #{id}\uFF1F",
21291
21131
  "dialog.retryTaskBody": "\u4EFB\u52A1\u5C06\u56DE\u5230\u5F85\u6267\u884C\u72B6\u6001\uFF0C\u5E76\u91CD\u7F6E\u81EA\u52A8\u91CD\u8BD5\u9884\u7B97\u3002",
21292
21132
  "dialog.deleteTask": "\u5220\u9664\u4EFB\u52A1 #{id}\uFF1F",
@@ -21485,6 +21325,7 @@ var init_ui = __esm({
21485
21325
  "template.cwdHint": "OpenCode runs the task in this directory.",
21486
21326
  "template.agent": "Agent",
21487
21327
  "template.model": "Model",
21328
+ "template.variant": "Model variant",
21488
21329
  "template.prompt": "Prompt",
21489
21330
  "template.scheduleType": "Schedule",
21490
21331
  "template.cronExpr": "Cron expression",
@@ -21522,9 +21363,11 @@ var init_ui = __esm({
21522
21363
  "catalog.chooseProject": "Choose a project directory first",
21523
21364
  "catalog.defaultModel": "Use the Agent / OpenCode default model",
21524
21365
  "catalog.defaultProvider": "Default model",
21366
+ "catalog.defaultVariant": "Use the Agent / model default",
21525
21367
  "catalog.provider": "Model provider",
21526
21368
  "catalog.model": "Model",
21527
- "catalog.modelHint": "Choose a provider, then a model returned by opencode models for this project. Default does not pass -m.",
21369
+ "catalog.modelHint": "Choose a provider, then a model returned by opencode models --verbose for this project. Default does not pass -m.",
21370
+ "catalog.variantHint": "Only variants declared by the selected model are shown. Default does not pass --variant.",
21528
21371
  "catalog.agentHint": "Loaded from opencode agent list for this project.",
21529
21372
  "catalog.loading": "Loading Agents and models available to this project\u2026",
21530
21373
  "catalog.loaded": "Loaded {agents} Agents and {models} models from local OpenCode.",
@@ -21555,6 +21398,7 @@ var init_ui = __esm({
21555
21398
  "details.copySuccess": "Raw data copied",
21556
21399
  "details.id": "ID",
21557
21400
  "details.project": "Project directory",
21401
+ "details.variant": "Model variant",
21558
21402
  "details.prompt": "Prompt",
21559
21403
  "details.result": "Result / failure reason",
21560
21404
  "details.category": "Category",
@@ -21587,7 +21431,7 @@ var init_ui = __esm({
21587
21431
  "details.enabledYes": "Enabled",
21588
21432
  "details.enabledNo": "Disabled",
21589
21433
  "dialog.cancelTask": "Cancel task #{id}?",
21590
- "dialog.cancelTaskBody": "A running task will terminate its process tree on the next worker poll.",
21434
+ "dialog.cancelTaskBody": "A running task will terminate its managed process group on the next worker poll.",
21591
21435
  "dialog.retryTask": "Retry task #{id}?",
21592
21436
  "dialog.retryTaskBody": "The task returns to pending and its automatic retry budget is reset.",
21593
21437
  "dialog.deleteTask": "Delete task #{id}?",
@@ -22009,14 +21853,14 @@ __export(web_exports, {
22009
21853
  });
22010
21854
  import {
22011
21855
  existsSync as existsSync7,
22012
- mkdirSync as mkdirSync4,
22013
- readFileSync as readFileSync4,
21856
+ mkdirSync as mkdirSync3,
21857
+ readFileSync as readFileSync3,
22014
21858
  readdirSync,
22015
21859
  renameSync as renameSync2,
22016
- statSync as statSync4,
22017
- writeFileSync as writeFileSync3
21860
+ statSync as statSync3,
21861
+ writeFileSync as writeFileSync2
22018
21862
  } from "fs";
22019
- import { homedir as homedir4 } from "os";
21863
+ import { homedir as homedir3 } from "os";
22020
21864
  import { basename as basename3, dirname as dirname7, join as join6 } from "path";
22021
21865
  function setDashboardRuntimeConfig(config) {
22022
21866
  runtimeConfig = config === null ? null : structuredClone(config);
@@ -22042,10 +21886,13 @@ function resolveDashboardConfigState(runtimeAvailable, restartRequired, managedR
22042
21886
  function isSafeDashboardRestartTarget(diagnostic, currentPid) {
22043
21887
  return diagnostic.pm2Installed && diagnostic.processFound && diagnostic.status === "online" && diagnostic.pid === currentPid && diagnostic.ready && diagnostic.scopeMatches;
22044
21888
  }
22045
- function canRestartCurrentGateway() {
21889
+ async function canRestartCurrentGateway(fresh = false) {
22046
21890
  if (runtimeConfig === null) return false;
22047
21891
  try {
22048
- return isSafeDashboardRestartTarget(getGatewayDiagnostic(), process.pid);
21892
+ return isSafeDashboardRestartTarget(
21893
+ await getDashboardGatewayDiagnostic({ fresh }),
21894
+ process.pid
21895
+ );
22049
21896
  } catch {
22050
21897
  return false;
22051
21898
  }
@@ -22064,7 +21911,7 @@ function listChildDirectories(path) {
22064
21911
  if (entry.isDirectory()) return [{ name: entry.name, path: entryPath, hidden: entry.name.startsWith(".") }];
22065
21912
  if (!entry.isSymbolicLink()) return [];
22066
21913
  try {
22067
- return statSync4(entryPath).isDirectory() ? [{ name: entry.name, path: entryPath, hidden: entry.name.startsWith(".") }] : [];
21914
+ return statSync3(entryPath).isDirectory() ? [{ name: entry.name, path: entryPath, hidden: entry.name.startsWith(".") }] : [];
22068
21915
  } catch {
22069
21916
  return [];
22070
21917
  }
@@ -22117,6 +21964,7 @@ function parseTaskPayload(value) {
22117
21964
  cwd: requiredString("cwd"),
22118
21965
  agent: requiredString("agent"),
22119
21966
  model: optionalString("model", "default"),
21967
+ variant: Object.hasOwn(input, "variant") ? optionalString("variant") : void 0,
22120
21968
  prompt: requiredString("prompt"),
22121
21969
  category: optionalString("category", "general"),
22122
21970
  batchId: optionalString("batchId"),
@@ -22132,6 +21980,7 @@ function editableTaskPayload(input) {
22132
21980
  name: input.name,
22133
21981
  agent: input.agent,
22134
21982
  model: input.model ?? "default",
21983
+ ...input.variant === void 0 ? {} : { variant: input.variant },
22135
21984
  prompt: input.prompt,
22136
21985
  category: input.category ?? "general",
22137
21986
  batchId: input.batchId ?? null,
@@ -22187,6 +22036,7 @@ function parseTemplatePayload(value) {
22187
22036
  name: requiredString("name"),
22188
22037
  agent: requiredString("agent"),
22189
22038
  model: optionalString("model", "default"),
22039
+ variant: Object.hasOwn(input, "variant") ? optionalString("variant") : void 0,
22190
22040
  prompt: requiredString("prompt"),
22191
22041
  cwd: requiredString("cwd"),
22192
22042
  category: optionalString("category", "general"),
@@ -22299,7 +22149,7 @@ function readCurrentConfig() {
22299
22149
  const configPath = getConfigPath();
22300
22150
  if (!existsSync7(configPath)) return {};
22301
22151
  try {
22302
- return JSON.parse(readFileSync4(configPath, "utf-8"));
22152
+ return JSON.parse(readFileSync3(configPath, "utf-8"));
22303
22153
  } catch {
22304
22154
  return {};
22305
22155
  }
@@ -22307,9 +22157,9 @@ function readCurrentConfig() {
22307
22157
  function writeConfig(cfg) {
22308
22158
  const configPath = getConfigPath();
22309
22159
  const dir = dirname7(configPath);
22310
- if (!existsSync7(dir)) mkdirSync4(dir, { recursive: true });
22160
+ if (!existsSync7(dir)) mkdirSync3(dir, { recursive: true });
22311
22161
  const tempPath = `${configPath}.${process.pid}.tmp`;
22312
- writeFileSync3(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
22162
+ writeFileSync2(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
22313
22163
  renameSync2(tempPath, configPath);
22314
22164
  }
22315
22165
  function statCard(value, label, tone, cardIcon, delay = "") {
@@ -22361,7 +22211,7 @@ function pagination(locale, basePath, page, pages, total, suffix = "") {
22361
22211
  const next = page < pages ? `<a class="btn" href="${basePath}?page=${page + 1}${suffix}">${t(locale, "pagination.next")}${icon("chevronRight")}</a>` : "";
22362
22212
  return `<div class="pagination">${previous}<span class="summary">${t(locale, "pagination.summary", { page, pages, total })}</span>${next}</div>`;
22363
22213
  }
22364
- var app, LEGACY_PROJECT_FILTER, TASK_STATUSES, SESSION_ID_PATTERN, runtimeConfig, restartScheduled, dashboardApp, web_default;
22214
+ var app, LEGACY_PROJECT_FILTER, TASK_STATUSES, SESSION_ID_PATTERN, LOOPBACK_HOST_PATTERN, runtimeConfig, restartScheduled, dashboardApp, web_default;
22365
22215
  var init_web = __esm({
22366
22216
  "src/web/index.tsx"() {
22367
22217
  "use strict";
@@ -22378,7 +22228,7 @@ var init_web = __esm({
22378
22228
  init_config();
22379
22229
  init_health();
22380
22230
  init_job_templates();
22381
- init_pm2();
22231
+ init_gateway_diagnostic();
22382
22232
  init_ui();
22383
22233
  app = new Hono2();
22384
22234
  LEGACY_PROJECT_FILTER = "__supertask_legacy__";
@@ -22391,9 +22241,18 @@ var init_web = __esm({
22391
22241
  "cancelled"
22392
22242
  ]);
22393
22243
  SESSION_ID_PATTERN = /^ses_[A-Za-z0-9_]+$/;
22244
+ LOOPBACK_HOST_PATTERN = /^(?:localhost|127\.0\.0\.1|\[::1\])(?::([1-9]\d{0,4}))?$/i;
22394
22245
  runtimeConfig = null;
22395
22246
  restartScheduled = false;
22396
22247
  app.use("*", async (c, next) => {
22248
+ const requestHostname = new URL(c.req.url).hostname;
22249
+ const hostHeader = c.req.header("Host");
22250
+ const loopbackHosts = ["localhost", "127.0.0.1", "[::1]"];
22251
+ const hostMatch = hostHeader?.match(LOOPBACK_HOST_PATTERN) ?? null;
22252
+ const hostPort = hostMatch?.[1] === void 0 ? null : Number(hostMatch[1]);
22253
+ if (!loopbackHosts.includes(requestHostname) || hostHeader !== void 0 && (!hostMatch || hostPort !== null && hostPort > 65535)) {
22254
+ return c.json({ error: "invalid dashboard host" }, 421);
22255
+ }
22397
22256
  await next();
22398
22257
  c.header("X-Content-Type-Options", "nosniff");
22399
22258
  c.header("X-Frame-Options", "DENY");
@@ -22425,13 +22284,13 @@ var init_web = __esm({
22425
22284
  return c.json(health, health.status === "ok" ? 200 : 503);
22426
22285
  });
22427
22286
  app.get("/api/filesystem/directories", (c) => {
22428
- const requestedPath = c.req.query("path")?.trim() || homedir4();
22287
+ const requestedPath = c.req.query("path")?.trim() || homedir3();
22429
22288
  try {
22430
22289
  validateTaskWorkingDirectory(requestedPath);
22431
22290
  return c.json({
22432
22291
  path: requestedPath,
22433
22292
  parent: dirname7(requestedPath),
22434
- home: homedir4(),
22293
+ home: homedir3(),
22435
22294
  directories: listChildDirectories(requestedPath)
22436
22295
  });
22437
22296
  } catch (error) {
@@ -22520,7 +22379,7 @@ var init_web = __esm({
22520
22379
  const latestRun = latestRuns.get(task.id);
22521
22380
  const executionActive = latestRun?.status === "running";
22522
22381
  const batchId = task.batchId?.trim() || null;
22523
- const searchable = esc(`${task.name} ${task.agent} ${task.prompt} ${task.cwd ?? ""} ${task.batchId ?? ""} ${task.category ?? ""}`);
22382
+ const searchable = esc(`${task.name} ${task.agent} ${task.model ?? ""} ${task.variant ?? ""} ${task.prompt} ${task.cwd ?? ""} ${task.batchId ?? ""} ${task.category ?? ""}`);
22524
22383
  return `<tr data-task-row data-search="${searchable}">
22525
22384
  <td class="faint" data-label="${t(locale, "table.id")}">#${task.id}</td>
22526
22385
  <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>
@@ -22598,11 +22457,12 @@ var init_web = __esm({
22598
22457
  <label class="form-field"><span>${t(locale, "template.cwd")}</span><div class="field-action"><input id="task-cwd" required autocomplete="off" list="task-cwd-options" placeholder="/path/to/project" oninput="updateTaskProjectStatus();scheduleCatalogLoad('task')"><button id="task-cwd-picker" type="button" class="btn" onclick="openDirectoryPicker('task-cwd')">${icon("folder")}${t(locale, "action.chooseFolder")}</button></div><small>${t(locale, "template.cwdHint")}</small></label>
22599
22458
  <datalist id="task-cwd-options">${projects.map((project) => `<option value="${esc(project.cwd)}"></option>`).join("")}</datalist>
22600
22459
  <label class="form-field"><span>${t(locale, "template.agent")}</span><select id="task-agent" required><option value="">${t(locale, "catalog.chooseProject")}</option></select><small>${t(locale, "catalog.agentHint")}</small></label>
22601
- <label class="form-field"><span>${t(locale, "template.model")}</span><div class="model-selector"><select id="task-model-provider" aria-label="${t(locale, "catalog.provider")}" onchange="populateModelOptions('task')"><option value="">${t(locale, "catalog.defaultProvider")}</option></select><select id="task-model" required aria-label="${t(locale, "catalog.model")}" disabled><option value="default">${t(locale, "catalog.defaultModel")}</option></select></div><small>${t(locale, "catalog.modelHint")}</small></label>
22460
+ <label class="form-field"><span>${t(locale, "template.model")}</span><div class="model-selector"><select id="task-model-provider" aria-label="${t(locale, "catalog.provider")}" onchange="handleProviderChange('task')"><option value="">${t(locale, "catalog.defaultProvider")}</option></select><select id="task-model" required aria-label="${t(locale, "catalog.model")}" onchange="handleModelChange('task')" disabled><option value="default">${t(locale, "catalog.defaultModel")}</option></select></div><small>${t(locale, "catalog.modelHint")}</small></label>
22461
+ <label class="form-field"><span>${t(locale, "template.variant")}</span><select id="task-variant" onchange="handleVariantChange('task')" disabled><option value="">${t(locale, "catalog.defaultVariant")}</option></select><small>${t(locale, "catalog.variantHint")}</small></label>
22602
22462
  <label class="form-field form-field-wide"><span>${t(locale, "template.prompt")}</span><textarea id="task-prompt" rows="6" required></textarea></label>
22603
22463
  </div>
22604
22464
  <p id="task-project-status" class="form-note"></p>
22605
- <p id="task-catalog-status" class="form-note catalog-status"></p>
22465
+ <p id="task-catalog-status" class="form-note catalog-status" role="status" aria-live="polite"></p>
22606
22466
  <details class="advanced-fields">
22607
22467
  <summary>${t(locale, "template.advanced")}</summary>
22608
22468
  <div class="template-form-grid">
@@ -22644,7 +22504,7 @@ var init_web = __esm({
22644
22504
  return `<tr>
22645
22505
  <td class="faint" data-label="${t(locale, "table.id")}">#${template.id}</td>
22646
22506
  <td data-primary data-label="${t(locale, "table.name")}"><div class="task-name">${esc(template.name)}</div><div class="task-prompt" title="${esc(template.prompt)}">${esc(template.prompt.substring(0, 140))}</div>
22647
- <div class="actions" style="margin-top:5px"><span class="tag">${esc(template.agent)}</span>${template.model && template.model !== "default" ? `<span class="tag">${esc(template.model)}</span>` : ""}</div></td>
22507
+ <div class="actions" style="margin-top:5px"><span class="tag">${esc(template.agent)}</span>${template.model && template.model !== "default" ? `<span class="tag">${esc(template.model)}</span>` : ""}${template.variant ? `<span class="tag">${esc(template.variant)}</span>` : ""}</div></td>
22648
22508
  <td data-label="${t(locale, "table.type")}"><span class="tag t-${scheduleType}">${typeLabel}</span></td>
22649
22509
  <td data-label="${t(locale, "table.rule")}" class="m small">${esc(rule)}</td>
22650
22510
  <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>
@@ -22674,14 +22534,15 @@ var init_web = __esm({
22674
22534
  <label class="form-field"><span>${t(locale, "template.name")}</span><input id="template-name" required maxlength="200" autocomplete="off"></label>
22675
22535
  <label class="form-field"><span>${t(locale, "template.cwd")}</span><div class="field-action"><input id="template-cwd" required autocomplete="off" placeholder="/path/to/project" oninput="scheduleCatalogLoad('template')"><button type="button" class="btn" onclick="openDirectoryPicker('template-cwd')">${icon("folder")}${t(locale, "action.chooseFolder")}</button></div><small>${t(locale, "template.cwdHint")}</small></label>
22676
22536
  <label class="form-field"><span>${t(locale, "template.agent")}</span><select id="template-agent" required><option value="">${t(locale, "catalog.chooseProject")}</option></select><small>${t(locale, "catalog.agentHint")}</small></label>
22677
- <label class="form-field"><span>${t(locale, "template.model")}</span><div class="model-selector"><select id="template-model-provider" aria-label="${t(locale, "catalog.provider")}" onchange="populateModelOptions('template')"><option value="">${t(locale, "catalog.defaultProvider")}</option></select><select id="template-model" required aria-label="${t(locale, "catalog.model")}" disabled><option value="default">${t(locale, "catalog.defaultModel")}</option></select></div><small>${t(locale, "catalog.modelHint")}</small></label>
22537
+ <label class="form-field"><span>${t(locale, "template.model")}</span><div class="model-selector"><select id="template-model-provider" aria-label="${t(locale, "catalog.provider")}" onchange="handleProviderChange('template')"><option value="">${t(locale, "catalog.defaultProvider")}</option></select><select id="template-model" required aria-label="${t(locale, "catalog.model")}" onchange="handleModelChange('template')" disabled><option value="default">${t(locale, "catalog.defaultModel")}</option></select></div><small>${t(locale, "catalog.modelHint")}</small></label>
22538
+ <label class="form-field"><span>${t(locale, "template.variant")}</span><select id="template-variant" onchange="handleVariantChange('template')" disabled><option value="">${t(locale, "catalog.defaultVariant")}</option></select><small>${t(locale, "catalog.variantHint")}</small></label>
22678
22539
  <label class="form-field form-field-wide"><span>${t(locale, "template.prompt")}</span><textarea id="template-prompt" rows="6" required></textarea></label>
22679
22540
  <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>
22680
22541
  <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>
22681
22542
  <label id="template-interval-field" class="form-field"><span>${t(locale, "template.interval")}</span>${durationControl(locale, "template-interval", 36e5, "interval")}<small>${t(locale, "template.intervalHint")}</small></label>
22682
22543
  <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>
22683
22544
  </div>
22684
- <p id="template-catalog-status" class="form-note catalog-status"></p>
22545
+ <p id="template-catalog-status" class="form-note catalog-status" role="status" aria-live="polite"></p>
22685
22546
  <details class="advanced-fields">
22686
22547
  <summary>${t(locale, "template.advanced")}</summary>
22687
22548
  <div class="template-form-grid">
@@ -22715,6 +22576,7 @@ var init_web = __esm({
22715
22576
  taskId: taskRuns4.taskId,
22716
22577
  sessionId: taskRuns4.sessionId,
22717
22578
  model: taskRuns4.model,
22579
+ variant: taskRuns4.variant,
22718
22580
  status: taskRuns4.status,
22719
22581
  startedAt: taskRuns4.startedAt,
22720
22582
  finishedAt: taskRuns4.finishedAt,
@@ -22735,7 +22597,7 @@ var init_web = __esm({
22735
22597
  const log = run.log ? renderRunLog(run.id, run.taskName, run.log, locale, run.status !== "done") : "";
22736
22598
  return `<tr class="run-summary-row">
22737
22599
  <td class="faint" data-label="${t(locale, "table.run")}">#${run.id}</td>
22738
- <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>
22600
+ <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 || run.variant ? `<div class="actions" style="margin-top:4px">${run.model ? `<span class="tag">${esc(run.model)}</span>` : ""}${run.variant ? `<span class="tag">${esc(run.variant)}</span>` : ""}</div>` : ""}</td>
22739
22601
  <td data-label="${t(locale, "table.agent")}"><span class="tag">${esc(run.taskAgent)}</span></td>
22740
22602
  <td data-label="${t(locale, "table.session")}" class="m small">${esc(maskSessionId(run.sessionId))}</td>
22741
22603
  <td data-label="${t(locale, "table.status")}"><span class="badge b-${status}">${runStatusText(locale, run.status ?? "unknown")}</span></td>
@@ -22762,7 +22624,7 @@ var init_web = __esm({
22762
22624
  const config = loadConfig();
22763
22625
  const activeConfig = runtimeConfig ?? config;
22764
22626
  const restartRequired = runtimeConfig !== null && !configsEqual(config, runtimeConfig);
22765
- const managedRestart = canRestartCurrentGateway();
22627
+ const managedRestart = await canRestartCurrentGateway();
22766
22628
  const configState = resolveDashboardConfigState(
22767
22629
  runtimeConfig !== null,
22768
22630
  restartRequired,
@@ -22785,7 +22647,7 @@ var init_web = __esm({
22785
22647
  const runRows = runningRuns.map((run) => {
22786
22648
  const session = maskSessionId(run.sessionId);
22787
22649
  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>
22788
- <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>
22650
+ <td data-label="${t(locale, "table.model")}" class="small">${esc(run.model) || "\u2014"}${run.variant ? ` \xB7 ${esc(run.variant)}` : ""}</td><td data-label="${t(locale, "table.startedAt")}" class="small">${formatDateTime(run.startedAt, locale)}</td>
22789
22651
  <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>
22790
22652
  <td data-label="${t(locale, "table.duration")}" class="small">${formatDuration(run.startedAt, null)}</td></tr>`;
22791
22653
  }).join("");
@@ -22882,9 +22744,9 @@ var init_web = __esm({
22882
22744
  if (!isValidSessionId(run.sessionId)) return c.json({ error: "session unavailable" }, 409);
22883
22745
  return c.json({ command: sessionCommand(run.sessionId) });
22884
22746
  });
22885
- app.get("/api/gateway/status", (c) => {
22747
+ app.get("/api/gateway/status", async (c) => {
22886
22748
  const savedConfig = loadConfig();
22887
- const managed = canRestartCurrentGateway();
22749
+ const managed = await canRestartCurrentGateway();
22888
22750
  return c.json({
22889
22751
  pid: process.pid,
22890
22752
  managed,
@@ -23009,7 +22871,7 @@ var init_web = __esm({
23009
22871
  return c.json({
23010
22872
  success: true,
23011
22873
  restartRequired: runtimeConfig === null || !configsEqual(savedConfig, runtimeConfig),
23012
- managed: canRestartCurrentGateway()
22874
+ managed: await canRestartCurrentGateway()
23013
22875
  });
23014
22876
  } catch (error) {
23015
22877
  return c.json({
@@ -23025,9 +22887,10 @@ var init_web = __esm({
23025
22887
  return c.json({ error: "confirmation must be RESTART" }, 400);
23026
22888
  }
23027
22889
  if (restartScheduled) return c.json({ error: "restart already scheduled" }, 409);
23028
- if (!canRestartCurrentGateway()) {
22890
+ if (!await canRestartCurrentGateway(true)) {
23029
22891
  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);
23030
22892
  }
22893
+ if (restartScheduled) return c.json({ error: "restart already scheduled" }, 409);
23031
22894
  restartScheduled = true;
23032
22895
  const previousPid = process.pid;
23033
22896
  setTimeout(() => {
@@ -23091,7 +22954,7 @@ function runCommandContext(executable, args, cwd) {
23091
22954
  function assertWorkerProcessIsolationSupported(platform = process.platform) {
23092
22955
  if (platform === "win32") {
23093
22956
  throw new Error(
23094
- "Windows Worker \u5DF2\u5B89\u5168\u7981\u7528\uFF1A\u5F53\u524D\u8FD0\u884C\u65F6\u65E0\u6CD5\u7528 Job Object \u8BC1\u660E\u6574\u4E2A OpenCode \u8FDB\u7A0B\u6811\u5DF2\u9000\u51FA"
22957
+ "Windows Worker \u5DF2\u5B89\u5168\u7981\u7528\uFF1A\u5F53\u524D\u8FD0\u884C\u65F6\u65E0\u6CD5\u7528 Job Object \u63D0\u4F9B\u7B49\u4EF7\u7684\u53D7\u7BA1\u8FDB\u7A0B\u9694\u79BB\u4E0E\u6392\u7A7A\u8BC1\u660E"
23095
22958
  );
23096
22959
  }
23097
22960
  }
@@ -23102,17 +22965,24 @@ var WorkerEngine = class {
23102
22965
  pollTimer = null;
23103
22966
  heartbeatTimer = null;
23104
22967
  pollCyclePromise = null;
22968
+ shutdownDeadlineMs = null;
22969
+ settlementRetryWakeups = /* @__PURE__ */ new Set();
23105
22970
  cfg;
23106
22971
  opencodeBin;
23107
22972
  maxOutputChars;
22973
+ settlementRetryDelaysMs;
22974
+ settlementRetryIntervalMs;
23108
22975
  constructor(cfg, options = {}) {
23109
22976
  this.cfg = cfg.worker;
23110
22977
  this.opencodeBin = options.opencodeBin ?? process.env.SUPERTASK_OPENCODE_BIN ?? "opencode";
23111
22978
  this.maxOutputChars = options.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
22979
+ this.settlementRetryDelaysMs = options.settlementRetryDelaysMs ?? [250, 1e3, 4e3];
22980
+ this.settlementRetryIntervalMs = options.settlementRetryIntervalMs ?? 5e3;
23112
22981
  }
23113
22982
  start() {
23114
22983
  assertWorkerProcessIsolationSupported();
23115
22984
  this.stopped = false;
22985
+ this.shutdownDeadlineMs = null;
23116
22986
  markGatewayActivity("worker");
23117
22987
  this.poll();
23118
22988
  this.heartbeatTimer = setInterval(() => {
@@ -23121,6 +22991,8 @@ var WorkerEngine = class {
23121
22991
  }
23122
22992
  async stop(gracePeriodMs = 0) {
23123
22993
  this.stopped = true;
22994
+ this.shutdownDeadlineMs = Date.now() + Math.max(0, gracePeriodMs);
22995
+ for (const wake of [...this.settlementRetryWakeups]) wake();
23124
22996
  if (this.pollTimer) {
23125
22997
  clearTimeout(this.pollTimer);
23126
22998
  this.pollTimer = null;
@@ -23190,27 +23062,25 @@ var WorkerEngine = class {
23190
23062
  if (databaseRunningCount >= this.cfg.maxConcurrency) break;
23191
23063
  let task;
23192
23064
  try {
23193
- task = await TaskService.next({ excludedBatchIds: [...this.activeBatchIds] });
23065
+ task = await TaskService.claimNext({ excludedBatchIds: [...this.activeBatchIds] });
23194
23066
  } catch (err) {
23195
23067
  this.logError("task claim failed", err);
23196
23068
  throw err;
23197
23069
  }
23198
23070
  if (!task) break;
23199
- if (this.stopped) break;
23200
- if (!await TaskService.start(task.id)) continue;
23201
- const batchId = normalizeTaskBatchId(task.batchId);
23202
- if (batchId) this.activeBatchIds.add(batchId);
23203
23071
  if (this.stopped) {
23204
23072
  await TaskService.resetRunningToPending([task.id]);
23205
- this.releaseBatch(task);
23206
23073
  break;
23207
23074
  }
23075
+ const batchId = normalizeTaskBatchId(task.batchId);
23076
+ if (batchId) this.activeBatchIds.add(batchId);
23208
23077
  let runId = null;
23209
23078
  try {
23210
23079
  const launchIdentity = `gateway-${process.pid}:launch:${randomUUID()}`;
23211
23080
  const run = await TaskRunService.create({
23212
23081
  taskId: task.id,
23213
23082
  model: this.resolveModel(task.model),
23083
+ variant: this.resolveVariant(task.variant),
23214
23084
  status: "running",
23215
23085
  workerPid: process.pid,
23216
23086
  lockedAt: Date.now(),
@@ -23273,8 +23143,10 @@ var WorkerEngine = class {
23273
23143
  }
23274
23144
  async spawnTask(task, runId, launchIdentity) {
23275
23145
  const model = this.resolveModel(task.model);
23146
+ const variant = this.resolveVariant(task.variant);
23276
23147
  const args = ["run", "--agent", task.agent, "--format", "json"];
23277
23148
  if (model) args.push("-m", model);
23149
+ if (variant) args.push("--variant", variant);
23278
23150
  args.push(task.prompt);
23279
23151
  const cwd = task.cwd || process.cwd();
23280
23152
  const child = spawn(process.execPath, [
@@ -23334,8 +23206,8 @@ var WorkerEngine = class {
23334
23206
  }
23335
23207
  });
23336
23208
  let spawnError = null;
23337
- const spawned = new Promise((resolve4, reject) => {
23338
- child.once("spawn", resolve4);
23209
+ const spawned = new Promise((resolve3, reject) => {
23210
+ child.once("spawn", resolve3);
23339
23211
  child.once("error", (error) => {
23340
23212
  spawnError = error;
23341
23213
  reject(error);
@@ -23383,10 +23255,10 @@ var WorkerEngine = class {
23383
23255
  return;
23384
23256
  }
23385
23257
  try {
23386
- await new Promise((resolve4, reject) => {
23258
+ await new Promise((resolve3, reject) => {
23387
23259
  child.stdin.end("START\n", (error) => {
23388
23260
  if (error) reject(error);
23389
- else resolve4();
23261
+ else resolve3();
23390
23262
  });
23391
23263
  });
23392
23264
  } catch (error) {
@@ -23412,7 +23284,7 @@ var WorkerEngine = class {
23412
23284
  if (!entry.guardianDrained) {
23413
23285
  const pid = entry.child.pid;
23414
23286
  const presence = pid == null ? spawnError == null ? "unknown" : "not-running" : inspectSpawnedProcessTreePresence(pid);
23415
- const message = "guardian \u672A\u63D0\u4F9B\u8FDB\u7A0B\u6811\u6392\u7A7A\u8BC1\u660E";
23287
+ const message = "guardian \u672A\u63D0\u4F9B\u53D7\u7BA1\u8FDB\u7A0B\u7EC4\u6392\u7A7A\u8BC1\u660E";
23416
23288
  if (presence !== "not-running") {
23417
23289
  if (entry.timeoutTimer) {
23418
23290
  clearTimeout(entry.timeoutTimer);
@@ -23433,7 +23305,7 @@ var WorkerEngine = class {
23433
23305
  );
23434
23306
  return;
23435
23307
  }
23436
- const failure = code === 0 ? void 0 : `${spawnError ? "\u65E0\u6CD5\u542F\u52A8 opencode" : "opencode \u9000\u51FA\u7801"} ${spawnError?.message ?? code ?? "null"}${signal ? `\uFF0C\u4FE1\u53F7 ${signal}` : ""}\uFF08agent=${entry.task.agent}\uFF0Cmodel=${this.resolveModel(entry.task.model) ?? "Agent/\u9ED8\u8BA4\u914D\u7F6E"}\uFF0Ccwd=${entry.task.cwd ?? process.cwd()}\uFF09`;
23308
+ const failure = code === 0 ? void 0 : `${spawnError ? "\u65E0\u6CD5\u542F\u52A8 opencode" : "opencode \u9000\u51FA\u7801"} ${spawnError?.message ?? code ?? "null"}${signal ? `\uFF0C\u4FE1\u53F7 ${signal}` : ""}\uFF08agent=${entry.task.agent}\uFF0Cmodel=${this.resolveModel(entry.task.model) ?? "Agent/\u9ED8\u8BA4\u914D\u7F6E"}\uFF0Cvariant=${this.resolveVariant(entry.task.variant) ?? "Agent/\u6A21\u578B\u9ED8\u8BA4\u914D\u7F6E"}\uFF0Ccwd=${entry.task.cwd ?? process.cwd()}\uFF09`;
23437
23309
  this.runDetached(
23438
23310
  this.settleEntry(entry, code, failure),
23439
23311
  "task settlement failed",
@@ -23448,17 +23320,48 @@ var WorkerEngine = class {
23448
23320
  clearTimeout(entry.timeoutTimer);
23449
23321
  entry.timeoutTimer = null;
23450
23322
  }
23451
- const settlement = this.commitEntry(entry, code, failure).then(() => true).catch((error) => {
23452
- markGatewayFailure("worker", error);
23453
- this.logError("task settlement failed", error, entry.task.id);
23454
- return false;
23455
- }).finally(() => {
23323
+ const settlement = this.commitEntryWithRetry(entry, code, failure).finally(() => {
23456
23324
  this.runningTasks.delete(entry.task.id);
23457
23325
  this.releaseBatch(entry.task);
23458
23326
  });
23459
23327
  entry.settlementPromise = settlement;
23460
23328
  return settlement;
23461
23329
  }
23330
+ async commitEntryWithRetry(entry, code, failure) {
23331
+ for (let attempt = 0; ; attempt += 1) {
23332
+ try {
23333
+ await this.commitEntry(entry, code, failure);
23334
+ return true;
23335
+ } catch (error) {
23336
+ markGatewayFailure("worker", error);
23337
+ const shortRetryDelayMs = this.settlementRetryDelaysMs[attempt];
23338
+ let retryDelayMs = shortRetryDelayMs ?? this.settlementRetryIntervalMs;
23339
+ if (this.stopped) {
23340
+ const remainingMs = Math.max(0, (this.shutdownDeadlineMs ?? 0) - Date.now());
23341
+ if (remainingMs === 0) return false;
23342
+ retryDelayMs = Math.min(retryDelayMs, remainingMs);
23343
+ }
23344
+ this.logError(
23345
+ `task settlement failed; retrying in ${retryDelayMs}ms`,
23346
+ error,
23347
+ entry.task.id
23348
+ );
23349
+ await this.waitForSettlementRetry(retryDelayMs);
23350
+ }
23351
+ }
23352
+ }
23353
+ waitForSettlementRetry(delayMs) {
23354
+ return new Promise((resolve3) => {
23355
+ let timer;
23356
+ const finish = () => {
23357
+ clearTimeout(timer);
23358
+ this.settlementRetryWakeups.delete(finish);
23359
+ resolve3();
23360
+ };
23361
+ timer = setTimeout(finish, delayMs);
23362
+ this.settlementRetryWakeups.add(finish);
23363
+ });
23364
+ }
23462
23365
  async commitEntry(entry, code, failure) {
23463
23366
  const termination = entry.termination;
23464
23367
  if (termination?.kind === "shutdown") return;
@@ -23622,6 +23525,9 @@ ${output}` : ""}`;
23622
23525
  if (!taskModel || taskModel === "default") return null;
23623
23526
  return taskModel;
23624
23527
  }
23528
+ resolveVariant(taskVariant) {
23529
+ return taskVariant?.trim() || null;
23530
+ }
23625
23531
  runDetached(operation, message, taskId) {
23626
23532
  operation.catch((error) => {
23627
23533
  markGatewayFailure("worker", error);
@@ -24022,7 +23928,29 @@ init_db2();
24022
23928
  init_task_service();
24023
23929
  init_health();
24024
23930
  init_process_control();
24025
- init_package_version();
23931
+
23932
+ // src/core/package-version.ts
23933
+ import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
23934
+ import { dirname as dirname4, join as join4 } from "path";
23935
+ import { fileURLToPath as fileURLToPath4 } from "url";
23936
+ function getPackageVersion() {
23937
+ let directory = dirname4(fileURLToPath4(import.meta.url));
23938
+ for (let depth = 0; depth < 5; depth += 1) {
23939
+ const packagePath = join4(directory, "package.json");
23940
+ if (existsSync4(packagePath)) {
23941
+ try {
23942
+ const pkg = JSON.parse(readFileSync2(packagePath, "utf8"));
23943
+ if (typeof pkg.version === "string") return pkg.version;
23944
+ } catch {
23945
+ return "0.0.0";
23946
+ }
23947
+ }
23948
+ directory = dirname4(directory);
23949
+ }
23950
+ return "0.0.0";
23951
+ }
23952
+
23953
+ // src/gateway/index.ts
24026
23954
  var STALE_THRESHOLD_MS = 3e4;
24027
23955
  async function runGatewayShutdownStep(failures, step, operation) {
24028
23956
  try {