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.
@@ -3,6 +3,8 @@ import { G as GatewayConfig } from '../config-CCUuD6Az.js';
3
3
  interface WorkerEngineOptions {
4
4
  opencodeBin?: string;
5
5
  maxOutputChars?: number;
6
+ settlementRetryDelaysMs?: number[];
7
+ settlementRetryIntervalMs?: number;
6
8
  }
7
9
  interface InterruptedTaskRun {
8
10
  taskId: number;
@@ -17,9 +19,13 @@ declare class WorkerEngine {
17
19
  private pollTimer;
18
20
  private heartbeatTimer;
19
21
  private pollCyclePromise;
22
+ private shutdownDeadlineMs;
23
+ private settlementRetryWakeups;
20
24
  private cfg;
21
25
  private opencodeBin;
22
26
  private maxOutputChars;
27
+ private settlementRetryDelaysMs;
28
+ private settlementRetryIntervalMs;
23
29
  constructor(cfg: GatewayConfig, options?: WorkerEngineOptions);
24
30
  start(): void;
25
31
  stop(gracePeriodMs?: number): Promise<InterruptedTaskRun[]>;
@@ -32,6 +38,8 @@ declare class WorkerEngine {
32
38
  private spawnTask;
33
39
  private handleChildClose;
34
40
  private settleEntry;
41
+ private commitEntryWithRetry;
42
+ private waitForSettlementRetry;
35
43
  private commitEntry;
36
44
  private reconcileCancelledTasks;
37
45
  private reconcileQuarantinedTasks;
@@ -45,6 +53,7 @@ declare class WorkerEngine {
45
53
  private releaseBatch;
46
54
  private outputWithCommand;
47
55
  private resolveModel;
56
+ private resolveVariant;
48
57
  private runDetached;
49
58
  private logError;
50
59
  }
@@ -5157,6 +5157,7 @@ var tasks = sqliteTable("tasks", {
5157
5157
  name: text("name").notNull(),
5158
5158
  agent: text("agent").notNull(),
5159
5159
  model: text("model").default("default"),
5160
+ variant: text("variant"),
5160
5161
  prompt: text("prompt").notNull(),
5161
5162
  cwd: text("cwd"),
5162
5163
  // 分类与优先级
@@ -5201,6 +5202,7 @@ var taskRuns = sqliteTable("task_runs", {
5201
5202
  taskId: integer("task_id").notNull().references(() => tasks.id, { onDelete: "cascade" }),
5202
5203
  sessionId: text("session_id"),
5203
5204
  model: text("model"),
5205
+ variant: text("variant"),
5204
5206
  status: text("status").default("running"),
5205
5207
  startedAt: integer("started_at", { mode: "timestamp" }).$defaultFn(() => /* @__PURE__ */ new Date()),
5206
5208
  finishedAt: integer("finished_at", { mode: "timestamp" }),
@@ -5221,6 +5223,7 @@ var taskTemplates = sqliteTable("task_templates", {
5221
5223
  name: text("name").notNull(),
5222
5224
  agent: text("agent").notNull(),
5223
5225
  model: text("model").default("default"),
5226
+ variant: text("variant"),
5224
5227
  prompt: text("prompt").notNull(),
5225
5228
  cwd: text("cwd"),
5226
5229
  category: text("category").default("general"),
@@ -5395,6 +5398,22 @@ function normalizeTaskBatchId(batchId) {
5395
5398
  return batchId.trim() || null;
5396
5399
  }
5397
5400
 
5401
+ // src/core/model-variant.ts
5402
+ var MAX_MODEL_VARIANT_LENGTH = 128;
5403
+ var CONTROL_CHARACTER_PATTERN = /[\u0000-\u001F\u007F]/;
5404
+ function normalizeModelVariant(value) {
5405
+ if (value === void 0 || value === null) return value;
5406
+ const normalized = value.trim();
5407
+ if (!normalized) return null;
5408
+ if (normalized.length > MAX_MODEL_VARIANT_LENGTH) {
5409
+ throw new Error(`variant \u957F\u5EA6\u4E0D\u80FD\u8D85\u8FC7 ${MAX_MODEL_VARIANT_LENGTH} \u4E2A\u5B57\u7B26`);
5410
+ }
5411
+ if (CONTROL_CHARACTER_PATTERN.test(normalized)) {
5412
+ throw new Error("variant \u4E0D\u80FD\u5305\u542B\u63A7\u5236\u5B57\u7B26");
5413
+ }
5414
+ return normalized;
5415
+ }
5416
+
5398
5417
  // src/core/services/task.service.ts
5399
5418
  var { tasks: tasks2, taskRuns: taskRuns2 } = schema_exports;
5400
5419
  var cleanupInvocation = 0;
@@ -5460,7 +5479,8 @@ var TaskService = class {
5460
5479
  static async add(data) {
5461
5480
  const normalizedData = {
5462
5481
  ...data,
5463
- batchId: normalizeTaskBatchId(data.batchId)
5482
+ batchId: normalizeTaskBatchId(data.batchId),
5483
+ variant: normalizeModelVariant(data.variant)
5464
5484
  };
5465
5485
  this.validateNewTask(normalizedData);
5466
5486
  return db.transaction((tx) => {
@@ -5488,7 +5508,11 @@ var TaskService = class {
5488
5508
  }
5489
5509
  static async update(id, data, scope = {}) {
5490
5510
  if (Object.keys(data).length === 0) throw new Error("\u81F3\u5C11\u63D0\u4F9B\u4E00\u4E2A\u8981\u4FEE\u6539\u7684\u5B57\u6BB5");
5491
- const normalizedData = data.batchId === void 0 ? data : { ...data, batchId: normalizeTaskBatchId(data.batchId) ?? null };
5511
+ const normalizedData = {
5512
+ ...data,
5513
+ ...data.batchId === void 0 ? {} : { batchId: normalizeTaskBatchId(data.batchId) ?? null },
5514
+ ...data.variant === void 0 ? {} : { variant: normalizeModelVariant(data.variant) ?? null }
5515
+ };
5492
5516
  return db.transaction((tx) => {
5493
5517
  const task = tx.select().from(tasks2).where(and(
5494
5518
  eq(tasks2.id, id),
@@ -5500,6 +5524,7 @@ var TaskService = class {
5500
5524
  name: normalizedData.name ?? task.name,
5501
5525
  agent: normalizedData.agent ?? task.agent,
5502
5526
  model: normalizedData.model ?? task.model,
5527
+ variant: normalizedData.variant === void 0 ? task.variant : normalizedData.variant,
5503
5528
  prompt: normalizedData.prompt ?? task.prompt,
5504
5529
  cwd: task.cwd,
5505
5530
  category: normalizedData.category ?? task.category,
@@ -5513,13 +5538,23 @@ var TaskService = class {
5513
5538
  });
5514
5539
  const maxRetries = normalizedData.maxRetries ?? task.maxRetries ?? 3;
5515
5540
  const exhausted = task.status === "failed" && (task.retryCount ?? 0) > maxRetries;
5516
- return tx.update(tasks2).set({
5541
+ const updated = tx.update(tasks2).set({
5517
5542
  ...normalizedData,
5518
5543
  ...exhausted ? {
5519
5544
  status: "dead_letter",
5520
5545
  retryAfter: null
5521
5546
  } : {}
5522
5547
  }).where(eq(tasks2.id, id)).returning().get() ?? null;
5548
+ if (exhausted && updated) {
5549
+ const finishedAt = /* @__PURE__ */ new Date();
5550
+ tx.update(tasks2).set({
5551
+ status: "dead_letter",
5552
+ finishedAt,
5553
+ retryAfter: null,
5554
+ resultLog: `\u4F9D\u8D56\u4EFB\u52A1 #${id} \u5DF2\u8FDB\u5165\u4E0D\u53EF\u6062\u590D\u7EC8\u6001`
5555
+ }).where(blockedDependentsOf(id)).run();
5556
+ }
5557
+ return updated;
5523
5558
  }, { behavior: "immediate" });
5524
5559
  }
5525
5560
  static validateNewTask(data) {
@@ -5540,7 +5575,7 @@ var TaskService = class {
5540
5575
  throw new Error(`${name} \u5FC5\u987B\u662F ${min} \u5230 ${max} \u4E4B\u95F4\u7684\u6574\u6570`);
5541
5576
  }
5542
5577
  }
5543
- static async next(scope = {}) {
5578
+ static buildRunnableTaskWhere(scope) {
5544
5579
  const baseConditions = [...this.buildScopeWhere(scope)];
5545
5580
  const nowMs = Date.now();
5546
5581
  const retryAfterFilter = or(
@@ -5575,40 +5610,43 @@ var TaskService = class {
5575
5610
  if (batchFilter) {
5576
5611
  conditions.push(batchFilter);
5577
5612
  }
5578
- const result = await db.select().from(tasks2).where(and(
5613
+ return and(
5579
5614
  ...conditions,
5580
5615
  or(
5581
5616
  isNull(tasks2.dependsOn),
5582
5617
  sql`EXISTS (
5583
- SELECT 1 FROM tasks AS dependency_task
5584
- WHERE dependency_task.id = ${tasks2.dependsOn}
5585
- AND dependency_task.status = 'done'
5586
- AND dependency_task.cwd IS ${tasks2.cwd}
5587
- )`
5618
+ SELECT 1 FROM tasks AS dependency_task
5619
+ WHERE dependency_task.id = ${tasks2.dependsOn}
5620
+ AND dependency_task.status = 'done'
5621
+ AND dependency_task.cwd IS ${tasks2.cwd}
5622
+ )`
5588
5623
  ),
5589
5624
  or(
5590
5625
  isNull(tasks2.batchId),
5591
5626
  sql`trim(${tasks2.batchId}, ${TASK_BATCH_TRIM_CHARACTERS}) = ''`,
5592
5627
  sql`NOT EXISTS (
5593
- SELECT 1 FROM tasks AS running_batch_task
5594
- WHERE trim(running_batch_task.batch_id, ${TASK_BATCH_TRIM_CHARACTERS})
5595
- = trim(${tasks2.batchId}, ${TASK_BATCH_TRIM_CHARACTERS})
5596
- AND (
5597
- running_batch_task.status = 'running'
5598
- OR EXISTS (
5599
- SELECT 1 FROM task_runs AS running_batch_run
5600
- WHERE running_batch_run.task_id = running_batch_task.id
5601
- AND running_batch_run.status = 'running'
5602
- )
5628
+ SELECT 1 FROM tasks AS running_batch_task
5629
+ WHERE trim(running_batch_task.batch_id, ${TASK_BATCH_TRIM_CHARACTERS})
5630
+ = trim(${tasks2.batchId}, ${TASK_BATCH_TRIM_CHARACTERS})
5631
+ AND (
5632
+ running_batch_task.status = 'running'
5633
+ OR EXISTS (
5634
+ SELECT 1 FROM task_runs AS running_batch_run
5635
+ WHERE running_batch_run.task_id = running_batch_task.id
5636
+ AND running_batch_run.status = 'running'
5603
5637
  )
5604
- )`
5638
+ )
5639
+ )`
5605
5640
  ),
5606
5641
  sql`NOT EXISTS (
5607
- SELECT 1 FROM task_runs AS candidate_active_run
5608
- WHERE candidate_active_run.task_id = ${tasks2.id}
5609
- AND candidate_active_run.status = 'running'
5610
- )`
5611
- )).orderBy(
5642
+ SELECT 1 FROM task_runs AS candidate_active_run
5643
+ WHERE candidate_active_run.task_id = ${tasks2.id}
5644
+ AND candidate_active_run.status = 'running'
5645
+ )`
5646
+ );
5647
+ }
5648
+ static async next(scope = {}) {
5649
+ const result = await db.select().from(tasks2).where(this.buildRunnableTaskWhere(scope)).orderBy(
5612
5650
  desc(tasks2.urgency),
5613
5651
  desc(tasks2.importance),
5614
5652
  asc(tasks2.createdAt),
@@ -5616,6 +5654,22 @@ var TaskService = class {
5616
5654
  ).limit(1);
5617
5655
  return result[0] ?? null;
5618
5656
  }
5657
+ static async claimNext(scope = {}) {
5658
+ return db.transaction((tx) => {
5659
+ const candidate = tx.select().from(tasks2).where(this.buildRunnableTaskWhere(scope)).orderBy(
5660
+ desc(tasks2.urgency),
5661
+ desc(tasks2.importance),
5662
+ asc(tasks2.createdAt),
5663
+ asc(tasks2.id)
5664
+ ).limit(1).get();
5665
+ if (!candidate) return null;
5666
+ return tx.update(tasks2).set({
5667
+ status: "running",
5668
+ startedAt: /* @__PURE__ */ new Date(),
5669
+ finishedAt: null
5670
+ }).where(eq(tasks2.id, candidate.id)).returning().get() ?? null;
5671
+ }, { behavior: "immediate" });
5672
+ }
5619
5673
  static async countRunning(scope = {}) {
5620
5674
  const scopeConditions = scope.legacyCwd ? [sql`${tasks2.cwd} IS NULL OR trim(${tasks2.cwd}) = ''`] : this.buildScopeWhere(scope);
5621
5675
  if (scope.batchId !== void 0) {
@@ -6248,7 +6302,6 @@ var TaskService = class {
6248
6302
  };
6249
6303
 
6250
6304
  // src/core/process-control.ts
6251
- import { spawnSync } from "child_process";
6252
6305
  import { fileURLToPath as fileURLToPath2 } from "url";
6253
6306
  import { basename, dirname as dirname2, resolve } from "path";
6254
6307
 
@@ -6273,6 +6326,84 @@ function isMatchingDrainProof(message, launchIdentity) {
6273
6326
 
6274
6327
  // src/core/process-control.ts
6275
6328
  var OS_COMMAND_TIMEOUT_MS = 2e3;
6329
+ var OS_COMMAND_MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
6330
+ var OS_COMMAND_RESULT_MARKER = "\n\0supertask-os-command-result:";
6331
+ var BOUNDED_OS_COMMAND_RUNNER = `
6332
+ const { writeSync } = await import('fs');
6333
+ let child;
6334
+ try {
6335
+ child = Bun.spawn(process.argv.slice(2), {
6336
+ stdin: 'ignore',
6337
+ stdout: 'pipe',
6338
+ stderr: 'ignore',
6339
+ });
6340
+ } catch {
6341
+ process.exit(127);
6342
+ }
6343
+ const terminateGroup = () => {
6344
+ if (process.platform !== 'win32') {
6345
+ try {
6346
+ process.kill(-process.pid, 'SIGKILL');
6347
+ } catch {
6348
+ }
6349
+ }
6350
+ try {
6351
+ child.kill('SIGKILL');
6352
+ } catch {
6353
+ }
6354
+ process.exit(1);
6355
+ };
6356
+ const timer = setTimeout(terminateGroup, ${OS_COMMAND_TIMEOUT_MS});
6357
+ try {
6358
+ const chunks = [];
6359
+ let outputBytes = 0;
6360
+ const readOutput = async () => {
6361
+ const reader = child.stdout.getReader();
6362
+ while (true) {
6363
+ const { done, value } = await reader.read();
6364
+ if (done) return;
6365
+ outputBytes += value.byteLength;
6366
+ if (outputBytes > ${OS_COMMAND_MAX_OUTPUT_BYTES}) terminateGroup();
6367
+ chunks.push(value);
6368
+ }
6369
+ };
6370
+ const [exitCode] = await Promise.all([child.exited, readOutput()]);
6371
+ clearTimeout(timer);
6372
+ for (const chunk of chunks) writeSync(1, chunk);
6373
+ writeSync(1, ${JSON.stringify(OS_COMMAND_RESULT_MARKER)} + String(exitCode));
6374
+ if (process.platform !== 'win32') terminateGroup();
6375
+ process.exit(0);
6376
+ } catch {
6377
+ clearTimeout(timer);
6378
+ terminateGroup();
6379
+ }
6380
+ `;
6381
+ function runBoundedOsCommand(command, args, captureOutput = true) {
6382
+ const result = Bun.spawnSync({
6383
+ cmd: [
6384
+ process.execPath,
6385
+ "-e",
6386
+ BOUNDED_OS_COMMAND_RUNNER,
6387
+ "supertask-os-command",
6388
+ command,
6389
+ ...args
6390
+ ],
6391
+ detached: process.platform !== "win32",
6392
+ maxBuffer: OS_COMMAND_MAX_OUTPUT_BYTES + 1024,
6393
+ stdin: "ignore",
6394
+ stdout: "pipe",
6395
+ stderr: "ignore"
6396
+ });
6397
+ const output = new TextDecoder().decode(result.stdout);
6398
+ const markerIndex = output.lastIndexOf(OS_COMMAND_RESULT_MARKER);
6399
+ if (markerIndex < 0) return { status: null, stdout: "" };
6400
+ const statusText = output.slice(markerIndex + OS_COMMAND_RESULT_MARKER.length);
6401
+ if (!/^\d+$/.test(statusText)) return { status: null, stdout: "" };
6402
+ return {
6403
+ status: Number(statusText),
6404
+ stdout: captureOutput ? output.slice(0, markerIndex) : ""
6405
+ };
6406
+ }
6276
6407
  function isSafePid(pid) {
6277
6408
  return Number.isInteger(pid) && pid > 1 && pid !== process.pid;
6278
6409
  }
@@ -6306,12 +6437,10 @@ function inspectSpawnedProcessTreePresence(pid) {
6306
6437
  }
6307
6438
  }
6308
6439
  function inspectUnixProcess(pid) {
6309
- const result = spawnSync("ps", ["-o", "pgid=", "-o", "command=", "-p", String(pid)], {
6310
- encoding: "utf8",
6311
- stdio: ["ignore", "pipe", "ignore"],
6312
- timeout: OS_COMMAND_TIMEOUT_MS,
6313
- killSignal: "SIGKILL"
6314
- });
6440
+ const result = runBoundedOsCommand(
6441
+ "ps",
6442
+ ["-o", "pgid=", "-o", "command=", "-p", String(pid)]
6443
+ );
6315
6444
  if (result.status !== 0) return null;
6316
6445
  const match = result.stdout.trim().match(/^(\d+)\s+(.+)$/s);
6317
6446
  if (!match) return null;
@@ -6319,30 +6448,18 @@ function inspectUnixProcess(pid) {
6319
6448
  }
6320
6449
  function inspectWindowsCommand(pid) {
6321
6450
  const script = `(Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}").CommandLine`;
6322
- const result = spawnSync(
6451
+ const result = runBoundedOsCommand(
6323
6452
  "powershell.exe",
6324
- ["-NoProfile", "-NonInteractive", "-Command", script],
6325
- {
6326
- encoding: "utf8",
6327
- stdio: ["ignore", "pipe", "ignore"],
6328
- timeout: OS_COMMAND_TIMEOUT_MS,
6329
- killSignal: "SIGKILL"
6330
- }
6453
+ ["-NoProfile", "-NonInteractive", "-Command", script]
6331
6454
  );
6332
6455
  if (result.status !== 0) return null;
6333
6456
  return result.stdout.trim() || null;
6334
6457
  }
6335
6458
  function inspectWindowsProcessTree(rootPid) {
6336
6459
  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`;
6337
- const result = spawnSync(
6460
+ const result = runBoundedOsCommand(
6338
6461
  "powershell.exe",
6339
- ["-NoProfile", "-NonInteractive", "-Command", script],
6340
- {
6341
- encoding: "utf8",
6342
- stdio: ["ignore", "pipe", "ignore"],
6343
- timeout: OS_COMMAND_TIMEOUT_MS,
6344
- killSignal: "SIGKILL"
6345
- }
6462
+ ["-NoProfile", "-NonInteractive", "-Command", script]
6346
6463
  );
6347
6464
  if (result.status !== 0) return null;
6348
6465
  return result.stdout.split(/\s+/).filter(Boolean).map(Number).filter((pid) => Number.isInteger(pid) && pid > 0);
@@ -6399,11 +6516,7 @@ function signalRecordedProcessTreeWithResult(pid, signal, expectedExecutable, la
6399
6516
  }
6400
6517
  const args = ["/PID", String(pid), "/T"];
6401
6518
  if (signal === "SIGKILL") args.push("/F");
6402
- const status = spawnSync("taskkill", args, {
6403
- stdio: "ignore",
6404
- timeout: OS_COMMAND_TIMEOUT_MS,
6405
- killSignal: "SIGKILL"
6406
- }).status;
6519
+ const status = runBoundedOsCommand("taskkill", args, false).status;
6407
6520
  if (status === 0) return "signalled";
6408
6521
  return isProcessAlive(pid) ? "signal-failed" : absentLeaderResult(pid);
6409
6522
  }
@@ -6574,7 +6687,7 @@ var TaskRunService = class {
6574
6687
  }
6575
6688
  if (current.childPid != null) {
6576
6689
  throw new LegacyRunAbandonConflictError(
6577
- `run #${runId} \u5DF2\u8BB0\u5F55 child PID ${current.childPid}\uFF0C\u5FC5\u987B\u7531 Worker/Watchdog \u786E\u8BA4\u8FDB\u7A0B\u6811\u9000\u51FA`
6690
+ `run #${runId} \u5DF2\u8BB0\u5F55 child PID ${current.childPid}\uFF0C\u5FC5\u987B\u7531 Worker/Watchdog \u786E\u8BA4\u53D7\u7BA1\u8FDB\u7A0B\u7EC4\u6392\u7A7A`
6578
6691
  );
6579
6692
  }
6580
6693
  if (current.workerPid != null && isProcessAlive(current.workerPid)) {
@@ -6663,7 +6776,7 @@ function runCommandContext(executable, args, cwd) {
6663
6776
  function assertWorkerProcessIsolationSupported(platform = process.platform) {
6664
6777
  if (platform === "win32") {
6665
6778
  throw new Error(
6666
- "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"
6779
+ "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"
6667
6780
  );
6668
6781
  }
6669
6782
  }
@@ -6674,17 +6787,24 @@ var WorkerEngine = class {
6674
6787
  pollTimer = null;
6675
6788
  heartbeatTimer = null;
6676
6789
  pollCyclePromise = null;
6790
+ shutdownDeadlineMs = null;
6791
+ settlementRetryWakeups = /* @__PURE__ */ new Set();
6677
6792
  cfg;
6678
6793
  opencodeBin;
6679
6794
  maxOutputChars;
6795
+ settlementRetryDelaysMs;
6796
+ settlementRetryIntervalMs;
6680
6797
  constructor(cfg, options = {}) {
6681
6798
  this.cfg = cfg.worker;
6682
6799
  this.opencodeBin = options.opencodeBin ?? process.env.SUPERTASK_OPENCODE_BIN ?? "opencode";
6683
6800
  this.maxOutputChars = options.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
6801
+ this.settlementRetryDelaysMs = options.settlementRetryDelaysMs ?? [250, 1e3, 4e3];
6802
+ this.settlementRetryIntervalMs = options.settlementRetryIntervalMs ?? 5e3;
6684
6803
  }
6685
6804
  start() {
6686
6805
  assertWorkerProcessIsolationSupported();
6687
6806
  this.stopped = false;
6807
+ this.shutdownDeadlineMs = null;
6688
6808
  markGatewayActivity("worker");
6689
6809
  this.poll();
6690
6810
  this.heartbeatTimer = setInterval(() => {
@@ -6693,6 +6813,8 @@ var WorkerEngine = class {
6693
6813
  }
6694
6814
  async stop(gracePeriodMs = 0) {
6695
6815
  this.stopped = true;
6816
+ this.shutdownDeadlineMs = Date.now() + Math.max(0, gracePeriodMs);
6817
+ for (const wake of [...this.settlementRetryWakeups]) wake();
6696
6818
  if (this.pollTimer) {
6697
6819
  clearTimeout(this.pollTimer);
6698
6820
  this.pollTimer = null;
@@ -6762,27 +6884,25 @@ var WorkerEngine = class {
6762
6884
  if (databaseRunningCount >= this.cfg.maxConcurrency) break;
6763
6885
  let task;
6764
6886
  try {
6765
- task = await TaskService.next({ excludedBatchIds: [...this.activeBatchIds] });
6887
+ task = await TaskService.claimNext({ excludedBatchIds: [...this.activeBatchIds] });
6766
6888
  } catch (err) {
6767
6889
  this.logError("task claim failed", err);
6768
6890
  throw err;
6769
6891
  }
6770
6892
  if (!task) break;
6771
- if (this.stopped) break;
6772
- if (!await TaskService.start(task.id)) continue;
6773
- const batchId = normalizeTaskBatchId(task.batchId);
6774
- if (batchId) this.activeBatchIds.add(batchId);
6775
6893
  if (this.stopped) {
6776
6894
  await TaskService.resetRunningToPending([task.id]);
6777
- this.releaseBatch(task);
6778
6895
  break;
6779
6896
  }
6897
+ const batchId = normalizeTaskBatchId(task.batchId);
6898
+ if (batchId) this.activeBatchIds.add(batchId);
6780
6899
  let runId = null;
6781
6900
  try {
6782
6901
  const launchIdentity = `gateway-${process.pid}:launch:${randomUUID()}`;
6783
6902
  const run = await TaskRunService.create({
6784
6903
  taskId: task.id,
6785
6904
  model: this.resolveModel(task.model),
6905
+ variant: this.resolveVariant(task.variant),
6786
6906
  status: "running",
6787
6907
  workerPid: process.pid,
6788
6908
  lockedAt: Date.now(),
@@ -6845,8 +6965,10 @@ var WorkerEngine = class {
6845
6965
  }
6846
6966
  async spawnTask(task, runId, launchIdentity) {
6847
6967
  const model = this.resolveModel(task.model);
6968
+ const variant = this.resolveVariant(task.variant);
6848
6969
  const args = ["run", "--agent", task.agent, "--format", "json"];
6849
6970
  if (model) args.push("-m", model);
6971
+ if (variant) args.push("--variant", variant);
6850
6972
  args.push(task.prompt);
6851
6973
  const cwd = task.cwd || process.cwd();
6852
6974
  const child = spawn(process.execPath, [
@@ -6984,7 +7106,7 @@ var WorkerEngine = class {
6984
7106
  if (!entry.guardianDrained) {
6985
7107
  const pid = entry.child.pid;
6986
7108
  const presence = pid == null ? spawnError == null ? "unknown" : "not-running" : inspectSpawnedProcessTreePresence(pid);
6987
- const message = "guardian \u672A\u63D0\u4F9B\u8FDB\u7A0B\u6811\u6392\u7A7A\u8BC1\u660E";
7109
+ const message = "guardian \u672A\u63D0\u4F9B\u53D7\u7BA1\u8FDB\u7A0B\u7EC4\u6392\u7A7A\u8BC1\u660E";
6988
7110
  if (presence !== "not-running") {
6989
7111
  if (entry.timeoutTimer) {
6990
7112
  clearTimeout(entry.timeoutTimer);
@@ -7005,7 +7127,7 @@ var WorkerEngine = class {
7005
7127
  );
7006
7128
  return;
7007
7129
  }
7008
- 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`;
7130
+ 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`;
7009
7131
  this.runDetached(
7010
7132
  this.settleEntry(entry, code, failure),
7011
7133
  "task settlement failed",
@@ -7020,17 +7142,48 @@ var WorkerEngine = class {
7020
7142
  clearTimeout(entry.timeoutTimer);
7021
7143
  entry.timeoutTimer = null;
7022
7144
  }
7023
- const settlement = this.commitEntry(entry, code, failure).then(() => true).catch((error) => {
7024
- markGatewayFailure("worker", error);
7025
- this.logError("task settlement failed", error, entry.task.id);
7026
- return false;
7027
- }).finally(() => {
7145
+ const settlement = this.commitEntryWithRetry(entry, code, failure).finally(() => {
7028
7146
  this.runningTasks.delete(entry.task.id);
7029
7147
  this.releaseBatch(entry.task);
7030
7148
  });
7031
7149
  entry.settlementPromise = settlement;
7032
7150
  return settlement;
7033
7151
  }
7152
+ async commitEntryWithRetry(entry, code, failure) {
7153
+ for (let attempt = 0; ; attempt += 1) {
7154
+ try {
7155
+ await this.commitEntry(entry, code, failure);
7156
+ return true;
7157
+ } catch (error) {
7158
+ markGatewayFailure("worker", error);
7159
+ const shortRetryDelayMs = this.settlementRetryDelaysMs[attempt];
7160
+ let retryDelayMs = shortRetryDelayMs ?? this.settlementRetryIntervalMs;
7161
+ if (this.stopped) {
7162
+ const remainingMs = Math.max(0, (this.shutdownDeadlineMs ?? 0) - Date.now());
7163
+ if (remainingMs === 0) return false;
7164
+ retryDelayMs = Math.min(retryDelayMs, remainingMs);
7165
+ }
7166
+ this.logError(
7167
+ `task settlement failed; retrying in ${retryDelayMs}ms`,
7168
+ error,
7169
+ entry.task.id
7170
+ );
7171
+ await this.waitForSettlementRetry(retryDelayMs);
7172
+ }
7173
+ }
7174
+ }
7175
+ waitForSettlementRetry(delayMs) {
7176
+ return new Promise((resolve2) => {
7177
+ let timer;
7178
+ const finish = () => {
7179
+ clearTimeout(timer);
7180
+ this.settlementRetryWakeups.delete(finish);
7181
+ resolve2();
7182
+ };
7183
+ timer = setTimeout(finish, delayMs);
7184
+ this.settlementRetryWakeups.add(finish);
7185
+ });
7186
+ }
7034
7187
  async commitEntry(entry, code, failure) {
7035
7188
  const termination = entry.termination;
7036
7189
  if (termination?.kind === "shutdown") return;
@@ -7194,6 +7347,9 @@ ${output}` : ""}`;
7194
7347
  if (!taskModel || taskModel === "default") return null;
7195
7348
  return taskModel;
7196
7349
  }
7350
+ resolveVariant(taskVariant) {
7351
+ return taskVariant?.trim() || null;
7352
+ }
7197
7353
  runDetached(operation, message, taskId) {
7198
7354
  operation.catch((error) => {
7199
7355
  markGatewayFailure("worker", error);