@ai-dossier/sched 0.23.5 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/engine.js CHANGED
@@ -73,6 +73,7 @@ exports.runLoop = runLoop;
73
73
  const core_1 = require("@ai-dossier/core");
74
74
  const batch_dispatch_1 = require("./batch-dispatch");
75
75
  const dispatch_1 = require("./dispatch");
76
+ const dispatch_health_1 = require("./dispatch-health");
76
77
  const groundtruth_1 = require("./groundtruth");
77
78
  const journal_1 = require("./journal");
78
79
  const labels_1 = require("./labels");
@@ -725,7 +726,14 @@ function writeFence(ctx, unit, issue, slot, truth) {
725
726
  * `unit-failed` line otherwise — and that indistinguishability is exactly
726
727
  * what cost imboard-monorepo#3999 (docs/agent-traps.md).
727
728
  */
728
- function failOrAdoptOpenPr(ctx, state, unit, slot, report, causeEvent, cause, evidence) {
729
+ function failOrAdoptOpenPr(ctx, state, unit, slot, report,
730
+ // #629: never `'dispatch-failure'` in practice — `enterRecovery` only
731
+ // reaches this function under `escalate: true` (its cap-check is itself
732
+ // gated on `escalate`), and a `dispatch-failure` cause always passes
733
+ // `escalate: false`. Kept as the two-member union `enterRecovery` uses
734
+ // before its escalate check, so a maintainer reading this signature does
735
+ // not read "a spend wall can terminally fail a unit" as a live path.
736
+ causeEvent, cause, evidence) {
729
737
  let prCheck = 'skipped';
730
738
  if (!report && causeEvent === 'verify-incomplete') {
731
739
  if (slot.branch === null) {
@@ -776,7 +784,7 @@ function failOrAdoptOpenPr(ctx, state, unit, slot, report, causeEvent, cause, ev
776
784
  // with `causeEvent === 'verify-incomplete'` thread it through
777
785
  // `completeUnitOrRecover`: `reconcileRunning`'s dead-pid rail, from
778
786
  // `recordDispatchRunLog`'s own read, and (since #620) `reconcileSlots`'
779
- // `exited`/`verifying` rail, from `readLastToolForSlot`. The two agree —
787
+ // `exited`/`verifying` rail, from `readDispatchSignalsForSlot`. The two agree —
780
788
  // the dispatch log is static once the agent has exited — so the tool name
781
789
  // survives regardless of which tick reaches this decision. It stays
782
790
  // OPTIONAL: a slice with no parseable `tool_use` yields null, hence the
@@ -798,7 +806,14 @@ function failOrAdoptOpenPr(ctx, state, unit, slot, report, causeEvent, cause, ev
798
806
  * mechanical-starting ladder and fail MERGED-AWARE at the cap: the PR is
799
807
  * already merged, so dependents stay released.
800
808
  */
801
- function enterRecovery(ctx, state, unit, causeEvent, cause, truth, evidence = {}) {
809
+ function enterRecovery(ctx, state, unit, causeEvent, cause, truth, evidence = {},
810
+ // #629: `escalate: false` for a CONFIRMED provider API error — it must
811
+ // never consume the per-unit escalation ladder or the ESCALATION_CAP
812
+ // (a spend/rate wall is not the issue's fault), so the respawn keeps the
813
+ // slot's CURRENT tier and `recoveries` count unchanged. Every other caller
814
+ // keeps the default (`true`), unaffected.
815
+ options = {}) {
816
+ const escalate = options.escalate ?? true;
802
817
  const issue = (0, journal_1.issueOfUnit)(unit);
803
818
  if (issue === null)
804
819
  return state;
@@ -809,21 +824,36 @@ function enterRecovery(ctx, state, unit, causeEvent, cause, truth, evidence = {}
809
824
  return state;
810
825
  killUnitAgent(ctx, state, unit);
811
826
  // #524: only the STALL path kills a still-live, not-yet-recorded agent —
812
- // `causeEvent === 'verify-incomplete'` arrives from `completeUnitOrRecover`
813
- // AFTER the agent's exit was already detected and recorded by the dead-pid
814
- // branch of `reconcileRunning`; recording again here would double-count
815
- // that same dispatch. #591: a stall kill still has a fresh log slice worth
816
- // reading for `last_tool` — a hung agent's last tool is exactly what tells
817
- // an operator what it hung in.
827
+ // `causeEvent === 'verify-incomplete'`/`'dispatch-failure'` arrive from
828
+ // `completeUnitOrRecover` AFTER the agent's exit was already detected and
829
+ // recorded by the dead-pid branch of `reconcileRunning`; recording again
830
+ // here would double-count that same dispatch. #591: a stall kill still has
831
+ // a fresh log slice worth reading for `last_tool` — a hung agent's last
832
+ // tool is exactly what tells an operator what it hung in.
818
833
  if (causeEvent === 'stalled') {
819
- const stallLastTool = recordDispatchRunLog(ctx, state, slot, unit);
834
+ const stallLastTool = recordDispatchRunLog(ctx, state, slot, unit).lastTool;
820
835
  if (stallLastTool !== null)
821
836
  evidence = { ...evidence, last_tool: stallLastTool };
822
837
  }
823
838
  const report = (0, state_1.isReportSlot)(slot);
824
- const nextTier = report ? (0, dispatch_1.reportTierFor)(slot.recoveries + 1) : (0, dispatch_1.escalateTier)(entry.tier);
825
- if (slot.recoveries >= types_1.ESCALATION_CAP || nextTier === null) {
826
- return failOrAdoptOpenPr(ctx, state, unit, slot, report, causeEvent, cause, evidence);
839
+ let resolvedTier;
840
+ if (escalate) {
841
+ const escalated = report ? (0, dispatch_1.reportTierFor)(slot.recoveries + 1) : (0, dispatch_1.escalateTier)(entry.tier);
842
+ if (slot.recoveries >= types_1.ESCALATION_CAP || escalated === null) {
843
+ // #629: unreachable with `causeEvent === 'dispatch-failure'` — that
844
+ // cause always passes `escalate: false` above, so this branch (and the
845
+ // narrower `causeEvent` type `failOrAdoptOpenPr` declares) is never
846
+ // actually asked to terminally fail a unit over a provider wall.
847
+ return failOrAdoptOpenPr(ctx, state, unit, slot, report, causeEvent, cause, evidence);
848
+ }
849
+ resolvedTier = escalated;
850
+ }
851
+ else {
852
+ // An unescalated redispatch keeps the CURRENT tier — `reportTierFor`
853
+ // evaluated at the unchanged `slot.recoveries` for a report slot (mirrors
854
+ // `recordDispatchRunLog`'s own tier expression), `entry.tier` verbatim
855
+ // otherwise — never `escalateTier`/`recoveries + 1`.
856
+ resolvedTier = report ? ((0, dispatch_1.reportTierFor)(slot.recoveries) ?? entry.tier) : entry.tier;
827
857
  }
828
858
  // Fence BEFORE the respawn (#504 AC1/AC4): `killUnitAgent` above only reaches a pid
829
859
  // this process can see and signal, and #472 proved that is not the same as a dead
@@ -832,17 +862,17 @@ function enterRecovery(ctx, state, unit, causeEvent, cause, truth, evidence = {}
832
862
  const fenced = writeFence(ctx, unit, issue, slot, truth);
833
863
  let next = (0, state_1.transitionSlot)(state, slot.id, 'recovering', {
834
864
  pid: null,
835
- recoveries: slot.recoveries + 1,
865
+ recoveries: escalate ? slot.recoveries + 1 : slot.recoveries,
836
866
  gen: fenced ?? slot.gen,
837
867
  // Only a fence that actually landed starts the short takeover watch: an
838
868
  // unfenced redispatch is already degraded, and cutting its allowance down
839
869
  // would compound one failure with another.
840
870
  fenced_at: fenced === null ? null : now.toISOString(),
841
871
  }, now);
842
- if (!report) {
872
+ if (!report && escalate) {
843
873
  next = {
844
874
  ...next,
845
- entries: next.entries.map((e) => e.issue === issue ? { ...e, tier: nextTier, updated_at: now.toISOString() } : e),
875
+ entries: next.entries.map((e) => e.issue === issue ? { ...e, tier: resolvedTier, updated_at: now.toISOString() } : e),
846
876
  };
847
877
  }
848
878
  journal(ctx, causeEvent, unit, {
@@ -851,10 +881,22 @@ function enterRecovery(ctx, state, unit, causeEvent, cause, truth, evidence = {}
851
881
  ...(slot.last_progress_at !== null ? { last_progress_at: slot.last_progress_at } : {}),
852
882
  ...evidence,
853
883
  });
884
+ // #629: an UNESCALATED redispatch (a confirmed provider API error) has no
885
+ // natural bound — `recoveries`/`ESCALATION_CAP` never advance for it — so
886
+ // once the dispatch-health pause has fired, respawning it every tick would
887
+ // reproduce the exact incident this fix exists to stop, just on the
888
+ // per-issue rail instead of the batch one (`runBatchTick` gets the
889
+ // equivalent gate below). Hold the slot in `recovering`, unspawned, until
890
+ // `sched resume`: `reconcileRecovering` (below) is what retries it once the
891
+ // pause clears — no separate re-classification happens while parked, since
892
+ // this function is reached exactly once per dead dispatch.
893
+ if (!escalate && next.paused) {
894
+ return next;
895
+ }
854
896
  journal(ctx, 'redispatched', unit, {
855
- tier: nextTier,
897
+ tier: resolvedTier,
856
898
  slot: slot.id,
857
- ...(0, dispatch_1.journalCmdModelFields)((0, dispatch_1.resolveTierSpawn)(ctx.dispatch, nextTier, issue)),
899
+ ...(0, dispatch_1.journalCmdModelFields)((0, dispatch_1.resolveTierSpawn)(ctx.dispatch, resolvedTier, issue)),
858
900
  });
859
901
  ctx.result.redispatched.push(unit);
860
902
  // Respawn immediately on the recovering rail — recovering → running. A
@@ -1125,22 +1167,24 @@ function journalStaleMilestoneIfIgnored(ctx, state, unit, slot, truth, verifiedC
1125
1167
  });
1126
1168
  return (0, state_1.patchSlot)(state, slot.id, { stale_milestone_ignored_for: slot.spawned_at }, now);
1127
1169
  }
1170
+ const NO_DISPATCH_SIGNALS = { lastTool: null, apiError: null };
1128
1171
  /**
1129
- * Read THIS dispatch's last tool call from its log slice (#591, #620) — a
1130
- * pure, side-effect-free parse, safe to call from any slot status and any
1131
- * number of times: unlike `recordDispatchRunLog` below, it writes nothing to
1132
- * `runs.jsonl`, so it carries no exactly-once constraint. Exists because a
1133
- * `verify-incomplete`/`unit-failed` decision can land on a LATER tick than
1134
- * the one that detected the dead pid (e.g. ground truth was unreachable in
1135
- * between) — by then the slot has moved past `running` and
1136
- * `recordDispatchRunLog`'s guard refuses to re-read, but the dispatch's log
1137
- * file is static once the agent has exited, so re-parsing it here yields the
1138
- * same answer every time.
1172
+ * Read THIS dispatch's last tool call and API-error classification (#591,
1173
+ * #620, #629) from its log slice — a pure, side-effect-free parse, safe to
1174
+ * call from any slot status and any number of times: unlike
1175
+ * `recordDispatchRunLog` below, it writes nothing to `runs.jsonl`, so it
1176
+ * carries no exactly-once constraint. Exists because a `verify-incomplete`/
1177
+ * `dispatch-failure` decision can land on a LATER tick than the one that
1178
+ * detected the dead pid (e.g. ground truth was unreachable in between) — by
1179
+ * then the slot has moved past `running` and `recordDispatchRunLog`'s guard
1180
+ * refuses to re-read, but the dispatch's log file is static once the agent
1181
+ * has exited, so re-parsing it here yields the same answer every time.
1139
1182
  */
1140
- function readLastToolForSlot(ctx, slot, unit) {
1183
+ function readDispatchSignalsForSlot(ctx, slot, unit) {
1141
1184
  if (slot.spawned_at === null)
1142
- return null;
1143
- return (0, core_1.parseLastToolUse)(dispatchLogSlice(ctx, slot, unit).content);
1185
+ return NO_DISPATCH_SIGNALS;
1186
+ const { content } = dispatchLogSlice(ctx, slot, unit);
1187
+ return { lastTool: (0, core_1.parseLastToolUse)(content), apiError: (0, core_1.parseDispatchApiError)(content) };
1144
1188
  }
1145
1189
  /**
1146
1190
  * THIS dispatch's slice of the per-unit log: path, start offset, contents.
@@ -1187,7 +1231,7 @@ function recordDispatchRunLog(ctx, state, slot, unit) {
1187
1231
  reason: slot.spawned_at === null ? 'never-spawned' : `already-recorded-${slot.status}`,
1188
1232
  slot: slot.id,
1189
1233
  });
1190
- return null;
1234
+ return NO_DISPATCH_SIGNALS;
1191
1235
  }
1192
1236
  const issue = (0, journal_1.issueOfUnit)(unit);
1193
1237
  const entry = issue === null ? undefined : (0, state_1.findEntry)(state, issue);
@@ -1199,7 +1243,7 @@ function recordDispatchRunLog(ctx, state, slot, unit) {
1199
1243
  journal(ctx, 'run-log-skipped', unit, {
1200
1244
  reason: issue === null ? 'not-an-issue-unit' : 'entry-gone',
1201
1245
  });
1202
- return null;
1246
+ return NO_DISPATCH_SIGNALS;
1203
1247
  }
1204
1248
  // Report slots ride the same tier the cycle escalation ladder set at
1205
1249
  // dispatch time, EXCEPT `entry.tier` is deliberately not updated for a
@@ -1228,7 +1272,9 @@ function recordDispatchRunLog(ctx, state, slot, unit) {
1228
1272
  (0, run_log_1.finalizeRunLogEntry)(runEntry, logContent, ctx.deps.homeDir, (event, extra) => journal(ctx, event, unit, extra), { log: logFile, offset });
1229
1273
  // #591: the last tool this dispatch called, so an unverified exit attributes to a
1230
1274
  // concrete cause (e.g. `Monitor`) without opening the transcript — see `enterRecovery`.
1231
- return (0, core_1.parseLastToolUse)(logContent);
1275
+ // #629: whether this SAME dispatch was a confirmed provider API error — see
1276
+ // `completeUnitOrRecover`.
1277
+ return { lastTool: (0, core_1.parseLastToolUse)(logContent), apiError: (0, core_1.parseDispatchApiError)(logContent) };
1232
1278
  }
1233
1279
  /** Reconcile one running slot against its polled ground truth. */
1234
1280
  function reconcileRunning(ctx, state, slot, truth, unit) {
@@ -1237,9 +1283,9 @@ function reconcileRunning(ctx, state, slot, truth, unit) {
1237
1283
  // is DETECTED, never trusted as completion (AC2/AC3).
1238
1284
  if (slot.pid !== null && !ctx.deps.spawnDeps.isAlive(slot.pid, slot.pid_start ?? undefined)) {
1239
1285
  journal(ctx, 'exit-detected', unit, { pid: slot.pid, slot: slot.id });
1240
- const lastTool = recordDispatchRunLog(ctx, state, slot, unit);
1286
+ const signals = recordDispatchRunLog(ctx, state, slot, unit);
1241
1287
  const exited = (0, state_1.transitionSlot)(state, slot.id, 'exited', {}, now);
1242
- return completeUnitOrRecover(ctx, exited, unit, truth, 'verify-complete', lastTool);
1288
+ return completeUnitOrRecover(ctx, exited, unit, truth, 'verify-complete', signals);
1243
1289
  }
1244
1290
  // The milestone poll FAILED (gh outage, missing binary) — unreachable is NOT
1245
1291
  // known-absent (decision 2, option A): stall and advance decisions pause for
@@ -1365,11 +1411,13 @@ function recordDispatchOutcome(ctx, state, unit, slot, suspect) {
1365
1411
  * event.
1366
1412
  */
1367
1413
  function completeUnitOrRecover(ctx, state, unit, truth, via,
1368
- // #591: the last tool this dispatch called (from `recordDispatchRunLog`'s dead-pid
1369
- // reading), threaded through to `enterRecovery`'s `unverified-exit` evidence. The
1370
- // `external-advance` caller passes none — it DOES record a run log (`reconcileRunning`,
1371
- // #524), but ground truth already confirmed completion, so this path never reaches the
1372
- // `unverified-exit` branch and the tool name has nothing to attribute.
1414
+ // #591/#629: the last tool called AND whether the exit was a confirmed
1415
+ // provider API error (from `recordDispatchRunLog`'s dead-pid reading),
1416
+ // threaded through to the unverified-exit decision below. The
1417
+ // `external-advance` caller passes none — it DOES record a run log
1418
+ // (`reconcileRunning`, #524), but ground truth already confirmed
1419
+ // completion, so this path never reaches that decision and neither signal
1420
+ // has anything to attribute.
1373
1421
  //
1374
1422
  // #620: a THUNK, not a value, for the deferred `exited`/`verifying` rail — that caller
1375
1423
  // has no read of its own to hand over and would otherwise have to read the dispatch log
@@ -1378,7 +1426,7 @@ function completeUnitOrRecover(ctx, state, unit, truth, via,
1378
1426
  // MAX_DISPATCH_LOG_BYTES (32 MiB) and its size is set by the spawned agent, so an
1379
1427
  // eager read is up to 32 MiB per slot per tick for as long as an outage holds slots in
1380
1428
  // `verifying`. Resolved once, only on the branch that consumes it.
1381
- lastTool = null) {
1429
+ dispatchSignals = NO_DISPATCH_SIGNALS) {
1382
1430
  const now = ctx.deps.now();
1383
1431
  let next = state;
1384
1432
  let slot = slotOf(next, unit);
@@ -1403,12 +1451,12 @@ lastTool = null) {
1403
1451
  // takes the unit (AC2's "never inferred from agent exit" cut both ways:
1404
1452
  // the park IS the milestone, the merge is not).
1405
1453
  if (entry !== undefined && entry.status === 'dispatched' && (0, groundtruth_1.isParkedMilestone)(truth.milestone)) {
1406
- // A verified park is also proof dispatch is healthy (#505) — reset the
1407
- // streak exactly like the sibling `completeUnit` branch below, or a
1408
- // healthy park sandwiched between two unrelated units' suspect exits
1409
- // would be invisible to the cross-unit correlation and could still tip
1410
- // it into a false-positive pause.
1411
- const parked = recordDispatchOutcome(ctx, next, unit, slot, false);
1454
+ // A verified park is also proof dispatch is healthy (#505/#629) — reset
1455
+ // both streaks exactly like the sibling `completeUnit` branch below, or a
1456
+ // healthy park sandwiched between two unrelated units' suspect/api-error
1457
+ // exits would be invisible to the correlation and could still tip it
1458
+ // into a false-positive pause.
1459
+ const parked = (0, dispatch_health_1.resetDispatchApiErrorStreak)(recordDispatchOutcome(ctx, next, unit, slot, false));
1412
1460
  const pr = (0, groundtruth_1.prOfMilestone)(truth.milestone); // non-null: isParkedMilestone guarantees it
1413
1461
  if (pr === null) {
1414
1462
  // Structurally unreachable, and deliberately not silent if it ever is:
@@ -1442,18 +1490,30 @@ lastTool = null) {
1442
1490
  const verifiedComplete = (0, groundtruth_1.isVerifiedComplete)(truth.milestone, closedSignal, slot.spawned_at);
1443
1491
  next = journalStaleMilestoneIfIgnored(ctx, next, unit, slot, truth, verifiedComplete, closedSignal);
1444
1492
  if (verifiedComplete) {
1445
- return completeUnit(ctx, recordDispatchOutcome(ctx, next, unit, slot, false), unit, via);
1493
+ const completed = (0, dispatch_health_1.resetDispatchApiErrorStreak)(recordDispatchOutcome(ctx, next, unit, slot, false));
1494
+ return completeUnit(ctx, completed, unit, via);
1495
+ }
1496
+ const resolved = typeof dispatchSignals === 'function' ? dispatchSignals() : dispatchSignals;
1497
+ // #629: a CONFIRMED provider API error is a dispatch failure, never an
1498
+ // unverified exit — it skips `recordDispatchOutcome`'s timing-based
1499
+ // suspect check entirely (the classification is deterministic, not a
1500
+ // heuristic) and never reaches `enterRecovery`'s escalation.
1501
+ if (resolved.apiError) {
1502
+ // `journalFailure: false` — `enterRecovery` journals its own `causeEvent`
1503
+ // (`'dispatch-failure'`) with this same evidence immediately below; without
1504
+ // the flag this rail double-journals every confirmed error (#629 review).
1505
+ const recorded = (0, dispatch_health_1.recordDispatchApiError)((event, u, extra) => journal(ctx, event, u, extra), next, unit, resolved.apiError, { journalFailure: false });
1506
+ return enterRecovery(ctx, recorded, unit, 'dispatch-failure', (0, dispatch_health_1.dispatchApiErrorDetail)(resolved.apiError), truth, (0, dispatch_health_1.dispatchApiErrorFields)(resolved.apiError), { escalate: false });
1446
1507
  }
1447
1508
  const suspect = msSinceLastProgress(slot, now) < types_1.SUSPECT_DISPATCH_WINDOW_MS;
1448
1509
  next = recordDispatchOutcome(ctx, next, unit, slot, suspect);
1449
- const resolvedLastTool = typeof lastTool === 'function' ? lastTool() : lastTool;
1450
1510
  return enterRecovery(ctx, next, unit, 'verify-incomplete', 'unverified-exit', truth, {
1451
1511
  observed: truth.milestone
1452
1512
  ? `milestone ${truth.milestone.phase}/${truth.milestone.status}; closed=${truth.closed}`
1453
1513
  : `no milestone; closed=${truth.closed}`,
1454
1514
  // #591: attributes the unverified exit to a concrete cause (e.g. `Monitor`)
1455
1515
  // without opening the transcript. Omitted when the log yielded no tool_use.
1456
- ...(resolvedLastTool !== null ? { last_tool: resolvedLastTool } : {}),
1516
+ ...(resolved.lastTool !== null ? { last_tool: resolved.lastTool } : {}),
1457
1517
  });
1458
1518
  }
1459
1519
  /** Re-attach or spawn a slot left `assigned` by a crash between assign and spawn. */
@@ -1472,6 +1532,14 @@ function reconcileAssigned(ctx, state, slot, unit) {
1472
1532
  }
1473
1533
  /** Reconcile a `recovering` slot: respawn with the escalated tier. */
1474
1534
  function reconcileRecovering(ctx, state, unit) {
1535
+ // #629: while paused, do not resume a `recovering` slot's respawn — this is
1536
+ // the crash-recovery rail (a sched restart caught a slot between
1537
+ // `enterRecovery`'s transition and its own `spawnUnit` call, OR `enterRecovery`
1538
+ // itself deliberately parked an unescalated redispatch here, see its own
1539
+ // pause check). Either way, `sched resume` is what lets the tick loop reach
1540
+ // this function again and actually respawn.
1541
+ if (state.paused)
1542
+ return state;
1475
1543
  return spawnUnit(ctx, state, unit);
1476
1544
  }
1477
1545
  // --- Tick phases ---
@@ -1513,7 +1581,7 @@ function reconcileSlots(ctx, state, polled) {
1513
1581
  // whatever `reconcileRunning` saw. Passed as a thunk so the read
1514
1582
  // happens only on the tick that actually reaches the unverified-exit
1515
1583
  // decision, not on every tick an outage holds the slot here.
1516
- next = completeUnitOrRecover(ctx, next, unit, truth, 'verify-complete', () => readLastToolForSlot(ctx, slot, unit));
1584
+ next = completeUnitOrRecover(ctx, next, unit, truth, 'verify-complete', () => readDispatchSignalsForSlot(ctx, slot, unit));
1517
1585
  break;
1518
1586
  case 'recovering':
1519
1587
  next = reconcileRecovering(ctx, next, unit);
@@ -2040,10 +2108,14 @@ function sleep(ms, shouldStop) {
2040
2108
  resolve();
2041
2109
  }
2042
2110
  }, Math.min(dispatch_1.STOP_POLL_MAX_MS, Math.max(dispatch_1.STOP_POLL_MIN_MS, Math.floor(ms / 10))));
2043
- if (typeof timer.unref === 'function')
2044
- timer.unref();
2045
- if (typeof stopCheck.unref === 'function')
2046
- stopCheck.unref();
2111
+ // NEVER unref these handles (#679). They are what holds the event loop
2112
+ // open between ticks: spawned agents are detached and unref'd on purpose
2113
+ // (dispatch.ts), so if the sleep timers are unref'd too, nothing keeps
2114
+ // the process alive once a tick's async work settles — the loop drains
2115
+ // and `sched start` exits cleanly after its first tick, silently
2116
+ // behaving like `--once`. Ref'd timers cost nothing here: SIGINT still
2117
+ // stops the engine promptly, because the ref'd stop-check polls
2118
+ // shouldStop() and resolves this sleep early.
2047
2119
  });
2048
2120
  }
2049
2121
  //# sourceMappingURL=engine.js.map