@cleocode/core 2026.10.25 → 2026.10.26

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 (198) hide show
  1. package/dist/build-provenance.json +2 -2
  2. package/dist/dispatch/contracts/output-contracts.d.ts +13 -0
  3. package/dist/dispatch/contracts/output-contracts.d.ts.map +1 -1
  4. package/dist/dispatch/contracts/output-contracts.js +29 -0
  5. package/dist/dispatch/contracts/output-contracts.js.map +1 -1
  6. package/dist/docs/docs-ops.d.ts +8 -0
  7. package/dist/docs/docs-ops.d.ts.map +1 -1
  8. package/dist/docs/docs-ops.js +24 -1
  9. package/dist/docs/docs-ops.js.map +1 -1
  10. package/dist/docs/docs-update.d.ts +8 -5
  11. package/dist/docs/docs-update.d.ts.map +1 -1
  12. package/dist/docs/docs-update.js +16 -5
  13. package/dist/docs/docs-update.js.map +1 -1
  14. package/dist/gateway-client/generated/types.gen.d.ts +27 -0
  15. package/dist/gateway-client/generated/types.gen.d.ts.map +1 -1
  16. package/dist/hooks/handlers/watchdog-hooks.d.ts.map +1 -1
  17. package/dist/hooks/handlers/watchdog-hooks.js +5 -0
  18. package/dist/hooks/handlers/watchdog-hooks.js.map +1 -1
  19. package/dist/internal.d.ts +3 -2
  20. package/dist/internal.d.ts.map +1 -1
  21. package/dist/internal.js +5 -2
  22. package/dist/internal.js.map +1 -1
  23. package/dist/lifecycle/verification/satisfies-validator.d.ts.map +1 -1
  24. package/dist/lifecycle/verification/satisfies-validator.js +22 -10
  25. package/dist/lifecycle/verification/satisfies-validator.js.map +1 -1
  26. package/dist/memory/brain-writer-thread.d.ts +30 -0
  27. package/dist/memory/brain-writer-thread.d.ts.map +1 -1
  28. package/dist/memory/brain-writer-thread.js +46 -1
  29. package/dist/memory/brain-writer-thread.js.map +1 -1
  30. package/dist/memory/brain-writer-worker.js +13 -2
  31. package/dist/memory/brain-writer-worker.js.map +1 -1
  32. package/dist/memory/embedding-backfill.d.ts +12 -0
  33. package/dist/memory/embedding-backfill.d.ts.map +1 -1
  34. package/dist/memory/embedding-backfill.js +51 -31
  35. package/dist/memory/embedding-backfill.js.map +1 -1
  36. package/dist/orchestrate/include-deps.d.ts +58 -0
  37. package/dist/orchestrate/include-deps.d.ts.map +1 -0
  38. package/dist/orchestrate/include-deps.js +138 -0
  39. package/dist/orchestrate/include-deps.js.map +1 -0
  40. package/dist/orchestrate/spawn-ops.d.ts +14 -33
  41. package/dist/orchestrate/spawn-ops.d.ts.map +1 -1
  42. package/dist/orchestrate/spawn-ops.js +49 -61
  43. package/dist/orchestrate/spawn-ops.js.map +1 -1
  44. package/dist/orchestrate/worktree-complete.d.ts +15 -1
  45. package/dist/orchestrate/worktree-complete.d.ts.map +1 -1
  46. package/dist/orchestrate/worktree-complete.js +36 -3
  47. package/dist/orchestrate/worktree-complete.js.map +1 -1
  48. package/dist/release/ci-evidence.d.ts +142 -4
  49. package/dist/release/ci-evidence.d.ts.map +1 -1
  50. package/dist/release/ci-evidence.js +298 -48
  51. package/dist/release/ci-evidence.js.map +1 -1
  52. package/dist/release/open.d.ts +25 -1
  53. package/dist/release/open.d.ts.map +1 -1
  54. package/dist/release/open.js +63 -11
  55. package/dist/release/open.js.map +1 -1
  56. package/dist/resources/admission-ledger.d.ts +83 -11
  57. package/dist/resources/admission-ledger.d.ts.map +1 -1
  58. package/dist/resources/admission-ledger.js +138 -39
  59. package/dist/resources/admission-ledger.js.map +1 -1
  60. package/dist/resources/governor.js +1 -1
  61. package/dist/resources/governor.js.map +1 -1
  62. package/dist/resources/hook-footprint.d.ts +24 -0
  63. package/dist/resources/hook-footprint.d.ts.map +1 -1
  64. package/dist/resources/hook-footprint.js +38 -1
  65. package/dist/resources/hook-footprint.js.map +1 -1
  66. package/dist/resources/run-class.d.ts +38 -5
  67. package/dist/resources/run-class.d.ts.map +1 -1
  68. package/dist/resources/run-class.js +60 -11
  69. package/dist/resources/run-class.js.map +1 -1
  70. package/dist/resources/run-governed.d.ts +13 -3
  71. package/dist/resources/run-governed.d.ts.map +1 -1
  72. package/dist/resources/run-governed.js +42 -9
  73. package/dist/resources/run-governed.js.map +1 -1
  74. package/dist/resources/slot-holder.d.ts +3 -3
  75. package/dist/resources/slot-holder.js +3 -3
  76. package/dist/resources/tool-groups.d.ts +11 -7
  77. package/dist/resources/tool-groups.d.ts.map +1 -1
  78. package/dist/resources/tool-groups.js +24 -13
  79. package/dist/resources/tool-groups.js.map +1 -1
  80. package/dist/spawn/branch-integration.d.ts +281 -0
  81. package/dist/spawn/branch-integration.d.ts.map +1 -0
  82. package/dist/spawn/branch-integration.js +715 -0
  83. package/dist/spawn/branch-integration.js.map +1 -0
  84. package/dist/spawn/branch-lock.d.ts +32 -270
  85. package/dist/spawn/branch-lock.d.ts.map +1 -1
  86. package/dist/spawn/branch-lock.js +59 -770
  87. package/dist/spawn/branch-lock.js.map +1 -1
  88. package/dist/spawn/index.d.ts +3 -1
  89. package/dist/spawn/index.d.ts.map +1 -1
  90. package/dist/spawn/index.js +5 -1
  91. package/dist/spawn/index.js.map +1 -1
  92. package/dist/store/exodus/recovery.d.ts.map +1 -1
  93. package/dist/store/exodus/recovery.js +9 -4
  94. package/dist/store/exodus/recovery.js.map +1 -1
  95. package/dist/system/health.d.ts.map +1 -1
  96. package/dist/system/health.js +3 -1
  97. package/dist/system/health.js.map +1 -1
  98. package/dist/tasks/ac-coverage-gate.d.ts +18 -2
  99. package/dist/tasks/ac-coverage-gate.d.ts.map +1 -1
  100. package/dist/tasks/ac-coverage-gate.js +40 -6
  101. package/dist/tasks/ac-coverage-gate.js.map +1 -1
  102. package/dist/tasks/ac-table.d.ts.map +1 -1
  103. package/dist/tasks/ac-table.js +3 -10
  104. package/dist/tasks/ac-table.js.map +1 -1
  105. package/dist/tasks/affected-packages.d.ts.map +1 -1
  106. package/dist/tasks/affected-packages.js +54 -10
  107. package/dist/tasks/affected-packages.js.map +1 -1
  108. package/dist/tasks/affected-scope.d.ts +10 -1
  109. package/dist/tasks/affected-scope.d.ts.map +1 -1
  110. package/dist/tasks/affected-scope.js +13 -3
  111. package/dist/tasks/affected-scope.js.map +1 -1
  112. package/dist/tasks/change-set.d.ts +9 -6
  113. package/dist/tasks/change-set.d.ts.map +1 -1
  114. package/dist/tasks/change-set.js +75 -97
  115. package/dist/tasks/change-set.js.map +1 -1
  116. package/dist/tasks/command-line.d.ts.map +1 -1
  117. package/dist/tasks/command-line.js +6 -2
  118. package/dist/tasks/command-line.js.map +1 -1
  119. package/dist/tasks/complete.d.ts.map +1 -1
  120. package/dist/tasks/complete.js +7 -2
  121. package/dist/tasks/complete.js.map +1 -1
  122. package/dist/tasks/done-plan.d.ts.map +1 -1
  123. package/dist/tasks/done-plan.js +111 -29
  124. package/dist/tasks/done-plan.js.map +1 -1
  125. package/dist/tasks/done-record.d.ts +14 -0
  126. package/dist/tasks/done-record.d.ts.map +1 -1
  127. package/dist/tasks/done-record.js +17 -10
  128. package/dist/tasks/done-record.js.map +1 -1
  129. package/dist/tasks/evidence-tree.d.ts +33 -0
  130. package/dist/tasks/evidence-tree.d.ts.map +1 -0
  131. package/dist/tasks/evidence-tree.js +132 -0
  132. package/dist/tasks/evidence-tree.js.map +1 -0
  133. package/dist/tasks/evidence.d.ts +42 -6
  134. package/dist/tasks/evidence.d.ts.map +1 -1
  135. package/dist/tasks/evidence.js +175 -79
  136. package/dist/tasks/evidence.js.map +1 -1
  137. package/dist/tasks/gate-preview.d.ts.map +1 -1
  138. package/dist/tasks/gate-preview.js +3 -2
  139. package/dist/tasks/gate-preview.js.map +1 -1
  140. package/dist/tasks/gate-runner.d.ts +11 -3
  141. package/dist/tasks/gate-runner.d.ts.map +1 -1
  142. package/dist/tasks/gate-runner.js +156 -37
  143. package/dist/tasks/gate-runner.js.map +1 -1
  144. package/dist/tasks/gate-staleness.d.ts +58 -0
  145. package/dist/tasks/gate-staleness.d.ts.map +1 -0
  146. package/dist/tasks/gate-staleness.js +78 -0
  147. package/dist/tasks/gate-staleness.js.map +1 -0
  148. package/dist/tasks/heavy-tool-env.d.ts +26 -1
  149. package/dist/tasks/heavy-tool-env.d.ts.map +1 -1
  150. package/dist/tasks/heavy-tool-env.js +92 -5
  151. package/dist/tasks/heavy-tool-env.js.map +1 -1
  152. package/dist/tasks/heavy-tool-limit.d.ts +24 -0
  153. package/dist/tasks/heavy-tool-limit.d.ts.map +1 -1
  154. package/dist/tasks/heavy-tool-limit.js +44 -1
  155. package/dist/tasks/heavy-tool-limit.js.map +1 -1
  156. package/dist/tasks/show.d.ts.map +1 -1
  157. package/dist/tasks/show.js +43 -0
  158. package/dist/tasks/show.js.map +1 -1
  159. package/dist/tasks/test-run-binding.d.ts +48 -3
  160. package/dist/tasks/test-run-binding.d.ts.map +1 -1
  161. package/dist/tasks/test-run-binding.js +111 -3
  162. package/dist/tasks/test-run-binding.js.map +1 -1
  163. package/dist/tasks/tool-cache.d.ts +58 -7
  164. package/dist/tasks/tool-cache.d.ts.map +1 -1
  165. package/dist/tasks/tool-cache.js +216 -16
  166. package/dist/tasks/tool-cache.js.map +1 -1
  167. package/dist/tasks/tool-resolver.d.ts +13 -0
  168. package/dist/tasks/tool-resolver.d.ts.map +1 -1
  169. package/dist/tasks/tool-resolver.js +34 -0
  170. package/dist/tasks/tool-resolver.js.map +1 -1
  171. package/dist/tasks/tool-runner-guard.d.ts +6 -0
  172. package/dist/tasks/tool-runner-guard.d.ts.map +1 -1
  173. package/dist/tasks/tool-runner-guard.js.map +1 -1
  174. package/dist/tasks/tool-semaphore.js +1 -1
  175. package/dist/tasks/tool-semaphore.js.map +1 -1
  176. package/dist/tasks/verification-policy.d.ts +26 -3
  177. package/dist/tasks/verification-policy.d.ts.map +1 -1
  178. package/dist/tasks/verification-policy.js +76 -11
  179. package/dist/tasks/verification-policy.js.map +1 -1
  180. package/dist/validation/doctor/checks.d.ts +12 -0
  181. package/dist/validation/doctor/checks.d.ts.map +1 -1
  182. package/dist/validation/doctor/checks.js +33 -0
  183. package/dist/validation/doctor/checks.js.map +1 -1
  184. package/dist/validation/engine-ops.d.ts +6 -0
  185. package/dist/validation/engine-ops.d.ts.map +1 -1
  186. package/dist/validation/engine-ops.js +51 -1
  187. package/dist/validation/engine-ops.js.map +1 -1
  188. package/dist/worktree/force-unlock.js +2 -2
  189. package/dist/worktree/force-unlock.js.map +1 -1
  190. package/dist/worktree/list.d.ts +7 -7
  191. package/dist/worktree/list.d.ts.map +1 -1
  192. package/dist/worktree/list.js +11 -56
  193. package/dist/worktree/list.js.map +1 -1
  194. package/dist/worktree/prune.d.ts +2 -2
  195. package/dist/worktree/prune.d.ts.map +1 -1
  196. package/dist/worktree/prune.js +4 -4
  197. package/dist/worktree/prune.js.map +1 -1
  198. package/package.json +13 -13
