@cleocode/core 2026.10.20 → 2026.10.21-rc.ci.692

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.
Files changed (57) hide show
  1. package/dist/build-provenance.json +2 -2
  2. package/dist/cloud/http.d.ts +10 -0
  3. package/dist/cloud/http.d.ts.map +1 -1
  4. package/dist/cloud/http.js +8 -0
  5. package/dist/cloud/http.js.map +1 -1
  6. package/dist/cloud/nexus-vault.d.ts +20 -0
  7. package/dist/cloud/nexus-vault.d.ts.map +1 -1
  8. package/dist/cloud/nexus-vault.js +100 -15
  9. package/dist/cloud/nexus-vault.js.map +1 -1
  10. package/dist/docs/docs-ops.d.ts.map +1 -1
  11. package/dist/docs/docs-ops.js +7 -3
  12. package/dist/docs/docs-ops.js.map +1 -1
  13. package/dist/resources/admission-ledger.d.ts +1 -1
  14. package/dist/resources/admission-ledger.d.ts.map +1 -1
  15. package/dist/resources/admission-ledger.js +246 -104
  16. package/dist/resources/admission-ledger.js.map +1 -1
  17. package/dist/resources/run-governed.d.ts.map +1 -1
  18. package/dist/resources/run-governed.js +108 -25
  19. package/dist/resources/run-governed.js.map +1 -1
  20. package/dist/store/backup-recover.d.ts.map +1 -1
  21. package/dist/store/backup-recover.js +9 -7
  22. package/dist/store/backup-recover.js.map +1 -1
  23. package/dist/store/backup-verify.d.ts.map +1 -1
  24. package/dist/store/backup-verify.js +5 -2
  25. package/dist/store/backup-verify.js.map +1 -1
  26. package/dist/store/recover-brain-db.d.ts.map +1 -1
  27. package/dist/store/recover-brain-db.js +4 -2
  28. package/dist/store/recover-brain-db.js.map +1 -1
  29. package/dist/store/recover-malformed-db.d.ts +10 -0
  30. package/dist/store/recover-malformed-db.d.ts.map +1 -1
  31. package/dist/store/recover-malformed-db.js +16 -2
  32. package/dist/store/recover-malformed-db.js.map +1 -1
  33. package/dist/store/row-identity-registry.d.ts.map +1 -1
  34. package/dist/store/row-identity-registry.js +4 -8
  35. package/dist/store/row-identity-registry.js.map +1 -1
  36. package/dist/store/sqlite-backup.d.ts +4 -1
  37. package/dist/store/sqlite-backup.d.ts.map +1 -1
  38. package/dist/store/sqlite-backup.js +96 -72
  39. package/dist/store/sqlite-backup.js.map +1 -1
  40. package/dist/store/sync/apply/applier.d.ts.map +1 -1
  41. package/dist/store/sync/apply/applier.js +4 -0
  42. package/dist/store/sync/apply/applier.js.map +1 -1
  43. package/dist/store/sync/apply/frame.d.ts +7 -3
  44. package/dist/store/sync/apply/frame.d.ts.map +1 -1
  45. package/dist/store/sync/apply/frame.js +41 -2
  46. package/dist/store/sync/apply/frame.js.map +1 -1
  47. package/dist/system/backup.d.ts +3 -2
  48. package/dist/system/backup.d.ts.map +1 -1
  49. package/dist/system/backup.js +3 -2
  50. package/dist/system/backup.js.map +1 -1
  51. package/dist/tasks/affected-packages.d.ts.map +1 -1
  52. package/dist/tasks/affected-packages.js +1 -2
  53. package/dist/tasks/affected-packages.js.map +1 -1
  54. package/dist/tasks/gate-runner.d.ts.map +1 -1
  55. package/dist/tasks/gate-runner.js +41 -0
  56. package/dist/tasks/gate-runner.js.map +1 -1
  57. package/package.json +13 -13
@@ -374,7 +374,7 @@ function isCachedPressure(v) {
374
374
  typeof c.sampledAtMs === 'number' &&
375
375
  (c.reading === null || (typeof c.reading === 'object' && c.reading !== null)));
