harnery 0.38.0 → 0.40.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/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +32 -8
- package/dist/commands/artifacts.d.ts.map +1 -1
- package/dist/commands/artifacts.js +97 -6
- package/dist/commands/backup.d.ts +16 -0
- package/dist/commands/backup.d.ts.map +1 -1
- package/dist/commands/backup.js +239 -33
- package/dist/commands/browse.d.ts +8 -0
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +466 -231
- package/dist/commands/diagnostics.d.ts.map +1 -1
- package/dist/commands/diagnostics.js +6 -3
- package/dist/commands/ledger-v3.d.ts.map +1 -1
- package/dist/commands/ledger-v3.js +20 -4
- package/dist/commands/qa-run.d.ts.map +1 -1
- package/dist/commands/qa-run.js +26 -3
- package/dist/commands/qa-status.d.ts.map +1 -1
- package/dist/commands/qa-status.js +7 -2
- package/dist/commands/resources.d.ts.map +1 -1
- package/dist/commands/resources.js +23 -3
- package/dist/commands/review-pack.d.ts.map +1 -1
- package/dist/commands/review-pack.js +98 -23
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +2 -2
- package/dist/core/adapters/profiles.d.ts +1 -1
- package/dist/core/agents/canonical-emit.d.ts +3 -2
- package/dist/core/agents/canonical-emit.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.js +2 -0
- package/dist/core/agents/cli.js +13 -6
- package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
- package/dist/core/agents/live-authority-v3.js +5 -3
- package/dist/core/agents/live-lifecycle-v3.d.ts +2 -2
- package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.d.ts +7 -5
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +63 -8
- package/dist/core/agents/session-finalizer-v3.d.ts +2 -2
- package/dist/core/agents/session-finalizer-v3.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.d.ts +3 -3
- package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.js +2 -1
- package/dist/core/agents/state/names.d.ts +35 -2
- package/dist/core/agents/state/names.d.ts.map +1 -1
- package/dist/core/agents/state/names.js +80 -5
- package/dist/core/agents/state/stale-sweep.d.ts +2 -1
- package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
- package/dist/core/agents/state/stale-sweep.js +23 -6
- package/dist/core/agents/turn-elapsed.d.ts +51 -0
- package/dist/core/agents/turn-elapsed.d.ts.map +1 -0
- package/dist/core/agents/turn-elapsed.js +91 -0
- package/dist/core/artifacts/activity.d.ts +14 -0
- package/dist/core/artifacts/activity.d.ts.map +1 -0
- package/dist/core/artifacts/activity.js +52 -0
- package/dist/core/artifacts/delivery-card.d.ts +39 -0
- package/dist/core/artifacts/delivery-card.d.ts.map +1 -0
- package/dist/core/artifacts/delivery-card.js +282 -0
- package/dist/core/artifacts/index.d.ts +42 -7
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +288 -76
- package/dist/core/artifacts/mutation-lock.d.ts +2 -0
- package/dist/core/artifacts/mutation-lock.d.ts.map +1 -0
- package/dist/core/artifacts/mutation-lock.js +91 -0
- package/dist/core/backup/host-snapshot.d.ts +18 -0
- package/dist/core/backup/host-snapshot.d.ts.map +1 -0
- package/dist/core/backup/host-snapshot.js +64 -0
- package/dist/core/config.d.ts +20 -5
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +44 -10
- package/dist/core/diagnostics/advice.d.ts +38 -3
- package/dist/core/diagnostics/advice.d.ts.map +1 -1
- package/dist/core/diagnostics/advice.js +103 -99
- package/dist/core/diagnostics/bundle.d.ts.map +1 -1
- package/dist/core/diagnostics/bundle.js +111 -47
- package/dist/core/diagnostics/comparison.d.ts.map +1 -1
- package/dist/core/diagnostics/comparison.js +3 -2
- package/dist/core/diagnostics/contract.d.ts +15 -31
- package/dist/core/diagnostics/contract.d.ts.map +1 -1
- package/dist/core/diagnostics/contract.js +2 -5
- package/dist/core/diagnostics/index.d.ts +2 -0
- package/dist/core/diagnostics/index.d.ts.map +1 -1
- package/dist/core/diagnostics/index.js +2 -0
- package/dist/core/diagnostics/pressure-contract.d.ts +260 -0
- package/dist/core/diagnostics/pressure-contract.d.ts.map +1 -0
- package/dist/core/diagnostics/pressure-contract.js +70 -0
- package/dist/core/diagnostics/pressure.d.ts +25 -0
- package/dist/core/diagnostics/pressure.d.ts.map +1 -0
- package/dist/core/diagnostics/pressure.js +1138 -0
- package/dist/core/diagnostics/replay.d.ts.map +1 -1
- package/dist/core/diagnostics/replay.js +90 -10
- package/dist/core/events/v3/adapter-id.d.ts +6 -0
- package/dist/core/events/v3/adapter-id.d.ts.map +1 -0
- package/dist/core/events/v3/adapter-id.js +7 -0
- package/dist/core/events/v3/archive-retention.d.ts.map +1 -1
- package/dist/core/events/v3/archive-retention.js +3 -2
- package/dist/core/events/v3/bootstrap.d.ts +41 -4
- package/dist/core/events/v3/bootstrap.d.ts.map +1 -1
- package/dist/core/events/v3/bootstrap.js +120 -17
- package/dist/core/events/v3/capabilities.d.ts +16 -9
- package/dist/core/events/v3/capabilities.d.ts.map +1 -1
- package/dist/core/events/v3/capabilities.js +37 -2
- package/dist/core/events/v3/capability-drift.d.ts +2 -2
- package/dist/core/events/v3/capability-drift.d.ts.map +1 -1
- package/dist/core/events/v3/control-witness.d.ts +48 -5
- package/dist/core/events/v3/control-witness.d.ts.map +1 -1
- package/dist/core/events/v3/control-witness.js +91 -36
- package/dist/core/events/v3/control.d.ts.map +1 -1
- package/dist/core/events/v3/control.js +22 -2
- package/dist/core/events/v3/index.d.ts +2 -1
- package/dist/core/events/v3/index.d.ts.map +1 -1
- package/dist/core/events/v3/index.js +2 -1
- package/dist/core/events/v3/live-feed.d.ts +7 -1
- package/dist/core/events/v3/live-feed.d.ts.map +1 -1
- package/dist/core/events/v3/live-feed.js +21 -3
- package/dist/core/events/v3/live-routing.d.ts.map +1 -1
- package/dist/core/events/v3/live-routing.js +10 -2
- package/dist/core/events/v3/producers/command-recorder.d.ts +2 -2
- package/dist/core/events/v3/producers/command-recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/command-recorder.js +2 -1
- package/dist/core/events/v3/producers/coordination-recorder.d.ts +2 -2
- package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/coordination-recorder.js +2 -1
- package/dist/core/events/v3/producers/hook-base.d.ts +2 -2
- package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -1
- package/dist/core/events/v3/producers/hook-base.js +15 -5
- package/dist/core/events/v3/producers/hook.d.ts.map +1 -1
- package/dist/core/events/v3/producers/hook.js +17 -4
- package/dist/core/events/v3/producers/intake.d.ts +4 -4
- package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
- package/dist/core/events/v3/producers/intake.js +3 -2
- package/dist/core/events/v3/producers/producer-state-observer.d.ts.map +1 -1
- package/dist/core/events/v3/producers/producer-state-observer.js +2 -1
- package/dist/core/events/v3/producers/recorder.d.ts +16 -5
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +31 -9
- package/dist/core/events/v3/reader.d.ts.map +1 -1
- package/dist/core/events/v3/reader.js +10 -13
- package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
- package/dist/core/events/v3/runtime-build.js +3 -2
- package/dist/core/events/v3/runtime-telemetry-capabilities.d.ts +2 -2
- package/dist/core/events/v3/runtime-telemetry-capabilities.d.ts.map +1 -1
- package/dist/core/events/v3/turn-telemetry.d.ts +2 -2
- package/dist/core/events/v3/turn-telemetry.d.ts.map +1 -1
- package/dist/core/hooks/adapter/output.d.ts +16 -14
- package/dist/core/hooks/adapter/output.d.ts.map +1 -1
- package/dist/core/hooks/adapter/output.js +36 -38
- package/dist/core/hooks/backup-schedule.d.ts +34 -0
- package/dist/core/hooks/backup-schedule.d.ts.map +1 -0
- package/dist/core/hooks/backup-schedule.js +114 -0
- package/dist/core/hooks/cli.js +61 -18
- package/dist/core/hooks/codex-wsl-bridge.d.ts +2 -0
- package/dist/core/hooks/codex-wsl-bridge.d.ts.map +1 -1
- package/dist/core/hooks/codex-wsl-bridge.js +18 -4
- package/dist/core/hooks/index.d.ts +1 -0
- package/dist/core/hooks/index.d.ts.map +1 -1
- package/dist/core/hooks/index.js +1 -0
- package/dist/core/hooks/prompt-context/contract.d.ts +61 -0
- package/dist/core/hooks/prompt-context/contract.d.ts.map +1 -0
- package/dist/core/hooks/prompt-context/contract.js +102 -0
- package/dist/core/hooks/prompt-context/index.d.ts +5 -0
- package/dist/core/hooks/prompt-context/index.d.ts.map +1 -0
- package/dist/core/hooks/prompt-context/index.js +2 -0
- package/dist/core/hooks/prompt-context/runner.d.ts +24 -0
- package/dist/core/hooks/prompt-context/runner.d.ts.map +1 -0
- package/dist/core/hooks/prompt-context/runner.js +249 -0
- package/dist/core/qa-artifacts.d.ts +3 -0
- package/dist/core/qa-artifacts.d.ts.map +1 -1
- package/dist/core/qa-artifacts.js +25 -3
- package/dist/core/resources/contract.d.ts +69 -3
- package/dist/core/resources/contract.d.ts.map +1 -1
- package/dist/core/resources/contract.js +1 -1
- package/dist/core/resources/index.d.ts +1 -0
- package/dist/core/resources/index.d.ts.map +1 -1
- package/dist/core/resources/index.js +1 -0
- package/dist/core/resources/native.d.ts +36 -0
- package/dist/core/resources/native.d.ts.map +1 -0
- package/dist/core/resources/native.js +418 -0
- package/dist/core/resources/sampler.d.ts +2 -0
- package/dist/core/resources/sampler.d.ts.map +1 -1
- package/dist/core/resources/sampler.js +31 -1
- package/dist/core/resources/status.d.ts +61 -0
- package/dist/core/resources/status.d.ts.map +1 -0
- package/dist/core/resources/status.js +536 -0
- package/dist/core/resources/system.d.ts +27 -0
- package/dist/core/resources/system.d.ts.map +1 -0
- package/dist/core/resources/system.js +439 -0
- package/dist/core/semantic/contract.d.ts +13 -13
- package/dist/core/semantic/service.d.ts +2 -0
- package/dist/core/semantic/service.d.ts.map +1 -1
- package/dist/core/semantic/service.js +27 -4
- package/dist/core/storage/builtins.js +41 -0
- package/dist/core/supervisor/contract.d.ts +41 -3
- package/dist/core/supervisor/contract.d.ts.map +1 -1
- package/dist/core/supervisor/contract.js +3 -2
- package/dist/core/supervisor/findings.d.ts +3 -0
- package/dist/core/supervisor/findings.d.ts.map +1 -1
- package/dist/core/supervisor/findings.js +88 -0
- package/dist/core/supervisor/history.d.ts.map +1 -1
- package/dist/core/supervisor/history.js +14 -0
- package/dist/core/supervisor/ledger-state.d.ts +55 -0
- package/dist/core/supervisor/ledger-state.d.ts.map +1 -0
- package/dist/core/supervisor/ledger-state.js +0 -0
- package/dist/core/supervisor/service.d.ts.map +1 -1
- package/dist/core/supervisor/service.js +120 -7
- package/dist/core/supervisor/storage.d.ts +3 -1
- package/dist/core/supervisor/storage.d.ts.map +1 -1
- package/dist/core/supervisor/storage.js +4 -0
- package/dist/core/workflow/admission.d.ts +3 -0
- package/dist/core/workflow/admission.d.ts.map +1 -1
- package/dist/core/workflow/admission.js +28 -16
- package/dist/core/workflow/engine.js +2 -2
- package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
- package/dist/core/workflow/live-session-v3.js +6 -1
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +4 -3
- package/dist/lib/browser/capture-fidelity.d.ts +7 -0
- package/dist/lib/browser/capture-fidelity.d.ts.map +1 -1
- package/dist/lib/browser/capture-fidelity.js +17 -0
- package/dist/lib/browser/client.d.ts +9 -0
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +85 -37
- package/dist/lib/browser/index.d.ts +3 -1
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +2 -1
- package/dist/lib/browser/page-review-budget.d.ts +13 -0
- package/dist/lib/browser/page-review-budget.d.ts.map +1 -0
- package/dist/lib/browser/page-review-budget.js +251 -0
- package/dist/lib/browser/page-review-contracts.d.ts +127 -0
- package/dist/lib/browser/page-review-contracts.d.ts.map +1 -0
- package/dist/lib/browser/page-review-contracts.js +5 -0
- package/dist/lib/browser/page-review-evidence.d.ts +48 -0
- package/dist/lib/browser/page-review-evidence.d.ts.map +1 -0
- package/dist/lib/browser/page-review-evidence.js +340 -0
- package/dist/lib/browser/page-review-judge.d.ts +1 -1
- package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
- package/dist/lib/browser/page-review-judge.js +46 -38
- package/dist/lib/browser/page-review-pack.d.ts +44 -22
- package/dist/lib/browser/page-review-pack.d.ts.map +1 -1
- package/dist/lib/browser/page-review-pack.js +116 -36
- package/dist/lib/browser/page-review-source.d.ts +12 -0
- package/dist/lib/browser/page-review-source.d.ts.map +1 -0
- package/dist/lib/browser/page-review-source.js +22 -0
- package/dist/lib/browser/qa-run-contracts.d.ts +35 -9
- package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
- package/dist/lib/browser/qa-run-contracts.js +48 -14
- package/dist/lib/browser/qa-run.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.js +215 -68
- package/dist/lib/browser/qa-snapshot.d.ts +6 -0
- package/dist/lib/browser/qa-snapshot.d.ts.map +1 -1
- package/dist/lib/browser/qa-snapshot.js +259 -48
- package/dist/lib/browser/review-capture-transaction.d.ts +13 -0
- package/dist/lib/browser/review-capture-transaction.d.ts.map +1 -0
- package/dist/lib/browser/review-capture-transaction.js +79 -0
- package/dist/lib/browser/standalone-html.d.ts.map +1 -1
- package/dist/lib/browser/standalone-html.js +4 -2
- package/dist/lib/browser/thumbnail-association.d.ts +34 -0
- package/dist/lib/browser/thumbnail-association.d.ts.map +1 -0
- package/dist/lib/browser/thumbnail-association.js +158 -0
- package/dist/lib/browser/tiling.d.ts +30 -0
- package/dist/lib/browser/tiling.d.ts.map +1 -1
- package/dist/lib/browser/tiling.js +38 -0
- package/dist/lib/docs-metadata-managed.d.ts.map +1 -1
- package/dist/lib/docs-metadata-managed.js +4 -1
- package/dist/lib/identities/assume.d.ts.map +1 -1
- package/dist/lib/identities/assume.js +7 -1
- package/dist/lib/instructions/templates.d.ts.map +1 -1
- package/dist/lib/instructions/templates.js +1 -0
- package/dist/lib/local-file-url.d.ts +12 -0
- package/dist/lib/local-file-url.d.ts.map +1 -1
- package/dist/lib/local-file-url.js +16 -2
- package/dist/lib/tunnel/state.d.ts +19 -4
- package/dist/lib/tunnel/state.d.ts.map +1 -1
- package/dist/lib/tunnel/state.js +53 -13
- package/package.json +7 -4
- package/schemas/config.schema.json +69 -2
- package/src/commands/agents.ts +42 -9
- package/src/commands/artifacts.ts +130 -6
- package/src/commands/backup.ts +287 -34
- package/src/commands/browse.ts +564 -267
- package/src/commands/diagnostics.ts +6 -3
- package/src/commands/ledger-v3.ts +20 -3
- package/src/commands/qa-run.ts +37 -3
- package/src/commands/qa-status.ts +11 -2
- package/src/commands/resources.ts +23 -3
- package/src/commands/review-pack.ts +165 -62
- package/src/commands/tunnel.ts +2 -1
- package/src/core/agents/canonical-emit.ts +4 -2
- package/src/core/agents/cli.ts +14 -6
- package/src/core/agents/live-authority-v3.ts +7 -3
- package/src/core/agents/live-lifecycle-v3.ts +2 -2
- package/src/core/agents/render/prompt-context.ts +68 -14
- package/src/core/agents/session-finalizer-v3.ts +2 -2
- package/src/core/agents/state/live-coordination-view.ts +4 -4
- package/src/core/agents/state/names.ts +92 -7
- package/src/core/agents/state/stale-sweep.ts +22 -8
- package/src/core/agents/turn-elapsed.ts +135 -0
- package/src/core/artifacts/activity.ts +71 -0
- package/src/core/artifacts/delivery-card.ts +375 -0
- package/src/core/artifacts/index.ts +385 -91
- package/src/core/artifacts/mutation-lock.ts +106 -0
- package/src/core/backup/host-snapshot.ts +76 -0
- package/src/core/config.ts +66 -13
- package/src/core/diagnostics/advice.ts +148 -120
- package/src/core/diagnostics/bundle.ts +138 -52
- package/src/core/diagnostics/comparison.ts +6 -2
- package/src/core/diagnostics/contract.ts +39 -42
- package/src/core/diagnostics/index.ts +2 -0
- package/src/core/diagnostics/pressure-contract.ts +294 -0
- package/src/core/diagnostics/pressure.ts +1361 -0
- package/src/core/diagnostics/replay.ts +116 -11
- package/src/core/events/v3/adapter-id.ts +12 -0
- package/src/core/events/v3/archive-retention.ts +3 -6
- package/src/core/events/v3/bootstrap.ts +163 -16
- package/src/core/events/v3/capabilities.ts +75 -25
- package/src/core/events/v3/capability-drift.ts +2 -2
- package/src/core/events/v3/control-witness.ts +172 -41
- package/src/core/events/v3/control.ts +34 -2
- package/src/core/events/v3/index.ts +3 -0
- package/src/core/events/v3/live-feed.ts +23 -5
- package/src/core/events/v3/live-routing.ts +14 -2
- package/src/core/events/v3/producers/command-recorder.ts +4 -4
- package/src/core/events/v3/producers/coordination-recorder.ts +4 -4
- package/src/core/events/v3/producers/hook-base.ts +21 -16
- package/src/core/events/v3/producers/hook.ts +21 -4
- package/src/core/events/v3/producers/intake.ts +6 -6
- package/src/core/events/v3/producers/producer-state-observer.ts +2 -1
- package/src/core/events/v3/producers/recorder.ts +50 -21
- package/src/core/events/v3/reader.ts +14 -13
- package/src/core/events/v3/runtime-build.ts +3 -2
- package/src/core/events/v3/runtime-telemetry-capabilities.ts +3 -3
- package/src/core/events/v3/turn-telemetry.ts +4 -4
- package/src/core/hooks/adapter/output.ts +50 -36
- package/src/core/hooks/backup-schedule.ts +147 -0
- package/src/core/hooks/cli.ts +65 -14
- package/src/core/hooks/codex-wsl-bridge.ts +21 -5
- package/src/core/hooks/index.ts +1 -0
- package/src/core/hooks/prompt-context/contract.ts +177 -0
- package/src/core/hooks/prompt-context/index.ts +22 -0
- package/src/core/hooks/prompt-context/runner.ts +303 -0
- package/src/core/qa-artifacts.ts +29 -3
- package/src/core/resources/contract.ts +76 -3
- package/src/core/resources/index.ts +1 -0
- package/src/core/resources/native.ts +481 -0
- package/src/core/resources/sampler.ts +46 -1
- package/src/core/resources/status.ts +717 -0
- package/src/core/resources/system.ts +530 -0
- package/src/core/semantic/service.ts +37 -3
- package/src/core/storage/builtins.ts +41 -0
- package/src/core/supervisor/contract.ts +45 -3
- package/src/core/supervisor/findings.ts +159 -0
- package/src/core/supervisor/history.ts +15 -0
- package/src/core/supervisor/ledger-state.ts +0 -0
- package/src/core/supervisor/service.ts +133 -5
- package/src/core/supervisor/storage.ts +7 -0
- package/src/core/workflow/admission.ts +33 -17
- package/src/core/workflow/engine.ts +2 -2
- package/src/core/workflow/live-session-v3.ts +6 -8
- package/src/core/workflow/proof.ts +4 -3
- package/src/lib/browser/capture-fidelity.ts +23 -0
- package/src/lib/browser/client.ts +96 -44
- package/src/lib/browser/index.ts +15 -1
- package/src/lib/browser/page-review-budget.ts +309 -0
- package/src/lib/browser/page-review-contracts.ts +129 -0
- package/src/lib/browser/page-review-evidence.ts +465 -0
- package/src/lib/browser/page-review-judge.ts +56 -40
- package/src/lib/browser/page-review-pack.ts +181 -43
- package/src/lib/browser/page-review-source.ts +30 -0
- package/src/lib/browser/qa-run-contracts.ts +62 -18
- package/src/lib/browser/qa-run.ts +259 -71
- package/src/lib/browser/qa-snapshot.ts +289 -48
- package/src/lib/browser/review-capture-transaction.ts +106 -0
- package/src/lib/browser/standalone-html.ts +4 -2
- package/src/lib/browser/thumbnail-association.ts +190 -0
- package/src/lib/browser/tiling.ts +63 -0
- package/src/lib/docs-metadata-managed.ts +4 -1
- package/src/lib/identities/assume.ts +12 -4
- package/src/lib/instructions/templates.ts +1 -0
- package/src/lib/local-file-url.ts +17 -2
- package/src/lib/tunnel/state.ts +70 -13
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import { closeSync, openSync, readdirSync, readSync, statfsSync, statSync } from "node:fs";
|
|
2
|
+
import { platform, tmpdir } from "node:os";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { performance } from "node:perf_hooks";
|
|
5
|
+
import type {
|
|
6
|
+
ResourceDiskSample,
|
|
7
|
+
ResourceIoSample,
|
|
8
|
+
ResourceOomSample,
|
|
9
|
+
ResourcePressureSample,
|
|
10
|
+
ResourcePressureWindow,
|
|
11
|
+
ResourceSupportState,
|
|
12
|
+
ResourceVmstatSample,
|
|
13
|
+
} from "./contract.ts";
|
|
14
|
+
|
|
15
|
+
interface FileSystemCapacity {
|
|
16
|
+
bsize: number | bigint;
|
|
17
|
+
blocks: number | bigint;
|
|
18
|
+
bavail: number | bigint;
|
|
19
|
+
bfree: number | bigint;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SystemResourceOptions {
|
|
23
|
+
platform?: NodeJS.Platform;
|
|
24
|
+
procRoot?: string;
|
|
25
|
+
sysRoot?: string;
|
|
26
|
+
nowMs?: number;
|
|
27
|
+
namespace?: string;
|
|
28
|
+
tmpPath?: string;
|
|
29
|
+
statfs?: (path: string) => FileSystemCapacity;
|
|
30
|
+
filesystemId?: (path: string) => string | number | bigint;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface IoCounters {
|
|
34
|
+
read: number;
|
|
35
|
+
write: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface IoBaseline {
|
|
39
|
+
sampledAt: number;
|
|
40
|
+
devices: Map<string, IoCounters>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The `/proc/vmstat` counters the reclaim rates are derived from, in report order. */
|
|
44
|
+
const VMSTAT_COUNTERS = ["pswpin", "pswpout", "pgscan_direct", "pgmajfault"] as const;
|
|
45
|
+
|
|
46
|
+
type VmstatCounterName = (typeof VMSTAT_COUNTERS)[number];
|
|
47
|
+
type VmstatCounters = Record<VmstatCounterName, number | null>;
|
|
48
|
+
|
|
49
|
+
interface VmstatBaseline {
|
|
50
|
+
sampledAt: number;
|
|
51
|
+
counters: VmstatCounters;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Linux reports `pswpin` and `pswpout` in pages, and the kernel does not
|
|
56
|
+
* publish the page size in `/proc/vmstat`. Every architecture Harnery runs on
|
|
57
|
+
* uses a 4096 byte base page, so that value converts pages to bytes here. A
|
|
58
|
+
* platform with a different base page would need its page size supplied.
|
|
59
|
+
*/
|
|
60
|
+
const VMSTAT_PAGE_SIZE_BYTES = 4_096;
|
|
61
|
+
|
|
62
|
+
const ioBaselines = new Map<string, IoBaseline>();
|
|
63
|
+
const oomBaselines = new Map<
|
|
64
|
+
string,
|
|
65
|
+
{ sampledAt: number; kills: number; lastKillAt: number | null }
|
|
66
|
+
>();
|
|
67
|
+
const vmstatBaselines = new Map<string, VmstatBaseline>();
|
|
68
|
+
const MAX_BASELINES = 64;
|
|
69
|
+
const MAX_BLOCK_DEVICES = 1_024;
|
|
70
|
+
|
|
71
|
+
/** Collect only the workspace and temporary volume; never walk their contents. */
|
|
72
|
+
export function collectSystemResources(
|
|
73
|
+
coordRoot: string,
|
|
74
|
+
options: SystemResourceOptions = {},
|
|
75
|
+
): {
|
|
76
|
+
disks: ResourceDiskSample[];
|
|
77
|
+
pressure: ResourcePressureSample;
|
|
78
|
+
io: ResourceIoSample;
|
|
79
|
+
oom: ResourceOomSample;
|
|
80
|
+
vmstat: ResourceVmstatSample;
|
|
81
|
+
} {
|
|
82
|
+
const currentPlatform = options.platform ?? platform();
|
|
83
|
+
const procRoot = options.procRoot ?? "/proc";
|
|
84
|
+
const sysRoot = options.sysRoot ?? "/sys";
|
|
85
|
+
const disks = collectDisks(coordRoot, options);
|
|
86
|
+
if (currentPlatform !== "linux") {
|
|
87
|
+
return {
|
|
88
|
+
disks,
|
|
89
|
+
pressure: {
|
|
90
|
+
state: "unsupported",
|
|
91
|
+
cpu: null,
|
|
92
|
+
memory: null,
|
|
93
|
+
io: null,
|
|
94
|
+
memory_full: null,
|
|
95
|
+
io_full: null,
|
|
96
|
+
reason: `Linux pressure stall information is unavailable on ${currentPlatform}.`,
|
|
97
|
+
},
|
|
98
|
+
io: unavailableIo("unsupported", `Disk I/O rates are not collected on ${currentPlatform}.`),
|
|
99
|
+
oom: {
|
|
100
|
+
state: "unsupported",
|
|
101
|
+
total_kills: null,
|
|
102
|
+
kills_since_last_sample: null,
|
|
103
|
+
last_kill_age_ms: null,
|
|
104
|
+
reason: `Kernel OOM kill counters are not collected on ${currentPlatform}.`,
|
|
105
|
+
},
|
|
106
|
+
vmstat: unavailableVmstat(
|
|
107
|
+
"unsupported",
|
|
108
|
+
`Kernel memory reclaim counters are not collected on ${currentPlatform}.`,
|
|
109
|
+
false,
|
|
110
|
+
),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const key = JSON.stringify([
|
|
114
|
+
resolve(coordRoot),
|
|
115
|
+
options.namespace ?? "linux",
|
|
116
|
+
resolve(procRoot),
|
|
117
|
+
resolve(sysRoot),
|
|
118
|
+
]);
|
|
119
|
+
return {
|
|
120
|
+
disks,
|
|
121
|
+
pressure: collectPressure(procRoot),
|
|
122
|
+
io: collectIo(procRoot, sysRoot, options.nowMs ?? performance.now(), key),
|
|
123
|
+
oom: collectOom(procRoot, options.nowMs ?? performance.now(), key),
|
|
124
|
+
vmstat: collectVmstat(procRoot, options.nowMs ?? performance.now(), key),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function collectDisks(coordRoot: string, options: SystemResourceOptions): ResourceDiskSample[] {
|
|
129
|
+
const inspect = options.statfs ?? statfsSync;
|
|
130
|
+
const filesystemId = options.filesystemId ?? ((path: string) => statSync(path).dev);
|
|
131
|
+
const seen = new Set<string | number | bigint>();
|
|
132
|
+
const results: ResourceDiskSample[] = [];
|
|
133
|
+
for (const path of new Set([resolve(coordRoot), resolve(options.tmpPath ?? tmpdir())])) {
|
|
134
|
+
try {
|
|
135
|
+
const stat = inspect(path);
|
|
136
|
+
const blockSize = Number(stat.bsize);
|
|
137
|
+
const total = blockSize * Number(stat.blocks);
|
|
138
|
+
const available = blockSize * Number(stat.bavail);
|
|
139
|
+
const free = blockSize * Number(stat.bfree);
|
|
140
|
+
if (
|
|
141
|
+
![blockSize, total, available, free].every(Number.isFinite) ||
|
|
142
|
+
blockSize <= 0 ||
|
|
143
|
+
total <= 0 ||
|
|
144
|
+
free < 0 ||
|
|
145
|
+
free > total ||
|
|
146
|
+
available > total
|
|
147
|
+
) {
|
|
148
|
+
throw new Error("Invalid filesystem capacity counters.");
|
|
149
|
+
}
|
|
150
|
+
// st_dev identifies the mounted filesystem, unlike coincidentally equal capacity values.
|
|
151
|
+
try {
|
|
152
|
+
const id = filesystemId(path);
|
|
153
|
+
if (seen.has(id)) continue;
|
|
154
|
+
seen.add(id);
|
|
155
|
+
} catch {
|
|
156
|
+
// Capacity is still useful when stat cannot identify the filesystem.
|
|
157
|
+
}
|
|
158
|
+
results.push({
|
|
159
|
+
path,
|
|
160
|
+
state: "supported",
|
|
161
|
+
total_bytes: total,
|
|
162
|
+
available_bytes: Math.max(0, available),
|
|
163
|
+
used_percent: round(((total - free) / total) * 100),
|
|
164
|
+
});
|
|
165
|
+
} catch (error) {
|
|
166
|
+
results.push({
|
|
167
|
+
path,
|
|
168
|
+
state: unsupportedError(error) ? "unsupported" : "error",
|
|
169
|
+
total_bytes: null,
|
|
170
|
+
available_bytes: null,
|
|
171
|
+
used_percent: null,
|
|
172
|
+
reason: errorReason(error),
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return results;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function collectPressure(procRoot: string): ResourcePressureSample {
|
|
180
|
+
const sample: ResourcePressureSample = {
|
|
181
|
+
state: "supported",
|
|
182
|
+
cpu: null,
|
|
183
|
+
memory: null,
|
|
184
|
+
io: null,
|
|
185
|
+
memory_full: null,
|
|
186
|
+
io_full: null,
|
|
187
|
+
};
|
|
188
|
+
const reasons: string[] = [];
|
|
189
|
+
let supported = 0;
|
|
190
|
+
let failures = 0;
|
|
191
|
+
for (const resource of ["cpu", "memory", "io"] as const) {
|
|
192
|
+
try {
|
|
193
|
+
const raw = readBounded(join(procRoot, "pressure", resource), 4_096);
|
|
194
|
+
const some = parsePressure(raw, "some");
|
|
195
|
+
// System-wide CPU full is undefined. Memory and I/O full measure all non-idle tasks stalled.
|
|
196
|
+
if (resource !== "cpu") sample[`${resource}_full`] = parsePressure(raw, "full");
|
|
197
|
+
sample[resource] = some;
|
|
198
|
+
supported++;
|
|
199
|
+
} catch (error) {
|
|
200
|
+
if (!missingError(error) && !unsupportedError(error)) failures++;
|
|
201
|
+
reasons.push(`${resource}: ${errorReason(error)}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
sample.state =
|
|
205
|
+
supported === 3
|
|
206
|
+
? "supported"
|
|
207
|
+
: supported > 0
|
|
208
|
+
? "partial"
|
|
209
|
+
: failures > 0
|
|
210
|
+
? "error"
|
|
211
|
+
: "unsupported";
|
|
212
|
+
if (reasons.length > 0) sample.reason = reasons.join("; ");
|
|
213
|
+
return sample;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function parsePressure(source: string, kind: "some" | "full"): ResourcePressureWindow {
|
|
217
|
+
const lines = source
|
|
218
|
+
.trim()
|
|
219
|
+
.split(/\r?\n/)
|
|
220
|
+
.filter((line) => line.startsWith(`${kind} `));
|
|
221
|
+
if (lines.length !== 1) throw new Error(`Malformed PSI: expected one ${kind} row.`);
|
|
222
|
+
const fields = new Map<string, string>();
|
|
223
|
+
for (const token of lines[0]!.trim().split(/\s+/).slice(1)) {
|
|
224
|
+
const [name, value, extra] = token.split("=");
|
|
225
|
+
if (!name || !value || extra !== undefined || fields.has(name))
|
|
226
|
+
throw new Error("Malformed PSI fields.");
|
|
227
|
+
fields.set(name, value);
|
|
228
|
+
}
|
|
229
|
+
const values = ["avg10", "avg60", "avg300"].map((name) => {
|
|
230
|
+
const raw = fields.get(name);
|
|
231
|
+
const value = Number(raw);
|
|
232
|
+
if (!raw || !/^\d+(?:\.\d+)?$/.test(raw) || !Number.isFinite(value) || value > 100) {
|
|
233
|
+
throw new Error(`Malformed PSI ${name}.`);
|
|
234
|
+
}
|
|
235
|
+
return value;
|
|
236
|
+
});
|
|
237
|
+
if (!/^\d+$/.test(fields.get("total") ?? "")) throw new Error("Malformed PSI total.");
|
|
238
|
+
return { avg10: values[0]!, avg60: values[1]!, avg300: values[2]! };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function collectOom(procRoot: string, now: number, key: string): ResourceOomSample {
|
|
242
|
+
try {
|
|
243
|
+
const rows = readBounded(join(procRoot, "vmstat"), 65_536)
|
|
244
|
+
.split(/\r?\n/)
|
|
245
|
+
.filter((line) => /^oom_kill\s/.test(line));
|
|
246
|
+
if (rows.length === 0) {
|
|
247
|
+
oomBaselines.delete(key);
|
|
248
|
+
return {
|
|
249
|
+
state: "unsupported",
|
|
250
|
+
total_kills: null,
|
|
251
|
+
kills_since_last_sample: null,
|
|
252
|
+
last_kill_age_ms: null,
|
|
253
|
+
reason: "Kernel does not expose oom_kill.",
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
const match = rows.length === 1 ? /^oom_kill\s+(\d+)\s*$/.exec(rows[0]!) : null;
|
|
257
|
+
const kills = match ? Number(match[1]) : NaN;
|
|
258
|
+
if (!Number.isSafeInteger(kills) || kills < 0 || !Number.isFinite(now))
|
|
259
|
+
throw new Error("Malformed OOM kill counter or sample time.");
|
|
260
|
+
const previous = oomBaselines.get(key);
|
|
261
|
+
const consecutive = previous && now > previous.sampledAt && kills >= previous.kills;
|
|
262
|
+
const delta = consecutive ? kills - previous.kills : null;
|
|
263
|
+
const lastKillAt = delta !== null && delta > 0 ? now : consecutive ? previous.lastKillAt : null;
|
|
264
|
+
oomBaselines.delete(key);
|
|
265
|
+
oomBaselines.set(key, { sampledAt: now, kills, lastKillAt });
|
|
266
|
+
if (oomBaselines.size > MAX_BASELINES) oomBaselines.delete(oomBaselines.keys().next().value!);
|
|
267
|
+
return {
|
|
268
|
+
state: "supported",
|
|
269
|
+
total_kills: kills,
|
|
270
|
+
kills_since_last_sample: delta,
|
|
271
|
+
last_kill_age_ms: lastKillAt === null ? null : now - lastKillAt,
|
|
272
|
+
...(delta === null
|
|
273
|
+
? { reason: "Recent OOM kills need two consecutive counters; baseline started." }
|
|
274
|
+
: {}),
|
|
275
|
+
};
|
|
276
|
+
} catch (error) {
|
|
277
|
+
oomBaselines.delete(key);
|
|
278
|
+
return {
|
|
279
|
+
state: missingError(error) || unsupportedError(error) ? "unsupported" : "error",
|
|
280
|
+
total_kills: null,
|
|
281
|
+
kills_since_last_sample: null,
|
|
282
|
+
last_kill_age_ms: null,
|
|
283
|
+
reason: errorReason(error),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Read the kernel reclaim counters and turn them into per-second rates. Only a
|
|
290
|
+
* pair of consecutive reads can express a rate, so the first read after a
|
|
291
|
+
* start, a restart, a stalled clock, or a counter reset reports
|
|
292
|
+
* `counters_reset` with null rates instead of inventing a baseline.
|
|
293
|
+
*/
|
|
294
|
+
function collectVmstat(procRoot: string, now: number, key: string): ResourceVmstatSample {
|
|
295
|
+
try {
|
|
296
|
+
if (!Number.isFinite(now)) throw new Error("Malformed reclaim sample time.");
|
|
297
|
+
const counters = parseVmstatCounters(readBounded(join(procRoot, "vmstat"), 65_536));
|
|
298
|
+
const missing = VMSTAT_COUNTERS.filter((name) => counters[name] === null);
|
|
299
|
+
if (missing.length === VMSTAT_COUNTERS.length) {
|
|
300
|
+
vmstatBaselines.delete(key);
|
|
301
|
+
return unavailableVmstat(
|
|
302
|
+
"unsupported",
|
|
303
|
+
"Kernel does not expose the memory reclaim counters.",
|
|
304
|
+
false,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
const state: ResourceSupportState = missing.length > 0 ? "partial" : "supported";
|
|
308
|
+
const partialReason =
|
|
309
|
+
missing.length > 0 ? `Kernel does not expose ${missing.join(", ")}.` : undefined;
|
|
310
|
+
const previous = vmstatBaselines.get(key);
|
|
311
|
+
vmstatBaselines.delete(key);
|
|
312
|
+
vmstatBaselines.set(key, { sampledAt: now, counters });
|
|
313
|
+
if (vmstatBaselines.size > MAX_BASELINES)
|
|
314
|
+
vmstatBaselines.delete(vmstatBaselines.keys().next().value!);
|
|
315
|
+
const elapsedSeconds = previous ? (now - previous.sampledAt) / 1_000 : 0;
|
|
316
|
+
if (!previous || !Number.isFinite(elapsedSeconds) || elapsedSeconds <= 0) {
|
|
317
|
+
return unavailableVmstat(
|
|
318
|
+
state,
|
|
319
|
+
joinReasons(
|
|
320
|
+
partialReason,
|
|
321
|
+
previous
|
|
322
|
+
? "The reclaim sample interval did not advance, so a new baseline started."
|
|
323
|
+
: "Reclaim rates need two consecutive counters, so a baseline started.",
|
|
324
|
+
),
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
const decreased = VMSTAT_COUNTERS.some((name) => {
|
|
328
|
+
const current = counters[name];
|
|
329
|
+
const last = previous.counters[name];
|
|
330
|
+
return current !== null && last !== null && current < last;
|
|
331
|
+
});
|
|
332
|
+
if (decreased) {
|
|
333
|
+
return unavailableVmstat(
|
|
334
|
+
state,
|
|
335
|
+
joinReasons(partialReason, "Kernel reclaim counters decreased, so a new baseline started."),
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
const perSecond = (name: VmstatCounterName): number | null => {
|
|
339
|
+
const current = counters[name];
|
|
340
|
+
const last = previous.counters[name];
|
|
341
|
+
if (current === null || last === null) return null;
|
|
342
|
+
return (current - last) / elapsedSeconds;
|
|
343
|
+
};
|
|
344
|
+
const swapIn = perSecond("pswpin");
|
|
345
|
+
const swapOut = perSecond("pswpout");
|
|
346
|
+
return {
|
|
347
|
+
state,
|
|
348
|
+
swap_in_bytes_per_second: swapIn === null ? null : round(swapIn * VMSTAT_PAGE_SIZE_BYTES),
|
|
349
|
+
swap_out_bytes_per_second: swapOut === null ? null : round(swapOut * VMSTAT_PAGE_SIZE_BYTES),
|
|
350
|
+
direct_reclaim_pages_per_second: nullableRound(perSecond("pgscan_direct")),
|
|
351
|
+
major_faults_per_second: nullableRound(perSecond("pgmajfault")),
|
|
352
|
+
counters_reset: false,
|
|
353
|
+
...(partialReason ? { reason: partialReason } : {}),
|
|
354
|
+
};
|
|
355
|
+
} catch (error) {
|
|
356
|
+
vmstatBaselines.delete(key);
|
|
357
|
+
return unavailableVmstat(
|
|
358
|
+
missingError(error) || unsupportedError(error) ? "unsupported" : "error",
|
|
359
|
+
errorReason(error),
|
|
360
|
+
false,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function parseVmstatCounters(source: string): VmstatCounters {
|
|
366
|
+
const counters: VmstatCounters = {
|
|
367
|
+
pswpin: null,
|
|
368
|
+
pswpout: null,
|
|
369
|
+
pgscan_direct: null,
|
|
370
|
+
pgmajfault: null,
|
|
371
|
+
};
|
|
372
|
+
for (const name of VMSTAT_COUNTERS) {
|
|
373
|
+
// Anchor on whitespace so pgscan_direct never absorbs pgscan_direct_throttle.
|
|
374
|
+
const rows = source.split(/\r?\n/).filter((line) => line.startsWith(`${name} `));
|
|
375
|
+
if (rows.length === 0) continue;
|
|
376
|
+
if (rows.length > 1) throw new Error(`Duplicate vmstat row for ${name}.`);
|
|
377
|
+
const match = new RegExp(`^${name}\\s+(\\d+)\\s*$`).exec(rows[0]!);
|
|
378
|
+
const value = match ? Number(match[1]) : NaN;
|
|
379
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
380
|
+
throw new Error(`Malformed vmstat counter for ${name}.`);
|
|
381
|
+
counters[name] = value;
|
|
382
|
+
}
|
|
383
|
+
return counters;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function unavailableVmstat(
|
|
387
|
+
state: ResourceSupportState,
|
|
388
|
+
reason: string,
|
|
389
|
+
countersReset = true,
|
|
390
|
+
): ResourceVmstatSample {
|
|
391
|
+
return {
|
|
392
|
+
state,
|
|
393
|
+
swap_in_bytes_per_second: null,
|
|
394
|
+
swap_out_bytes_per_second: null,
|
|
395
|
+
direct_reclaim_pages_per_second: null,
|
|
396
|
+
major_faults_per_second: null,
|
|
397
|
+
counters_reset: countersReset,
|
|
398
|
+
reason,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function joinReasons(...parts: (string | undefined)[]): string {
|
|
403
|
+
return parts.filter((part): part is string => Boolean(part)).join(" ");
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function collectIo(procRoot: string, sysRoot: string, now: number, key: string): ResourceIoSample {
|
|
407
|
+
try {
|
|
408
|
+
const devices = readIoCounters(procRoot, sysRoot);
|
|
409
|
+
const previous = ioBaselines.get(key);
|
|
410
|
+
ioBaselines.delete(key);
|
|
411
|
+
ioBaselines.set(key, { sampledAt: now, devices });
|
|
412
|
+
if (ioBaselines.size > MAX_BASELINES) ioBaselines.delete(ioBaselines.keys().next().value!);
|
|
413
|
+
if (!previous) return unavailableIo("supported", "Disk I/O rates need two samples.");
|
|
414
|
+
const elapsed = (now - previous.sampledAt) / 1_000;
|
|
415
|
+
if (!Number.isFinite(elapsed) || elapsed <= 0)
|
|
416
|
+
return unavailableIo("supported", "Disk I/O sample interval did not advance.");
|
|
417
|
+
if (devices.size !== previous.devices.size)
|
|
418
|
+
return unavailableIo("supported", "Block devices changed; disk I/O baseline restarted.");
|
|
419
|
+
let read = 0;
|
|
420
|
+
let write = 0;
|
|
421
|
+
for (const [id, current] of devices) {
|
|
422
|
+
const last = previous.devices.get(id);
|
|
423
|
+
if (!last || current.read < last.read || current.write < last.write) {
|
|
424
|
+
return unavailableIo(
|
|
425
|
+
"supported",
|
|
426
|
+
"Block device counters reset or devices changed; disk I/O baseline restarted.",
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
read += current.read - last.read;
|
|
430
|
+
write += current.write - last.write;
|
|
431
|
+
}
|
|
432
|
+
// Linux diskstats sectors are always 512 bytes, independent of device block size.
|
|
433
|
+
return {
|
|
434
|
+
state: "supported",
|
|
435
|
+
read_bytes_per_second: round((read * 512) / elapsed),
|
|
436
|
+
write_bytes_per_second: round((write * 512) / elapsed),
|
|
437
|
+
};
|
|
438
|
+
} catch (error) {
|
|
439
|
+
ioBaselines.delete(key);
|
|
440
|
+
return unavailableIo(
|
|
441
|
+
missingError(error) || unsupportedError(error) ? "unsupported" : "error",
|
|
442
|
+
errorReason(error),
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function readIoCounters(procRoot: string, sysRoot: string): Map<string, IoCounters> {
|
|
448
|
+
const entries = readdirSync(join(sysRoot, "block"));
|
|
449
|
+
if (entries.length > MAX_BLOCK_DEVICES)
|
|
450
|
+
throw new Error("Block device inventory exceeds the collector limit.");
|
|
451
|
+
const selected = new Set<string>();
|
|
452
|
+
for (const device of entries) {
|
|
453
|
+
// Whole devices only. Exclude memory-backed disks and stacked devices so
|
|
454
|
+
// device-mapper/RAID activity is counted once, at the underlying disks.
|
|
455
|
+
if (/^(?:loop|ram|zram)\d+$/.test(device)) continue;
|
|
456
|
+
if (readdirSync(join(sysRoot, "block", device, "slaves")).length > 0) continue;
|
|
457
|
+
selected.add(device);
|
|
458
|
+
}
|
|
459
|
+
if (selected.size === 0)
|
|
460
|
+
throw Object.assign(new Error("No underlying block devices are visible."), { code: "ENOTSUP" });
|
|
461
|
+
const results = new Map<string, IoCounters>();
|
|
462
|
+
for (const line of readBounded(join(procRoot, "diskstats"), 1_048_576).trim().split(/\r?\n/)) {
|
|
463
|
+
const fields = line.trim().split(/\s+/);
|
|
464
|
+
const device = fields[2];
|
|
465
|
+
if (!device || !selected.has(device)) continue;
|
|
466
|
+
if (
|
|
467
|
+
fields.length < 14 ||
|
|
468
|
+
![...fields.slice(0, 2), ...fields.slice(3)].every((value) => /^\d+$/.test(value))
|
|
469
|
+
) {
|
|
470
|
+
throw new Error(`Malformed diskstats for ${device}.`);
|
|
471
|
+
}
|
|
472
|
+
const read = Number(fields[5]);
|
|
473
|
+
const write = Number(fields[9]);
|
|
474
|
+
if (![read, write].every(Number.isSafeInteger))
|
|
475
|
+
throw new Error(`Invalid diskstats counters for ${device}.`);
|
|
476
|
+
const id = `${fields[0]}:${fields[1]}:${device}`;
|
|
477
|
+
if (results.has(id)) throw new Error(`Duplicate diskstats for ${device}.`);
|
|
478
|
+
results.set(id, { read, write });
|
|
479
|
+
}
|
|
480
|
+
if (results.size !== selected.size)
|
|
481
|
+
throw new Error("Block device inventory changed while collecting diskstats.");
|
|
482
|
+
return results;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function readBounded(path: string, limit: number): string {
|
|
486
|
+
const fd = openSync(path, "r");
|
|
487
|
+
try {
|
|
488
|
+
const buffer = Buffer.alloc(limit + 1);
|
|
489
|
+
let offset = 0;
|
|
490
|
+
while (offset < buffer.length) {
|
|
491
|
+
const count = readSync(fd, buffer, offset, buffer.length - offset, null);
|
|
492
|
+
if (count === 0) break;
|
|
493
|
+
offset += count;
|
|
494
|
+
}
|
|
495
|
+
if (offset > limit) throw new Error("Resource counter file exceeds the collector limit.");
|
|
496
|
+
return buffer.toString("utf8", 0, offset);
|
|
497
|
+
} finally {
|
|
498
|
+
closeSync(fd);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function errorCode(error: unknown): string | undefined {
|
|
503
|
+
return error && typeof error === "object" && "code" in error ? String(error.code) : undefined;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function missingError(error: unknown): boolean {
|
|
507
|
+
return ["ENOENT", "ENOTDIR"].includes(errorCode(error) ?? "");
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function unsupportedError(error: unknown): boolean {
|
|
511
|
+
return ["ENOSYS", "ENOTSUP", "EOPNOTSUPP"].includes(errorCode(error) ?? "");
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function errorReason(error: unknown): string {
|
|
515
|
+
return (
|
|
516
|
+
errorCode(error) ?? (error instanceof Error ? error.message : "Resource collection failed.")
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function unavailableIo(state: ResourceSupportState, reason: string): ResourceIoSample {
|
|
521
|
+
return { state, read_bytes_per_second: null, write_bytes_per_second: null, reason };
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function round(value: number): number {
|
|
525
|
+
return Math.round(value * 100) / 100;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function nullableRound(value: number | null): number | null {
|
|
529
|
+
return value === null ? null : round(value);
|
|
530
|
+
}
|
|
@@ -65,6 +65,11 @@ export {
|
|
|
65
65
|
export const SEMANTIC_SERVICE_DEFAULT_DEBOUNCE_MS = 5_000;
|
|
66
66
|
export const SEMANTIC_SERVICE_DEFAULT_WAKE_MS = 1_000;
|
|
67
67
|
export const SEMANTIC_SERVICE_DEFAULT_HEARTBEAT_MS = 5_000;
|
|
68
|
+
// Ceiling for the wake timer while sweeps find nothing. The timer is only the fallback
|
|
69
|
+
// behind fs.watch on the active ledger, so backing it off delays nothing that writes an
|
|
70
|
+
// event; it stops an idle daemon from re-reading the manifest and agent documents once a
|
|
71
|
+
// second all night. Observed 2026-09-06: 142,637 sweeps in 70 h, 10,291 of them passes.
|
|
72
|
+
export const SEMANTIC_SERVICE_DEFAULT_IDLE_WAKE_MAX_MS = 30_000;
|
|
68
73
|
const FOREIGN_STATUS_STALE_MS = 2 * 60_000;
|
|
69
74
|
const MAX_FILE_BYTES = 512 * 1024;
|
|
70
75
|
const MAX_LOG_BYTES = 512 * 1024;
|
|
@@ -96,6 +101,7 @@ export interface RunSemanticServiceDaemonInput {
|
|
|
96
101
|
callsPerHour?: number;
|
|
97
102
|
debounceMs?: number;
|
|
98
103
|
wakeIntervalMs?: number;
|
|
104
|
+
idleWakeMaxMs?: number;
|
|
99
105
|
heartbeatIntervalMs?: number;
|
|
100
106
|
maxSweeps?: number;
|
|
101
107
|
now?: () => Date;
|
|
@@ -251,6 +257,13 @@ export async function runSemanticServiceDaemon(
|
|
|
251
257
|
input.heartbeatIntervalMs ?? SEMANTIC_SERVICE_DEFAULT_HEARTBEAT_MS,
|
|
252
258
|
"heartbeat interval",
|
|
253
259
|
);
|
|
260
|
+
const idleWakeMaxMs = Math.max(
|
|
261
|
+
wakeIntervalMs,
|
|
262
|
+
positiveInterval(
|
|
263
|
+
input.idleWakeMaxMs ?? SEMANTIC_SERVICE_DEFAULT_IDLE_WAKE_MAX_MS,
|
|
264
|
+
"idle wake ceiling",
|
|
265
|
+
),
|
|
266
|
+
);
|
|
254
267
|
const readSince = input.readSince ?? readLedgerV3Since;
|
|
255
268
|
const runOnce =
|
|
256
269
|
input.runOnce ??
|
|
@@ -279,6 +292,8 @@ export async function runSemanticServiceDaemon(
|
|
|
279
292
|
};
|
|
280
293
|
let stopRequested = false;
|
|
281
294
|
let dirtySince: number | undefined;
|
|
295
|
+
let idleSweeps = 0;
|
|
296
|
+
let wakeEarly: (() => void) | undefined;
|
|
282
297
|
let lastLoggedErrorCode: string | undefined;
|
|
283
298
|
let lastLoggedErrorAt = Number.NEGATIVE_INFINITY;
|
|
284
299
|
const writeStatus = (): void => {
|
|
@@ -289,6 +304,8 @@ export async function runSemanticServiceDaemon(
|
|
|
289
304
|
stopRequested = true;
|
|
290
305
|
status.state = "stopping";
|
|
291
306
|
writeStatus();
|
|
307
|
+
// A backed-off wait must not hold a stop request for up to the idle ceiling.
|
|
308
|
+
wakeEarly?.();
|
|
292
309
|
};
|
|
293
310
|
process.on("SIGINT", requestStop);
|
|
294
311
|
process.on("SIGTERM", requestStop);
|
|
@@ -306,6 +323,7 @@ export async function runSemanticServiceDaemon(
|
|
|
306
323
|
while (!stopRequested && !existsSync(paths.stop)) {
|
|
307
324
|
const sweepAt = now();
|
|
308
325
|
status.sweep_count += 1;
|
|
326
|
+
let sawWork = false;
|
|
309
327
|
try {
|
|
310
328
|
const before = safeManifest(coordRoot);
|
|
311
329
|
let read = readSince(coordRoot, before?.cursor, { authority: "active" });
|
|
@@ -313,6 +331,7 @@ export async function runSemanticServiceDaemon(
|
|
|
313
331
|
read = readSince(coordRoot, undefined, { authority: "active" });
|
|
314
332
|
}
|
|
315
333
|
requireCompleteLedger(read);
|
|
334
|
+
if (read.events.length > 0) sawWork = true;
|
|
316
335
|
if (read.events.length > 0 || !before?.cursor) {
|
|
317
336
|
dirtySince ??= sweepAt.getTime();
|
|
318
337
|
}
|
|
@@ -342,6 +361,7 @@ export async function runSemanticServiceDaemon(
|
|
|
342
361
|
shouldStop: () => stopRequested || existsSync(paths.stop),
|
|
343
362
|
});
|
|
344
363
|
status.pass_count += 1;
|
|
364
|
+
sawWork = true;
|
|
345
365
|
status.model_calls += report.model_calls;
|
|
346
366
|
status.cache_hits += report.cache_hits;
|
|
347
367
|
status.process_usage = mergeSemanticUsageAggregates(
|
|
@@ -392,10 +412,19 @@ export async function runSemanticServiceDaemon(
|
|
|
392
412
|
}
|
|
393
413
|
status.last_sweep_at = now().toISOString();
|
|
394
414
|
writeStatus();
|
|
415
|
+
idleSweeps = sawWork ? 0 : idleSweeps + 1;
|
|
395
416
|
if (input.maxSweeps !== undefined && status.sweep_count >= input.maxSweeps) break;
|
|
396
417
|
if (!stopRequested && !existsSync(paths.stop)) {
|
|
397
|
-
|
|
398
|
-
|
|
418
|
+
// Double the fallback timer per idle sweep, up to the ceiling; any sweep that
|
|
419
|
+
// sees events or runs a pass snaps it back to the configured interval.
|
|
420
|
+
const waitMs = Math.min(idleWakeMaxMs, wakeIntervalMs * 2 ** Math.min(idleSweeps, 20));
|
|
421
|
+
if (input.waitForWake) await input.waitForWake(waitMs);
|
|
422
|
+
else {
|
|
423
|
+
await waitForLedgerWake(eventV3ActiveWatchPath(coordRoot), waitMs, (wake) => {
|
|
424
|
+
wakeEarly = wake;
|
|
425
|
+
});
|
|
426
|
+
wakeEarly = undefined;
|
|
427
|
+
}
|
|
399
428
|
}
|
|
400
429
|
}
|
|
401
430
|
} finally {
|
|
@@ -566,7 +595,11 @@ function pidAlive(pid: number): boolean {
|
|
|
566
595
|
}
|
|
567
596
|
}
|
|
568
597
|
|
|
569
|
-
async function waitForLedgerWake(
|
|
598
|
+
async function waitForLedgerWake(
|
|
599
|
+
path: string,
|
|
600
|
+
milliseconds: number,
|
|
601
|
+
onWait?: (wake: () => void) => void,
|
|
602
|
+
): Promise<void> {
|
|
570
603
|
await new Promise<void>((done) => {
|
|
571
604
|
let settled = false;
|
|
572
605
|
let watcher: ReturnType<typeof watch> | undefined;
|
|
@@ -583,6 +616,7 @@ async function waitForLedgerWake(path: string, milliseconds: number): Promise<vo
|
|
|
583
616
|
} catch {
|
|
584
617
|
// The timer is the polling fallback when the active file does not exist yet.
|
|
585
618
|
}
|
|
619
|
+
onWait?.(finish);
|
|
586
620
|
});
|
|
587
621
|
}
|
|
588
622
|
|
|
@@ -395,6 +395,42 @@ function durableHistoryFamilies(): HarneryStorageFamily[] {
|
|
|
395
395
|
);
|
|
396
396
|
return [
|
|
397
397
|
...histories,
|
|
398
|
+
family({
|
|
399
|
+
id: "agent-mailbox-history",
|
|
400
|
+
owner: "agent mailbox service",
|
|
401
|
+
storage_class: "durable-object-history",
|
|
402
|
+
roots: (context) => [subtree(context, ".harnery/mailbox")],
|
|
403
|
+
format: "jsonl",
|
|
404
|
+
durability: "crash-safe",
|
|
405
|
+
writer_model: "multi-process",
|
|
406
|
+
policy: ownerHistoryPolicy("agent-mailbox-history-v1"),
|
|
407
|
+
consumers: ["agent mailbox service", "prompt delivery", "storage inventory"],
|
|
408
|
+
provider: filesystemProvider("agent-mailbox-provider", "agent mailbox lifecycle"),
|
|
409
|
+
}),
|
|
410
|
+
family({
|
|
411
|
+
id: "workflow-approval-history",
|
|
412
|
+
owner: "workflow approval service",
|
|
413
|
+
storage_class: "durable-object-history",
|
|
414
|
+
roots: (context) => [subtree(context, ".harnery/approvals")],
|
|
415
|
+
format: "json",
|
|
416
|
+
durability: "crash-safe",
|
|
417
|
+
writer_model: "object-owned",
|
|
418
|
+
policy: ownerHistoryPolicy("workflow-approval-history-v1"),
|
|
419
|
+
consumers: ["workflow approval service", "governor service", "storage inventory"],
|
|
420
|
+
provider: filesystemProvider("workflow-approval-provider", "workflow approval lifecycle"),
|
|
421
|
+
}),
|
|
422
|
+
family({
|
|
423
|
+
id: "agent-name-history",
|
|
424
|
+
owner: "agent identity service",
|
|
425
|
+
storage_class: "durable-object-history",
|
|
426
|
+
roots: (context) => [exact(context, ".harnery/.name-history", "file")],
|
|
427
|
+
format: "jsonl",
|
|
428
|
+
durability: "crash-safe",
|
|
429
|
+
writer_model: "multi-process",
|
|
430
|
+
policy: ownerHistoryPolicy("agent-name-history-v1"),
|
|
431
|
+
consumers: ["agent identity service", "session rendering", "storage inventory"],
|
|
432
|
+
provider: filesystemProvider("agent-name-history-provider", "agent identity lifecycle"),
|
|
433
|
+
}),
|
|
398
434
|
family({
|
|
399
435
|
id: "project-configuration",
|
|
400
436
|
owner: "Harnery configuration service",
|
|
@@ -432,6 +468,11 @@ function durableHistoryFamilies(): HarneryStorageFamily[] {
|
|
|
432
468
|
function cacheFamilies(): HarneryStorageFamily[] {
|
|
433
469
|
const definitions = [
|
|
434
470
|
["active-agent-projection", ".harnery/active", "Event Ledger V3 and native heartbeats"],
|
|
471
|
+
[
|
|
472
|
+
"storage-dashboard-cache",
|
|
473
|
+
".harnery/cache/storage-footprint",
|
|
474
|
+
"canonical metadata-only storage inventory",
|
|
475
|
+
],
|
|
435
476
|
["pid-map-cache", ".harnery/pid-map", "active native process observations"],
|
|
436
477
|
["remote-presence-cache", ".harnery/presence/remote", "presence relay observations"],
|
|
437
478
|
["event-v3-live-display", ".harnery/live/v3", "Event Ledger V3 canonical events"],
|