@@ -74,12 +74,12 @@ import { join } from 'node:path';
74
74
  import lockfile from 'proper-lockfile';
75
75
  import { getLogger } from '../logger.js';
76
76
  import { getCleoHome } from '../paths.js';
77
- import { admissionCapacityBytes, admissionReserveBytes, defaultSingleProcessHeapMb, GIB_PER_WORKER, HEAVY_TOOL_HEAP_MB, heavyToolWorkers, isHeavyTool, isMemoryBoundTool, planHeavyToolEnv, vitestWorkerCeiling, } from '../tasks/heavy-tool-env.js';
78
- import { gitHookPath, HOOK_PEAK_MARGIN, hookFootprintKey, readHookPeak } from './hook-footprint.js';
77
+ import { admissionCapacityBytes, admissionReserveBytes, defaultSingleProcessHeapMb, GIB_PER_WORKER, HEAVY_TOOL_HEAP_MB, heavyToolWorkers, isHeavyTool, isMemoryBoundTool, planHeavyToolEnv, projectSettingOverrides, vitestWorkerCeiling, } from '../tasks/heavy-tool-env.js';
78
+ import { gitHookPath, HOOK_MIN_RUNS, HOOK_PEAK_MARGIN, hookFootprintKey, readHookPeak, scriptFootprint, } from './hook-footprint.js';
79
79
  import { pressureScore, ResourceMonitor } from './monitor.js';