376
376
  }
377
- function readLedgerDoc(dir) {
377
+ function readLedgerDoc(dir, strictRead = false) {
378
378
  const empty = {
379
379
  version: LEDGER_VERSION,
380
380
  entries: [],
@@ -386,21 +386,36 @@ function readLedgerDoc(dir) {
386
386
  try {
387
387
  raw = readFileSync(ledgerFile(dir), 'utf-8');
388
388
  }
389
- catch {
389
+ catch (err) {
390
+ if (strictRead && err?.code !== 'ENOENT')
391
+ throw err;
390
392
  return empty;
391
393
  }
392
394
  let parsed;
393
395
  try {
394
396
  parsed = JSON.parse(raw);
395
397
  }
396
- catch {
398
+ catch (err) {
399
+ if (strictRead)
400
+ throw err;
397
401
  return { ...empty, corrupt: true };
398
402
  }
399
403
  if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
404
+ // @sync-invariant none:local-only admission cleanup must not replace an unreadable machine-local ledger
405
+ if (strictRead)
406
+ throw new SyntaxError('admission ledger is not a valid document');
400
407
  return { ...empty, corrupt: true };
401
408
  }
402
409
  const o = parsed;
403
410
  const version = typeof o.version === 'number' && Number.isFinite(o.version) ? o.version : LEDGER_VERSION;
411
+ if (strictRead && version > LEDGER_VERSION) {
412
+ // @sync-invariant none:local-only admission cleanup cannot interpret a newer machine-local ledger
413
+ throw new LedgerVersionError(version, ledgerFile(dir));
414
+ }
415
+ if (strictRead && !Array.isArray(o.entries)) {
416
+ // @sync-invariant none:local-only admission cleanup needs a complete machine-local entry list
417
+ throw new SyntaxError('admission ledger entries are not an array');
418
+ }
404
419
  const entries = [];
405
420
  const foreign = [];
406
421
  for (const v of Array.isArray(o.entries) ? o.entries : []) {
@@ -408,6 +423,10 @@ function readLedgerDoc(dir) {
408
423
  entries.push(v);
409
424
  else if (typeof v === 'object' && v !== null && !Array.isArray(v))
410
425
  foreign.push(v);
426
+ else if (strictRead) {
427
+ // @sync-invariant none:local-only admission cleanup cannot discard malformed machine-local entries
428
+ throw new SyntaxError('admission ledger contains an invalid entry');
429
+ }
411
430
  }
412
431
  return {
413
432
  version,
@@ -479,43 +498,80 @@ export class LedgerVersionError extends Error {
479
498
  }
480
499
  }
481
500
  /** Take the ledger lock, retrying only while another process holds it. */
482
- async function lockLedger(file) {
483
- for (let attempt = 1;; attempt++) {
484
- try {
485
- return await lockfile.lock(file, {
486
- lockfilePath: `${file}.lock`,
487
- realpath: false,
488
- stale: LEDGER_CRITICAL_STALE_MS,
489
- retries: 0,
490
- onCompromised: (err) => {
491
- log().warn({ err: err.message }, 'admission ledger lock compromised; continuing');
492
- },
493
- });
494
- }
495
- catch (err) {
496
- const code = err?.code;
497
- if (code !== 'ELOCKED' || attempt >= LOCK_ATTEMPTS)
498
- throw err;
499
- const backoff = Math.min(LOCK_BACKOFF_MS[1], LOCK_BACKOFF_MS[0] * 1.2 ** attempt);
500
- await new Promise((resolve) => {
501
- setTimeout(resolve, backoff * (1 + Math.random()));
502
- });
501
+ async function lockLedger(file, signal) {
502
+ signal?.throwIfAborted();
503
+ let onAbort = () => { };
504
+ const aborted = new Promise((_resolve, reject) => {
505
+ onAbort = () => reject(signal?.reason);
506
+ signal?.addEventListener('abort', onAbort, { once: true });
507
+ });
508
+ try {
509
+ for (let attempt = 1;; attempt++) {
510
+ try {
511
+ const pending = lockfile
512
+ .lock(file, {
513
+ lockfilePath: `${file}.lock`,
514
+ realpath: false,
515
+ stale: LEDGER_CRITICAL_STALE_MS,
516
+ retries: 0,
517
+ onCompromised: (err) => {
518
+ log().warn({ err: err.message }, 'admission ledger lock compromised; continuing');
519
+ },
520
+ })
521
+ .then((unlock) => {
522
+ if (signal?.aborted) {
523
+ void unlock().catch(() => { });
524
+ signal.throwIfAborted();
525
+ }
526
+ return unlock;
527
+ });
528
+ const unlock = await Promise.race([pending, aborted]);
529
+ if (signal?.aborted) {
530
+ void unlock().catch(() => { });
531
+ signal.throwIfAborted();
532
+ }
533
+ return unlock;
534
+ }
535
+ catch (err) {
536
+ signal?.throwIfAborted();
537
+ const code = err?.code;
538
+ if (code !== 'ELOCKED' || attempt >= LOCK_ATTEMPTS)
539
+ throw err;
540
+ const backoff = Math.min(LOCK_BACKOFF_MS[1], LOCK_BACKOFF_MS[0] * 1.2 ** attempt);
541
+ let timer;
542
+ try {
543
+ await Promise.race([
544
+ new Promise((resolve) => {
545
+ timer = setTimeout(resolve, backoff * (1 + Math.random()));
546
+ }),
547
+ aborted,
548
+ ]);
549
+ }
550
+ finally {
551
+ clearTimeout(timer);
552
+ }
553
+ }
503
554
  }
504
555
  }
556
+ finally {
557
+ signal?.removeEventListener('abort', onAbort);
558
+ }
505
559
  }
506
560
  /**
507
561
  * Run `fn` inside the ledger's critical section: read, decide, write. `fn`
508
562
  * must be synchronous and do no I/O of its own; its `doc` (or `null` for no
509
563
  * change) is written back before the lock is released. A file of a newer
510
564
  * format throws {@link LedgerVersionError} and is left untouched; a corrupt
511
- * file is moved aside before it is replaced.
565
+ * file is moved aside before it is replaced. Cleanup uses a strict read so
566
+ * unavailable or malformed state is retried without replacing the ledger.
512
567
  */
513
- async function withLedger(dir, fn) {
568
+ async function withLedger(dir, fn, signal, strictRead = false) {
514
569
  mkdirSync(dir, { recursive: true });
515
570
  const file = ledgerFile(dir);
516
- const unlock = await lockLedger(file);
571
+ const unlock = await lockLedger(file, signal);
517
572
  try {
518
- const current = readLedgerDoc(dir);
573
+ signal?.throwIfAborted();
574
+ const current = readLedgerDoc(dir, strictRead);
519
575
  if (current.version > LEDGER_VERSION) {
520
576
  // @sync-invariant none:local-only the admission ledger is a machine-local file, not a store write
521
577
  throw new LedgerVersionError(current.version, file);
@@ -1399,7 +1455,10 @@ async function admitInner(req, opts) {
1399
1455
  // single-process grant a bigger run queues for itself (T13452), naming the
1400
1456
  // parent: schedulePass never makes it wait on its own ancestors.
1401
1457
  const parent = enclosingGrant(readLedger(dir), pid, env, opts.facts);
1402
- if (parent !== null && (isHeavy(parent) || parent.footprintBytes >= req.footprintBytes)) {
1458
+ if (parent !== null &&
1459
+ (isHeavy(parent) || parent.footprintBytes >= req.footprintBytes) &&
1460
+ !opts.signal?.aborted &&
1461
+ (!opts.wait || timeoutMs <= 0 || now() - t0 < timeoutMs)) {
1403
1462
  return { admitted: true, grant: passThrough(admissionToken(parent), true, null) };
1404
1463
  }
1405
1464
  const entry = {
@@ -1423,13 +1482,45 @@ async function admitInner(req, opts) {
1423
1482
  ...(req.footprintReason !== undefined ? { footprintReason: req.footprintReason } : {}),
1424
1483
  ...(req.exclusive === true ? { exclusive: true } : {}),
1425
1484
  };
1485
+ const deadline = t0 + timeoutMs;
1486
+ const stop = new AbortController();
1487
+ const onAbort = () => stop.abort();
1488
+ opts.signal?.addEventListener('abort', onAbort, { once: true });
1489
+ if (opts.signal?.aborted)
1490
+ stop.abort();
1491
+ let timer;
1492
+ const expire = () => {
1493
+ const remaining = deadline - now();
1494
+ if (remaining <= 0)
1495
+ stop.abort();
1496
+ else
1497
+ timer = setTimeout(expire, Math.min(2_147_483_647, remaining));
1498
+ };
1499
+ // Zero is the existing one-pass contract used by captured gate executions.
1500
+ if (opts.wait && timeoutMs > 0)
1501
+ expire();
1502
+ const checkDeadline = () => {
1503
+ if (opts.wait && timeoutMs > 0 && now() >= deadline)
1504
+ stop.abort();
1505
+ };
1506
+ const checkQueue = () => {
1507
+ checkDeadline();
1508
+ stop.signal.throwIfAborted();
1509
+ };
1510
+ let lastDoc = readLedgerDoc(dir);
1511
+ let lastReading = null;
1426
1512
  /** One pass: reap, (re-)enqueue ourselves, schedule; returns our entry's state. */
1427
1513
  const pass = async () => {
1428
1514
  const before = readLedgerDoc(dir);
1515
+ lastDoc = before;
1429
1516
  const nowMs = now();
1430
1517
  const { share, lightShare, reading, sampled } = await sampleShare(opts, before.pressure, nowMs);
1431
- const dead = findDead(before, nowMs, probe);
1518
+ lastReading = reading;
1519
+ checkQueue();
1520
+ const dead = findDead(before, now(), probe);
1432
1521
  return withLedger(dir, (doc) => {
1522
+ checkQueue();
1523
+ const nowMs = now();
1433
1524
  let entries = doc.entries;
1434
1525
  const at = entries.findIndex((e) => e.id === entry.id);
1435
1526
  if (at < 0)
@@ -1438,11 +1529,12 @@ async function admitInner(req, opts) {
1438
1529
  entries = entries.map((e) => (e.id === entry.id ? { ...e, heartbeatAtMs: nowMs } : e));
1439
1530
  }
1440
1531
  const next = reapAndSchedule({ ...doc, entries }, dead, { capacityBytes, share, lightShare, nowMs }, sampled);
1532
+ lastDoc = next;
1441
1533
  return {
1442
1534
  doc: next,
1443
1535
  result: { mine: next.entries.find((e) => e.id === entry.id) ?? null, doc: next, reading },
1444
1536
  };
1445
- });
1537
+ }, stop.signal);
1446
1538
  };
1447
1539
  const leave = async () => withLedger(dir, (doc) => {
1448
1540
  const mine = doc.entries.find((e) => e.id === entry.id) ?? null;
@@ -1476,56 +1568,87 @@ async function admitInner(req, opts) {
1476
1568
  holders: describeLoad(doc, nowMs),
1477
1569
  };
1478
1570
  };
1479
- let first = await pass();
1480
- if (first.mine?.state === 'admitted') {
1481
- return { admitted: true, grant: holdGrant(first.mine, dir, now() - t0, opts) };
1482
- }
1483
- if (!opts.wait) {
1484
- const raced = await leave();
1485
- if (raced !== null)
1486
- return { admitted: true, grant: holdGrant(raced, dir, now() - t0, opts) };
1487
- return { admitted: false, refusal: refusal(first.doc, first.reading) };
1488
- }
1489
- let lastPassAt = now();
1490
- let lastReportAt = t0;
1491
- let lastKeepAliveAt = Number.NEGATIVE_INFINITY;
1492
- let reading = first.reading;
1493
- for (;;) {
1494
- if (opts.keepAlive && now() - lastKeepAliveAt >= (opts.keepAliveMs ?? 300_000)) {
1495
- lastKeepAliveAt = now();
1496
- await opts.keepAlive().catch(() => undefined);
1571
+ const attempt = async () => {
1572
+ checkQueue();
1573
+ let first = await pass();
1574
+ checkQueue();
1575
+ if (first.mine?.state === 'admitted') {
1576
+ return { admitted: true, grant: holdGrant(first.mine, dir, now() - t0, opts) };
1497
1577
  }
1498
- if (reading !== null)
1499
- opts.memoryPressure?.waiting(reading, now() - t0);
1500
- if (now() - lastReportAt >= LEDGER_HOLDER_REPORT_MS && opts.notice) {
1501
- lastReportAt = now();
1502
- opts.notice(holderReport(first.doc, entry, pid, now() - t0, capacityBytes, now()));
1503
- }
1504
- const remaining = timeoutMs - (now() - t0);
1505
- if (remaining <= 0 || opts.signal?.aborted === true) {
1578
+ if (!opts.wait) {
1506
1579
  const raced = await leave();
1580
+ checkQueue();
1507
1581
  if (raced !== null)
1508
1582
  return { admitted: true, grant: holdGrant(raced, dir, now() - t0, opts) };
1509
- return { admitted: false, refusal: refusal(first.doc, reading) };
1510
- }
1511
- await sleep(Math.min(opts.pollMs ?? READ_POLL_MS, remaining));
1512
- const seen = readLedgerDoc(dir);
1513
- const mine = seen.entries.find((e) => e.id === entry.id);
1514
- if (mine?.state === 'admitted') {
1515
- opts.memoryPressure?.admitted(now() - t0, null);
1516
- return { admitted: true, grant: holdGrant(mine, dir, now() - t0, opts) };
1583
+ return { admitted: false, refusal: refusal(first.doc, first.reading) };
1517
1584
  }
1518
- if (seen.pressure !== null && isFresh(seen.pressure, now()))
1519
- reading = seen.pressure.reading;
1520
- if (passDue(mine === undefined, now() - lastPassAt, seen.pressure, now(), opts)) {
1521
- lastPassAt = now();
1522
- first = await pass();
1523
- reading = first.reading;
1524
- if (first.mine?.state === 'admitted') {
1585
+ let lastPassAt = now();
1586
+ let lastReportAt = t0;
1587
+ let lastKeepAliveAt = Number.NEGATIVE_INFINITY;
1588
+ let reading = first.reading;
1589
+ for (;;) {
1590
+ if (opts.keepAlive && now() - lastKeepAliveAt >= (opts.keepAliveMs ?? 300_000)) {
1591
+ lastKeepAliveAt = now();
1592
+ await opts.keepAlive().catch(() => undefined);
1593
+ checkQueue();
1594
+ }
1595
+ if (reading !== null)
1596
+ opts.memoryPressure?.waiting(reading, now() - t0);
1597
+ if (now() - lastReportAt >= LEDGER_HOLDER_REPORT_MS && opts.notice) {
1598
+ lastReportAt = now();
1599
+ opts.notice(holderReport(first.doc, entry, pid, now() - t0, capacityBytes, now()));
1600
+ }
1601
+ const remaining = timeoutMs - (now() - t0);
1602
+ if (remaining <= 0)
1603
+ stop.abort();
1604
+ checkQueue();
1605
+ await sleep(Math.min(opts.pollMs ?? READ_POLL_MS, remaining));
1606
+ checkQueue();
1607
+ const seen = readLedgerDoc(dir);
1608
+ const mine = seen.entries.find((e) => e.id === entry.id);
1609
+ if (mine?.state === 'admitted') {
1525
1610
  opts.memoryPressure?.admitted(now() - t0, null);
1526
- return { admitted: true, grant: holdGrant(first.mine, dir, now() - t0, opts) };
1611
+ checkQueue();
1612
+ return { admitted: true, grant: holdGrant(mine, dir, now() - t0, opts) };
1613
+ }
1614
+ if (seen.pressure !== null && isFresh(seen.pressure, now()))
1615
+ reading = seen.pressure.reading;
1616
+ if (passDue(mine === undefined, now() - lastPassAt, seen.pressure, now(), opts)) {
1617
+ lastPassAt = now();
1618
+ first = await pass();
1619
+ checkQueue();
1620
+ reading = first.reading;
1621
+ if (first.mine?.state === 'admitted') {
1622
+ opts.memoryPressure?.admitted(now() - t0, null);
1623
+ checkQueue();
1624
+ return { admitted: true, grant: holdGrant(first.mine, dir, now() - t0, opts) };
1625
+ }
1527
1626
  }
1528
1627
  }
1628
+ };
1629
+ const aborted = new Promise((_resolve, reject) => {
1630
+ const rejectAbort = () => reject(stop.signal.reason);
1631
+ if (stop.signal.aborted)
1632
+ rejectAbort();
1633
+ else
1634
+ stop.signal.addEventListener('abort', rejectAbort, { once: true });
1635
+ });
1636
+ try {
1637
+ return await Promise.race([attempt(), aborted]);
1638
+ }
1639
+ catch (err) {
1640
+ checkDeadline();
1641
+ if (!stop.signal.aborted)
1642
+ throw err;
1643
+ return { admitted: false, refusal: refusal(lastDoc, lastReading) };
1644
+ }
1645
+ finally {
1646
+ clearTimeout(timer);
1647
+ opts.signal?.removeEventListener('abort', onAbort);
1648
+ if (stop.signal.aborted) {
1649
+ // A bounded deferral does not prove removal while the ledger is unavailable.
1650
+ void releaseLedgerEntry(entry, dir, { ...opts, signal: stop.signal });
1651
+ }
1529
1652
  }
1530
1653
  }
1531
1654
  /**
@@ -1576,46 +1699,41 @@ function holderReport(doc, mine, pid, waitedMs, capacityBytes, nowMs) {
1576
1699
  }
1577
1700
  /** First and longest delay between attempts to remove a released entry that failed. */
1578
1701
  const RELEASE_RETRY_MS = [1_000, LEDGER_HEARTBEAT_MS];
1579
- /** Hold an admitted entry: heartbeat, follow tool groups, release. */
1580
- function holdGrant(entry, dir, waitedMs, opts) {
1581
- const update = (patch) => withLedger(dir, (doc) => {
1582
- if (!doc.entries.some((e) => e.id === entry.id))
1583
- return { doc: null, result: undefined };
1584
- return {
1585
- doc: { ...doc, entries: doc.entries.map((e) => (e.id === entry.id ? patch(e) : e)) },
1586
- result: undefined,
1587
- };
1588
- }).catch(() => {
1589
- // Best effort: a missed heartbeat only means the next probe asks ps.
1590
- });
1702
+ /** Retire an admission, retrying unavailable storage without keeping the runner alive. */
1703
+ async function releaseLedgerEntry(entry, dir, opts) {
1591
1704
  const now = opts.now ?? Date.now;
1592
- const timer = setInterval(() => {
1593
- void update((e) => ({ ...e, heartbeatAtMs: now() }));
1594
- }, LEDGER_HEARTBEAT_MS);
1595
- timer.unref();
1596
- const following = followToolGroups((groups) => {
1597
- void update((e) => ({ ...e, toolGroups: [...groups] }));
1598
- });
1599
- if (following.groups.length > 0)
1600
- void update((e) => ({ ...e, toolGroups: [...following.groups] }));
1601
- /** Remove the entry and schedule the queue. True when done (or never possible here). */
1705
+ /** Remove the entry and schedule the queue. Only a successful read can establish absence. */
1602
1706
  const removeOnce = async () => {
1603
1707
  try {
1604
- const before = readLedgerDoc(dir);
1708
+ const before = readLedgerDoc(dir, true);
1709
+ if (!before.entries.some((e) => e.id === entry.id) &&
1710
+ !before.foreign.some((e) => e.id === entry.id))
1711
+ return true;
1712
+ if (opts.signal?.aborted) {
1713
+ await withLedger(dir, (doc) => ({
1714
+ doc: {
1715
+ ...doc,
1716
+ entries: doc.entries.filter((e) => e.id !== entry.id),
1717
+ foreign: doc.foreign.filter((e) => e.id !== entry.id),
1718
+ },
1719
+ result: undefined,
1720
+ }), undefined, true);
1721
+ return true;
1722
+ }
1605
1723
  const nowMs = now();
1606
1724
  const { share, lightShare, sampled } = await sampleShare(opts, before.pressure, nowMs);
1607
1725
  const capacityBytes = opts.capacityBytes ?? admissionCapacityBytes();
1608
1726
  await withLedger(dir, (doc) => ({
1609
- doc: reapAndSchedule({ ...doc, entries: doc.entries.filter((e) => e.id !== entry.id) }, NO_DEAD, { capacityBytes, share, lightShare, nowMs }, sampled),
1727
+ doc: reapAndSchedule({
1728
+ ...doc,
1729
+ entries: doc.entries.filter((e) => e.id !== entry.id),
1730
+ foreign: doc.foreign.filter((e) => e.id !== entry.id),
1731
+ }, NO_DEAD, { capacityBytes, share, lightShare, nowMs }, sampled),
1610
1732
  result: undefined,
1611
- }));
1733
+ }), undefined, true);
1612
1734
  return true;
1613
1735
  }
1614
1736
  catch (err) {
1615
- // State that cannot be written cannot be cleaned either: the entry is
1616
- // reaped once this process is gone.
1617
- if (admissionIoError(err) !== null)
1618
- return true;
1619
1737
  log().warn({ err: err instanceof Error ? err.message : String(err), id: entry.id }, 'admission release failed; retrying in the background');
1620
1738
  return false;
1621
1739
  }
@@ -1630,6 +1748,31 @@ function holdGrant(entry, dir, waitedMs, opts) {
1630
1748
  }, delayMs);
1631
1749
  t.unref();
1632
1750
  };
1751
+ if (!(await removeOnce()))
1752
+ retry(RELEASE_RETRY_MS[0]);
1753
+ }
1754
+ /** Hold an admitted entry: heartbeat, follow tool groups, release. */
1755
+ function holdGrant(entry, dir, waitedMs, opts) {
1756
+ const update = (patch) => withLedger(dir, (doc) => {
1757
+ if (!doc.entries.some((e) => e.id === entry.id))
1758
+ return { doc: null, result: undefined };
1759
+ return {
1760
+ doc: { ...doc, entries: doc.entries.map((e) => (e.id === entry.id ? patch(e) : e)) },
1761
+ result: undefined,
1762
+ };
1763
+ }).catch(() => {
1764
+ // Best effort: a missed heartbeat only means the next probe asks ps.
1765
+ });
1766
+ const now = opts.now ?? Date.now;
1767
+ const timer = setInterval(() => {
1768
+ void update((e) => ({ ...e, heartbeatAtMs: now() }));
1769
+ }, LEDGER_HEARTBEAT_MS);
1770
+ timer.unref();
1771
+ const following = followToolGroups((groups) => {
1772
+ void update((e) => ({ ...e, toolGroups: [...groups] }));
1773
+ });
1774
+ if (following.groups.length > 0)
1775
+ void update((e) => ({ ...e, toolGroups: [...following.groups] }));
1633
1776
  let released = false;
1634
1777
  return {
1635
1778
  id: entry.id,
@@ -1644,8 +1787,7 @@ function holdGrant(entry, dir, waitedMs, opts) {
1644
1787
  released = true;
1645
1788
  clearInterval(timer);
1646
1789
  following.stop();
1647
- if (!(await removeOnce()))
1648
- retry(RELEASE_RETRY_MS[0]);
1790
+ await releaseLedgerEntry(entry, dir, opts);
1649
1791
  },
1650
1792
  };
1651
1793
  }