opencode-supertask 0.1.40 → 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;
@@ -5538,13 +5538,23 @@ var TaskService = class {
5538
5538
  });
5539
5539
  const maxRetries = normalizedData.maxRetries ?? task.maxRetries ?? 3;
5540
5540
  const exhausted = task.status === "failed" && (task.retryCount ?? 0) > maxRetries;
5541
- return tx.update(tasks2).set({
5541
+ const updated = tx.update(tasks2).set({
5542
5542
  ...normalizedData,
5543
5543
  ...exhausted ? {
5544
5544
  status: "dead_letter",
5545
5545
  retryAfter: null
5546
5546
  } : {}
5547
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;
5548
5558
  }, { behavior: "immediate" });
5549
5559
  }
5550
5560
  static validateNewTask(data) {
@@ -5565,7 +5575,7 @@ var TaskService = class {
5565
5575
  throw new Error(`${name} \u5FC5\u987B\u662F ${min} \u5230 ${max} \u4E4B\u95F4\u7684\u6574\u6570`);
5566
5576
  }
5567
5577
  }
5568
- static async next(scope = {}) {
5578
+ static buildRunnableTaskWhere(scope) {
5569
5579
  const baseConditions = [...this.buildScopeWhere(scope)];
5570
5580
  const nowMs = Date.now();
5571
5581
  const retryAfterFilter = or(
@@ -5600,40 +5610,43 @@ var TaskService = class {
5600
5610
  if (batchFilter) {
5601
5611
  conditions.push(batchFilter);
5602
5612
  }
5603
- const result = await db.select().from(tasks2).where(and(
5613
+ return and(
5604
5614
  ...conditions,
5605
5615
  or(
5606
5616
  isNull(tasks2.dependsOn),
5607
5617
  sql`EXISTS (
5608
- SELECT 1 FROM tasks AS dependency_task
5609
- WHERE dependency_task.id = ${tasks2.dependsOn}
5610
- AND dependency_task.status = 'done'
5611
- AND dependency_task.cwd IS ${tasks2.cwd}
5612
- )`
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
+ )`
5613
5623
  ),
5614
5624
  or(
5615
5625
  isNull(tasks2.batchId),
5616
5626
  sql`trim(${tasks2.batchId}, ${TASK_BATCH_TRIM_CHARACTERS}) = ''`,
5617
5627
  sql`NOT EXISTS (
5618
- SELECT 1 FROM tasks AS running_batch_task
5619
- WHERE trim(running_batch_task.batch_id, ${TASK_BATCH_TRIM_CHARACTERS})
5620
- = trim(${tasks2.batchId}, ${TASK_BATCH_TRIM_CHARACTERS})
5621
- AND (
5622
- running_batch_task.status = 'running'
5623
- OR EXISTS (
5624
- SELECT 1 FROM task_runs AS running_batch_run
5625
- WHERE running_batch_run.task_id = running_batch_task.id
5626
- AND running_batch_run.status = 'running'
5627
- )
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'
5628
5637
  )
5629
- )`
5638
+ )
5639
+ )`
5630
5640
  ),
5631
5641
  sql`NOT EXISTS (
5632
- SELECT 1 FROM task_runs AS candidate_active_run
5633
- WHERE candidate_active_run.task_id = ${tasks2.id}
5634
- AND candidate_active_run.status = 'running'
5635
- )`
5636
- )).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(
5637
5650
  desc(tasks2.urgency),
5638
5651
  desc(tasks2.importance),
5639
5652
  asc(tasks2.createdAt),
@@ -5641,6 +5654,22 @@ var TaskService = class {
5641
5654
  ).limit(1);
5642
5655
  return result[0] ?? null;
5643
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
+ }
5644
5673
  static async countRunning(scope = {}) {
5645
5674
  const scopeConditions = scope.legacyCwd ? [sql`${tasks2.cwd} IS NULL OR trim(${tasks2.cwd}) = ''`] : this.buildScopeWhere(scope);
5646
5675
  if (scope.batchId !== void 0) {
@@ -6273,7 +6302,6 @@ var TaskService = class {
6273
6302
  };
6274
6303
 
6275
6304
  // src/core/process-control.ts
6276
- import { spawnSync } from "child_process";
6277
6305
  import { fileURLToPath as fileURLToPath2 } from "url";
6278
6306
  import { basename, dirname as dirname2, resolve } from "path";
6279
6307
 
@@ -6298,6 +6326,84 @@ function isMatchingDrainProof(message, launchIdentity) {
6298
6326
 
6299
6327
  // src/core/process-control.ts
6300
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
+ }
6301
6407
  function isSafePid(pid) {
6302
6408
  return Number.isInteger(pid) && pid > 1 && pid !== process.pid;
6303
6409
  }
@@ -6331,12 +6437,10 @@ function inspectSpawnedProcessTreePresence(pid) {
6331
6437
  }
6332
6438
  }
6333
6439
  function inspectUnixProcess(pid) {
6334
- const result = spawnSync("ps", ["-o", "pgid=", "-o", "command=", "-p", String(pid)], {
6335
- encoding: "utf8",
6336
- stdio: ["ignore", "pipe", "ignore"],
6337
- timeout: OS_COMMAND_TIMEOUT_MS,
6338
- killSignal: "SIGKILL"
6339
- });
6440
+ const result = runBoundedOsCommand(
6441
+ "ps",
6442
+ ["-o", "pgid=", "-o", "command=", "-p", String(pid)]
6443
+ );
6340
6444
  if (result.status !== 0) return null;
6341
6445
  const match = result.stdout.trim().match(/^(\d+)\s+(.+)$/s);
6342
6446
  if (!match) return null;
@@ -6344,30 +6448,18 @@ function inspectUnixProcess(pid) {
6344
6448
  }
6345
6449
  function inspectWindowsCommand(pid) {
6346
6450
  const script = `(Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}").CommandLine`;
6347
- const result = spawnSync(
6451
+ const result = runBoundedOsCommand(
6348
6452
  "powershell.exe",
6349
- ["-NoProfile", "-NonInteractive", "-Command", script],
6350
- {
6351
- encoding: "utf8",
6352
- stdio: ["ignore", "pipe", "ignore"],
6353
- timeout: OS_COMMAND_TIMEOUT_MS,
6354
- killSignal: "SIGKILL"
6355
- }
6453
+ ["-NoProfile", "-NonInteractive", "-Command", script]
6356
6454
  );
6357
6455
  if (result.status !== 0) return null;
6358
6456
  return result.stdout.trim() || null;
6359
6457
  }
6360
6458
  function inspectWindowsProcessTree(rootPid) {
6361
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`;
6362
- const result = spawnSync(
6460
+ const result = runBoundedOsCommand(
6363
6461
  "powershell.exe",
6364
- ["-NoProfile", "-NonInteractive", "-Command", script],
6365
- {
6366
- encoding: "utf8",
6367
- stdio: ["ignore", "pipe", "ignore"],
6368
- timeout: OS_COMMAND_TIMEOUT_MS,
6369
- killSignal: "SIGKILL"
6370
- }
6462
+ ["-NoProfile", "-NonInteractive", "-Command", script]
6371
6463
  );
6372
6464
  if (result.status !== 0) return null;
6373
6465
  return result.stdout.split(/\s+/).filter(Boolean).map(Number).filter((pid) => Number.isInteger(pid) && pid > 0);
@@ -6424,11 +6516,7 @@ function signalRecordedProcessTreeWithResult(pid, signal, expectedExecutable, la
6424
6516
  }
6425
6517
  const args = ["/PID", String(pid), "/T"];
6426
6518
  if (signal === "SIGKILL") args.push("/F");
6427
- const status = spawnSync("taskkill", args, {
6428
- stdio: "ignore",
6429
- timeout: OS_COMMAND_TIMEOUT_MS,
6430
- killSignal: "SIGKILL"
6431
- }).status;
6519
+ const status = runBoundedOsCommand("taskkill", args, false).status;
6432
6520
  if (status === 0) return "signalled";
6433
6521
  return isProcessAlive(pid) ? "signal-failed" : absentLeaderResult(pid);
6434
6522
  }
@@ -6599,7 +6687,7 @@ var TaskRunService = class {
6599
6687
  }
6600
6688
  if (current.childPid != null) {
6601
6689
  throw new LegacyRunAbandonConflictError(
6602
- `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`
6603
6691
  );
6604
6692
  }
6605
6693
  if (current.workerPid != null && isProcessAlive(current.workerPid)) {
@@ -6688,7 +6776,7 @@ function runCommandContext(executable, args, cwd) {
6688
6776
  function assertWorkerProcessIsolationSupported(platform = process.platform) {
6689
6777
  if (platform === "win32") {
6690
6778
  throw new Error(
6691
- "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"
6692
6780
  );
6693
6781
  }
6694
6782
  }
@@ -6699,17 +6787,24 @@ var WorkerEngine = class {
6699
6787
  pollTimer = null;
6700
6788
  heartbeatTimer = null;
6701
6789
  pollCyclePromise = null;
6790
+ shutdownDeadlineMs = null;
6791
+ settlementRetryWakeups = /* @__PURE__ */ new Set();
6702
6792
  cfg;
6703
6793
  opencodeBin;
6704
6794
  maxOutputChars;
6795
+ settlementRetryDelaysMs;
6796
+ settlementRetryIntervalMs;
6705
6797
  constructor(cfg, options = {}) {
6706
6798
  this.cfg = cfg.worker;
6707
6799
  this.opencodeBin = options.opencodeBin ?? process.env.SUPERTASK_OPENCODE_BIN ?? "opencode";
6708
6800
  this.maxOutputChars = options.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
6801
+ this.settlementRetryDelaysMs = options.settlementRetryDelaysMs ?? [250, 1e3, 4e3];
6802
+ this.settlementRetryIntervalMs = options.settlementRetryIntervalMs ?? 5e3;
6709
6803
  }
6710
6804
  start() {
6711
6805
  assertWorkerProcessIsolationSupported();
6712
6806
  this.stopped = false;
6807
+ this.shutdownDeadlineMs = null;
6713
6808
  markGatewayActivity("worker");
6714
6809
  this.poll();
6715
6810
  this.heartbeatTimer = setInterval(() => {
@@ -6718,6 +6813,8 @@ var WorkerEngine = class {
6718
6813
  }
6719
6814
  async stop(gracePeriodMs = 0) {
6720
6815
  this.stopped = true;
6816
+ this.shutdownDeadlineMs = Date.now() + Math.max(0, gracePeriodMs);
6817
+ for (const wake of [...this.settlementRetryWakeups]) wake();
6721
6818
  if (this.pollTimer) {
6722
6819
  clearTimeout(this.pollTimer);
6723
6820
  this.pollTimer = null;
@@ -6787,21 +6884,18 @@ var WorkerEngine = class {
6787
6884
  if (databaseRunningCount >= this.cfg.maxConcurrency) break;
6788
6885
  let task;
6789
6886
  try {
6790
- task = await TaskService.next({ excludedBatchIds: [...this.activeBatchIds] });
6887
+ task = await TaskService.claimNext({ excludedBatchIds: [...this.activeBatchIds] });
6791
6888
  } catch (err) {
6792
6889
  this.logError("task claim failed", err);
6793
6890
  throw err;
6794
6891
  }
6795
6892
  if (!task) break;
6796
- if (this.stopped) break;
6797
- if (!await TaskService.start(task.id)) continue;
6798
- const batchId = normalizeTaskBatchId(task.batchId);
6799
- if (batchId) this.activeBatchIds.add(batchId);
6800
6893
  if (this.stopped) {
6801
6894
  await TaskService.resetRunningToPending([task.id]);
6802
- this.releaseBatch(task);
6803
6895
  break;
6804
6896
  }
6897
+ const batchId = normalizeTaskBatchId(task.batchId);
6898
+ if (batchId) this.activeBatchIds.add(batchId);
6805
6899
  let runId = null;
6806
6900
  try {
6807
6901
  const launchIdentity = `gateway-${process.pid}:launch:${randomUUID()}`;
@@ -7012,7 +7106,7 @@ var WorkerEngine = class {
7012
7106
  if (!entry.guardianDrained) {
7013
7107
  const pid = entry.child.pid;
7014
7108
  const presence = pid == null ? spawnError == null ? "unknown" : "not-running" : inspectSpawnedProcessTreePresence(pid);
7015
- 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";
7016
7110
  if (presence !== "not-running") {
7017
7111
  if (entry.timeoutTimer) {
7018
7112
  clearTimeout(entry.timeoutTimer);
@@ -7048,17 +7142,48 @@ var WorkerEngine = class {
7048
7142
  clearTimeout(entry.timeoutTimer);
7049
7143
  entry.timeoutTimer = null;
7050
7144
  }
7051
- const settlement = this.commitEntry(entry, code, failure).then(() => true).catch((error) => {
7052
- markGatewayFailure("worker", error);
7053
- this.logError("task settlement failed", error, entry.task.id);
7054
- return false;
7055
- }).finally(() => {
7145
+ const settlement = this.commitEntryWithRetry(entry, code, failure).finally(() => {
7056
7146
  this.runningTasks.delete(entry.task.id);
7057
7147
  this.releaseBatch(entry.task);
7058
7148
  });
7059
7149
  entry.settlementPromise = settlement;
7060
7150
  return settlement;
7061
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
+ }
7062
7187
  async commitEntry(entry, code, failure) {
7063
7188
  const termination = entry.termination;
7064
7189
  if (termination?.kind === "shutdown") return;