80
80
  import { checkMemoryGate, MEMORY_GATE_RETRY_AFTER_MS, } from './pressure-gate.js';
81
81
  import { processAncestors, processGroupOf, processGroupsOf } from './run-admission.js';
82
- import { canonicalForClass, gitHookFor, lightCommandReason, namedTestFileCount, runFootprint, } from './run-class.js';
82
+ import { canonicalForClass, gitHookFor, heavyScriptReason, lightCommandReason, namedTestFileCount, nodeScriptOf, runFootprint, } from './run-class.js';
83
83
  import { ownProcessStartedAt, systemPidProbe } from './slot-holder.js';
84
84
  import { followToolGroups, isProbeableId } from './tool-groups.js';
85
85
  import { memoryPressureCause, TOP_MEMORY_COUNT, TOP_MEMORY_FAILED_REUSE_MS, TOP_MEMORY_REUSE_MS, topMemoryConsumers, } from './top-memory.js';
@@ -244,7 +244,10 @@ function scopedBuildWorkers(totalRamGib, env) {
244
244
  * A formatter on named files is charged {@link LIGHT_FOOTPRINT_BYTES} with no
245
245
  * heap plan. A single Node linter on files, or a one-project `tsc -p`, is
246
246
  * charged one process. A scoped build of a command that runs no heavy tool
247
- * (`node x.mjs`, a hook-free `git push`) is charged light (T13452). Anything
247
+ * (`node x.mjs`, a hook-free `git push`) is charged light (T13452), unless
248
+ * the script hosts a heavy tool: its path names a heavy job or it loads a
249
+ * compiler or test runner (T14050). Such a script, like a hook, is charged its
250
+ * measured peak once measured, and the class plan until then. Anything
248
251
  * else gets the class's heavy-run plan, with a named-file test run capped at
249
252
  * one worker per file (T13132) and a scoped build at half a heavy run's
250
253
  * workers (T13759). A test run is also capped at the project's own vitest
@@ -259,6 +262,8 @@ function scopedBuildWorkers(totalRamGib, env) {
259
262
  * @param hookPeak - a hook's measured peak. @defaultValue reads the hook footprint store
260
263
  * @param cwd - the directory the command runs in, read for the project's vitest
261
264
  * limit (T14033). @defaultValue process.cwd()
265
+ * @param scriptPeak - a `node` script's key, loaded heavy tool and measured
266
+ * peak (T14050). @defaultValue reads the script and the footprint store
262
267
  *
263
268
  * @example
264
269
  * ```ts
@@ -269,18 +274,37 @@ export function planRunFootprint(cls, argv, env = process.env, totalRamGib = tot
269
274
  hookPeak = (hook, globals) => {
270
275
  const key = hookFootprintKey(process.cwd(), hook, globals, env); // CWD-OK: as above
271
276
  return key === null ? null : { key, peakBytes: readHookPeak(key, admissionDir()) };
272
- }, cwd = process.cwd()) {
277
+ }, cwd = process.cwd(), // CWD-OK: cleo run runs the command in its own cwd
278
+ scriptPeak = (script) => {
279
+ const found = scriptFootprint(cwd, script);
280
+ return found === null ? null : { ...found, peakBytes: readHookPeak(found.key, admissionDir()) };
281
+ }) {
273
282
  let scope = cls === 'full-build' ? null : runFootprint(argv);
283
+ const sized = cls === 'scoped-build' && scope?.size === 'class';
284
+ // T14050: a node script that hosts a heavy tool (its path names one, or it
285
+ // loads a compiler or test runner in-process) is never light.
286
+ const script = sized ? nodeScriptOf(argv) : null;
287
+ const scripted = script !== null ? scriptPeak(script) : null;
288
+ const host = script === null
289
+ ? null
290
+ : (heavyScriptReason(script) ??
291
+ (scripted?.heavyTool ? `it loads ${scripted.heavyTool}` : null));
274
292
  // T13452: a scoped build of a command that runs no heavy tool (`node x.mjs`,
275
293
  // `ls`) is charged light. An explicit test run keeps its class plan.
276
- const light = cls === 'scoped-build' && scope?.size === 'class' ? lightCommandReason(argv) : null;
294
+ const light = sized && host === null ? lightCommandReason(argv) : null;
277
295
  if (light !== null)
278
296
  scope = { size: 'light', reason: light };
297
+ else if (host !== null)
298
+ scope = { size: 'class', reason: `node ${script} hosts a heavy tool (${host})` };
279
299
  // T13452: a hooked git push/commit keeps the class plan, because whatever
280
300
  // the hook runs (tests, a build) rides this admission.
281
301
  const probe = scope?.size === 'light' ? gitHookFor(argv) : null;
282
302
  const hook = probe?.hooks.find((h) => hookInstalled(h, probe.globals));
283
- const measured = hook !== undefined && probe !== null ? hookPeak(hook, probe.globals) : null;
303
+ const measured = hook !== undefined && probe !== null
304
+ ? hookPeak(hook, probe.globals)
305
+ : host !== null
306
+ ? scripted
307
+ : null;
284
308
  if (hook !== undefined) {
285
309
  scope = { size: 'class', reason: `runs the ${hook} hook` };
286
310
  }
@@ -297,7 +321,7 @@ hookPeak = (hook, globals) => {
297
321
  const { overlay, resources } = planHeavyToolEnv('typecheck', env, totalRamGib);
298
322
  return {
299
323
  overlay,
300
- resources,
324
+ resources: withProjectOverrides(resources, overlay, cwd),
301
325
  ...(resources !== null ? { footprintBytes: planFootprintBytes(resources) } : {}),
302
326
  footprintReason: scope.reason,
303
327
  namedFiles: null,
@@ -326,15 +350,39 @@ hookPeak = (hook, globals) => {
326
350
  : planned;
327
351
  return {
328
352
  overlay,
329
- resources,
353
+ resources: withProjectOverrides(resources, overlay, cwd),
330
354
  ...(charged !== undefined ? { footprintBytes: charged } : {}),
331
355
  footprintReason: peak !== null && charged !== planned
332
356
  ? `${reason}, measured peak ${gib(peak)} + ${Math.round((HOOK_PEAK_MARGIN - 1) * 100)}%`
333
- : reason,
357
+ : measured !== null && peak === null && host !== null
358
+ ? `${reason}; charged the plan until ${HOOK_MIN_RUNS} runs are measured`
359
+ : reason,
334
360
  namedFiles,
335
361
  ...(measured !== null ? { hookKey: measured.key } : {}),
336
362
  };
337
363
  }
364
+ /**
365
+ * The charge `cleo run` prints before admission (T14050): the bytes asked of
366
+ * the ledger and how they were derived.
367
+ *
368
+ * @param cls - the resolved class.
369
+ * @param plan - from {@link planRunFootprint}.
370
+ *
371
+ * @example
372
+ * ```ts
373
+ * describeCharge('scoped-build', plan); // '1.0 GiB: biome check on 2 named paths'
374
+ * ```
375
+ */
376
+ export function describeCharge(cls, plan) {
377
+ return `${gib(plan.footprintBytes ?? footprintForClass(cls))}: ${plan.footprintReason}`;
378
+ }
379
+ /** A plan with the project-file settings its overlay overrides (T14038). */
380
+ function withProjectOverrides(resources, overlay, cwd) {
381
+ if (resources === null)
382
+ return null;
383
+ const projectOverrides = projectSettingOverrides(cwd, overlay);
384
+ return projectOverrides.length > 0 ? { ...resources, projectOverrides } : resources;
385
+ }
338
386
  /** Governor classes whose admission is the ledger's. */
339
387
  export const LEDGER_CLASSES = new Set([
340
388
  'test-run',
@@ -643,49 +691,80 @@ async function withLedger(dir, fn, signal, strictRead = false) {
643
691
  }
644
692
  }
645
693
  }
646
- // ---------------------------------------------------------------------------
647
- // Liveness
648
- // ---------------------------------------------------------------------------
649
694
  /**
650
- * Whether an entry's holder is still there.
695
+ * Whether an entry's holder is still there, and why.
651
696
  *
652
697
  * - A pid that is provably gone (or recycled: alive with another start time,
653
- * once the heartbeat is stale) is dead as soon as every tool group it
654
- * started is gone too; a detached tool outlives a SIGKILLed cleo.
698
+ * once the heartbeat is stale) is dead as soon as its recorded child and
699
+ * every tool group it started are gone too; a detached tool outlives a
700
+ * SIGKILLed cleo, and so does a foreground child (T13046).
655
701
  * - A fresh heartbeat, or a live pid whose start time matches the recorded
656
702
  * one, is alive.
657
703
  * - A holder that cannot be identified (the probe fails, or no start time was
658
704
  * recorded or can be read) is alive until its heartbeat is
659
- * {@link LEDGER_ORPHAN_MS} old, and so is a gone holder whose tool group id
660
- * still runs (a group id carries no start time, so it may be recycled).
661
- * Past that bound both are dead: nothing holds the budget forever.
705
+ * {@link LEDGER_ORPHAN_MS} old, and so is a gone holder whose child or tool
706
+ * group id still runs (a group id carries no start time, so it may be
707
+ * recycled). Past that bound both are dead: nothing holds the budget forever.
662
708
  * - An entry of another host is judged by its heartbeat alone.
663
709
  *
664
710
  * @param entry - the ledger entry.
665
711
  * @param nowMs - the clock.
666
712
  * @param probe - pid probe (tests inject one).
667
713
  */
668
- export function entryLiveness(entry, nowMs, probe = systemPidProbe) {
714
+ export function entryVerdict(entry, nowMs, probe = systemPidProbe) {
715
+ const alive = (reason) => ({ state: 'alive', reason });
716
+ const dead = (reason) => ({ state: 'dead', reason });
669
717
  const age = nowMs - entry.heartbeatAtMs;
670
- if (entry.host !== hostname())
671
- return age < LEDGER_HEARTBEAT_STALE_MS ? 'alive' : 'dead';
718
+ if (entry.host !== hostname()) {
719
+ return age < LEDGER_HEARTBEAT_STALE_MS
720
+ ? alive(`other-host:${entry.host}`)
721
+ : dead(`other-host-stale:${entry.host}`);
722
+ }
672
723
  const orphaned = age >= LEDGER_ORPHAN_MS;
673
- const goneUnlessGroups = () => orphaned || entry.toolGroups.every((g) => isProbeableId(g) && probe.groupLiveness(g) === 'gone')
674
- ? 'dead'
675
- : 'alive';
724
+ /** What the holder left running: its child, then its tool groups. */
725
+ const survivor = () => {
726
+ const child = entry.child;
727
+ if (child !== undefined && isProbeableId(child.pid) && probe.liveness(child.pid) !== 'gone') {
728
+ const started = child.startedAt === null ? null : probe.startedAt(child.pid);
729
+ if (started === null || started === child.startedAt)
730
+ return `child-alive:${child.pid}`;
731
+ }
732
+ for (const g of entry.toolGroups) {
733
+ const state = isProbeableId(g) ? probe.groupLiveness(g) : 'unknown';
734
+ if (state !== 'gone')
735
+ return `group-${state}:${g}`;
736
+ }
737
+ return null;
738
+ };
739
+ const goneUnlessSurvivor = (reason) => {
740
+ if (orphaned)
741
+ return dead('orphaned');
742
+ const left = survivor();
743
+ return left === null ? dead(reason) : alive(left);
744
+ };
676
745
  const pid = probe.liveness(entry.pid);
677
746
  if (pid === 'gone')
678
- return goneUnlessGroups();
747
+ return goneUnlessSurvivor('pid-gone');
679
748
  if (age < LEDGER_HEARTBEAT_STALE_MS)
680
- return 'alive';
749
+ return alive(pid === 'alive' ? 'pid-alive' : 'heartbeat-fresh');
681
750
  if (pid === 'alive' && entry.startedAt !== null) {
682
751
  const startedAt = probe.startedAt(entry.pid);
683
752
  if (startedAt === entry.startedAt)
684
- return 'alive';
753
+ return alive('pid-alive');
685
754
  if (startedAt !== null)
686
- return goneUnlessGroups(); // a recycled pid
755
+ return goneUnlessSurvivor('pid-recycled');
687
756
  }
688
- return orphaned ? 'dead' : 'alive';
757
+ return orphaned ? dead('orphaned') : alive('unidentifiable');
758
+ }
759
+ /**
760
+ * Whether an entry's holder is still there: {@link entryVerdict}'s state.
761
+ *
762
+ * @param entry - the ledger entry.
763
+ * @param nowMs - the clock.
764
+ * @param probe - pid probe (tests inject one).
765
+ */
766
+ export function entryLiveness(entry, nowMs, probe = systemPidProbe) {
767
+ return entryVerdict(entry, nowMs, probe).state;
689
768
  }
690
769
  /**
691
770
  * The budget an entry of an unknown format is charged: its numeric
@@ -1208,15 +1287,26 @@ export function describeScope(e) {
1208
1287
  return parts.length > 0 ? ` [${parts.join(', ')}]` : '';
1209
1288
  }
1210
1289
  /**
1211
- * One line per admitted run: label, scope and task, pid, command, directory and age.
1290
+ * One line per admitted run: label, scope and task, pid and host, command,
1291
+ * directory, charge and age, then its heartbeat age, child, tool groups and
1292
+ * liveness verdict with the reason (T14066), so a waiter can tell a stale
1293
+ * holder from a real one.
1212
1294
  *
1213
1295
  * @param entries - the ledger.
1214
1296
  * @param nowMs - the clock.
1297
+ * @param probe - pid probe for the verdict (tests inject one).
1215
1298
  */
1216
- export function describeHolders(entries, nowMs) {
1299
+ export function describeHolders(entries, nowMs, probe = systemPidProbe) {
1217
1300
  return entries
1218
1301
  .filter((e) => e.state === 'admitted')
1219
- .map((e) => `${e.label}${describeScope(e)} pid ${e.pid} (${e.command})${e.cwd ? ` in ${e.cwd}` : ''}, ${gib(e.footprintBytes)}${e.footprintReason ? ` (${e.footprintReason})` : ''}, for ${age(nowMs - (e.admittedAtMs ?? e.enqueuedAtMs))}`);
1302
+ .map((e) => {
1303
+ const verdict = entryVerdict(e, nowMs, probe);
1304
+ const child = e.child !== undefined ? `, child ${e.child.pid}` : '';
1305
+ const groups = e.toolGroups.length > 0 ? `, groups ${e.toolGroups.join(',')}` : '';
1306
+ return (`${e.label}${describeScope(e)} pid ${e.pid} on ${e.host} (${e.command})${e.cwd ? ` in ${e.cwd}` : ''}, ` +
1307
+ `${gib(e.footprintBytes)}${e.footprintReason ? ` (${e.footprintReason})` : ''}, for ${age(nowMs - (e.admittedAtMs ?? e.enqueuedAtMs))}; ` +
1308
+ `heartbeat ${age(nowMs - e.heartbeatAtMs)} ago${child}${groups}, ${verdict.state} (${verdict.reason})`);
1309
+ });
1220
1310
  }
1221
1311
  /** Commands too generic to name a lock-taking wrapper. */
1222
1312
  const GENERIC_PROGRAMS = new Set([
@@ -1632,6 +1722,12 @@ async function admitInner(req, opts) {
1632
1722
  const { share, lightShare, reading, sampled } = await sampleShare(opts, before.pressure, nowMs, before.entries);
1633
1723
  lastReading = reading;
1634
1724
  checkQueue();
1725
+ try {
1726
+ opts.onPass?.();
1727
+ }
1728
+ catch {
1729
+ // Best effort: the orphan bound still frees what this could not.
1730
+ }
1635
1731
  const dead = findDead(before, now(), probe);
1636
1732
  return withLedger(dir, (doc) => {
1637
1733
  checkQueue();
@@ -1679,7 +1775,7 @@ async function admitInner(req, opts) {
1679
1775
  retryAfterMs: reading !== null ? MEMORY_GATE_RETRY_AFTER_MS : 2_000,
1680
1776
  memoryPressure: reading,
1681
1777
  ahead,
1682
- holders: describeLoad(doc, nowMs),
1778
+ holders: describeLoad(doc, nowMs, probe),
1683
1779
  };
1684
1780
  };
1685
1781
  const attempt = async () => {
@@ -1712,7 +1808,7 @@ async function admitInner(req, opts) {
1712
1808
  }
1713
1809
  if (now() - lastReportAt >= LEDGER_HOLDER_REPORT_MS && opts.notice) {
1714
1810
  lastReportAt = now();
1715
- opts.notice(holderReport(first.doc, entry, pid, now() - t0, capacityBytes, now(), timeoutMs));
1811
+ opts.notice(holderReport(first.doc, entry, pid, now() - t0, capacityBytes, now(), timeoutMs, probe));
1716
1812
  }
1717
1813
  const remaining = timeoutMs - (now() - t0);
1718
1814
  if (remaining <= 0)
@@ -1791,8 +1887,8 @@ function budgetLoad(doc, capacityBytes) {
1791
1887
  };
1792
1888
  }
1793
1889
  /** {@link describeHolders}, plus a line for entries of another CLEO version. */
1794
- function describeLoad(doc, nowMs) {
1795
- const lines = describeHolders(doc.entries, nowMs);
1890
+ function describeLoad(doc, nowMs, probe) {
1891
+ const lines = describeHolders(doc.entries, nowMs, probe);
1796
1892
  if (doc.foreign.length > 0) {
1797
1893
  lines.push(`${doc.foreign.length} entr${doc.foreign.length === 1 ? 'y' : 'ies'} of another CLEO version`);
1798
1894
  }
@@ -1803,13 +1899,13 @@ function describeLoad(doc, nowMs) {
1803
1899
  * budget, it names the cause (the host's largest memory users, CLEO or not)
1804
1900
  * and when the wait gives up (T13914).
1805
1901
  */
1806
- function holderReport(doc, mine, pid, waitedMs, capacityBytes, nowMs, timeoutMs) {
1902
+ function holderReport(doc, mine, pid, waitedMs, capacityBytes, nowMs, timeoutMs, probe) {
1807
1903
  const holders = doc.entries.filter((e) => e.state === 'admitted');
1808
1904
  const used = budgetLoad(doc, capacityBytes).bytes;
1809
1905
  const ahead = waitingAhead(doc.entries, mine, nowMs);
1810
1906
  const lines = [
1811
1907
  `still waiting after ${age(waitedMs)} for the machine budget (${gib(used)} of ${budgetPhrase(mine, ledgerBudget(doc.pressure, capacityBytes))} in use, ` +
1812
- `queue position ${ahead + 1}${mine.priority === 'urgent' ? ' (urgent)' : ''}, ${ahead} waiting ahead). Holders: ${describeLoad(doc, nowMs).join('; ') || 'none'}.`,
1908
+ `queue position ${ahead + 1}${mine.priority === 'urgent' ? ' (urgent)' : ''}, ${ahead} waiting ahead). Holders: ${describeLoad(doc, nowMs, probe).join(' | ') || 'none'}.`,
1813
1909
  ];
1814
1910
  const reading = doc.pressure?.reading ?? null;
1815
1911
  if (reading !== null) {
@@ -1906,6 +2002,9 @@ function holdGrant(entry, dir, waitedMs, opts) {
1906
2002
  ungoverned: null,
1907
2003
  waitedMs,
1908
2004
  footprintBytes: entry.footprintBytes,
2005
+ recordChild: (child) => {
2006
+ void update((e) => ({ ...e, child }));
2007
+ },
1909
2008
  release: async () => {
1910
2009
  if (released)
1911
2010
  return;