@quickchatai/quickreview 0.0.0-stage → 1.1.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/LICENSE +23 -0
- package/README.md +385 -2
- package/THIRD-PARTY-NOTICES.md +134 -0
- package/agents/pass-abstraction.md +64 -0
- package/agents/pass-ask-human.md +134 -0
- package/agents/pass-backcompat.md +67 -0
- package/agents/pass-baseline.md +61 -0
- package/agents/pass-behaviour-delta.md +64 -0
- package/agents/pass-bounds.md +36 -0
- package/agents/pass-claims.md +31 -0
- package/agents/pass-concurrency.md +168 -0
- package/agents/pass-consistency.md +57 -0
- package/agents/pass-contracts.md +30 -0
- package/agents/pass-data-safety.md +54 -0
- package/agents/pass-deferred.md +35 -0
- package/agents/pass-delivery.md +37 -0
- package/agents/pass-duplication.md +60 -0
- package/agents/pass-intent.md +63 -0
- package/agents/pass-lint-conformance.md +46 -0
- package/agents/pass-maintainability.md +67 -0
- package/agents/pass-placement.md +68 -0
- package/agents/pass-prod-risk.md +24 -0
- package/agents/pass-rollout.md +32 -0
- package/agents/pass-roundtrip.md +46 -0
- package/agents/pass-security.md +31 -0
- package/agents/pass-simplicity.md +68 -0
- package/agents/pass-statelessness.md +27 -0
- package/agents/pass-style.md +37 -0
- package/agents/pass-sweep.md +57 -0
- package/agents/pass-tests.md +58 -0
- package/agents/pass-ui.md +23 -0
- package/agents/pass-validate.md +50 -0
- package/agents/pass-widened.md +46 -0
- package/agents/scan/pass-scan.md +86 -0
- package/bin/qcr.js +11 -0
- package/config.example.toml +1067 -0
- package/deploy/ansible/README.md +356 -0
- package/deploy/ansible/group_vars/all.yml +212 -0
- package/deploy/ansible/inventory.example.ini +25 -0
- package/deploy/ansible/playbook.yml +360 -0
- package/deploy/collect-secrets.sh +289 -0
- package/deploy/collect-slack-secrets.mjs +1190 -0
- package/deploy/github-app-manifest.json +31 -0
- package/deploy/manifest.schema.json +75 -0
- package/deploy/qcr-update.sh +803 -0
- package/deploy/reviewer/AGENTS.override.md +12 -0
- package/dist/extensions/cli/bash-guard-ambient.js +42 -0
- package/dist/extensions/cli/bash-guard-ambient.js.map +1 -0
- package/dist/extensions/cli/bash-guard.js +5 -0
- package/dist/extensions/cli/bash-guard.js.map +1 -0
- package/dist/extensions/cli/companion-guard.js +5 -0
- package/dist/extensions/cli/companion-guard.js.map +1 -0
- package/dist/extensions/cli/debug-round.js +8 -0
- package/dist/extensions/cli/debug-round.js.map +1 -0
- package/dist/extensions/cli/inject-bridge.js +5 -0
- package/dist/extensions/cli/inject-bridge.js.map +1 -0
- package/dist/extensions/cli/ledger-pin.js +5 -0
- package/dist/extensions/cli/ledger-pin.js.map +1 -0
- package/dist/extensions/cli/lens.js +13 -0
- package/dist/extensions/cli/lens.js.map +1 -0
- package/dist/extensions/cli/muted-theme.js +5 -0
- package/dist/extensions/cli/muted-theme.js.map +1 -0
- package/dist/extensions/cli/onboarding.js +5 -0
- package/dist/extensions/cli/onboarding.js.map +1 -0
- package/dist/extensions/cli/setup.js +5 -0
- package/dist/extensions/cli/setup.js.map +1 -0
- package/dist/extensions/cli/tui-view.js +5 -0
- package/dist/extensions/cli/tui-view.js.map +1 -0
- package/dist/extensions/companion-guard.js +361 -0
- package/dist/extensions/companion-guard.js.map +1 -0
- package/dist/extensions/debug-round.js +204 -0
- package/dist/extensions/debug-round.js.map +1 -0
- package/dist/extensions/inject-bridge.js +603 -0
- package/dist/extensions/inject-bridge.js.map +1 -0
- package/dist/extensions/muted-theme.js +12 -0
- package/dist/extensions/muted-theme.js.map +1 -0
- package/dist/extensions/onboarding.js +175 -0
- package/dist/extensions/onboarding.js.map +1 -0
- package/dist/extensions/setup-replay.js +269 -0
- package/dist/extensions/setup-replay.js.map +1 -0
- package/dist/extensions/setup-scan.js +998 -0
- package/dist/extensions/setup-scan.js.map +1 -0
- package/dist/extensions/setup-triage.js +175 -0
- package/dist/extensions/setup-triage.js.map +1 -0
- package/dist/extensions/setup.js +1160 -0
- package/dist/extensions/setup.js.map +1 -0
- package/dist/extensions/tui-view.js +389 -0
- package/dist/extensions/tui-view.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/client.js +376 -0
- package/dist/extensions/vendor/openviking-pi/client.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/config.js +189 -0
- package/dist/extensions/vendor/openviking-pi/config.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/index.js +421 -0
- package/dist/extensions/vendor/openviking-pi/index.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/lib/capture-adapter.mjs +86 -0
- package/dist/extensions/vendor/openviking-pi/lib/capture-adapter.mjs.map +1 -0
- package/dist/extensions/vendor/openviking-pi/lib/takeover-core.mjs +408 -0
- package/dist/extensions/vendor/openviking-pi/lib/takeover-core.mjs.map +1 -0
- package/dist/extensions/vendor/openviking-pi/lib/uri-guard-adapter.mjs +41 -0
- package/dist/extensions/vendor/openviking-pi/lib/uri-guard-adapter.mjs.map +1 -0
- package/dist/extensions/vendor/openviking-pi/qcr-hint.js +290 -0
- package/dist/extensions/vendor/openviking-pi/qcr-hint.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/qcr-honest.js +148 -0
- package/dist/extensions/vendor/openviking-pi/qcr-honest.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/qcr-hooks.js +68 -0
- package/dist/extensions/vendor/openviking-pi/qcr-hooks.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/qcr-leaf.js +77 -0
- package/dist/extensions/vendor/openviking-pi/qcr-leaf.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/qcr-lesson.js +216 -0
- package/dist/extensions/vendor/openviking-pi/qcr-lesson.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/qcr-namespaces.js +154 -0
- package/dist/extensions/vendor/openviking-pi/qcr-namespaces.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/qcr-recall.js +254 -0
- package/dist/extensions/vendor/openviking-pi/qcr-recall.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/qcr-shim.js +94 -0
- package/dist/extensions/vendor/openviking-pi/qcr-shim.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/qcr-write.js +333 -0
- package/dist/extensions/vendor/openviking-pi/qcr-write.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/recall.js +108 -0
- package/dist/extensions/vendor/openviking-pi/recall.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/shared/capture-utils.mjs +529 -0
- package/dist/extensions/vendor/openviking-pi/shared/capture-utils.mjs.map +1 -0
- package/dist/extensions/vendor/openviking-pi/shared/credentials.mjs +205 -0
- package/dist/extensions/vendor/openviking-pi/shared/credentials.mjs.map +1 -0
- package/dist/extensions/vendor/openviking-pi/shared/pending-queue.mjs +442 -0
- package/dist/extensions/vendor/openviking-pi/shared/pending-queue.mjs.map +1 -0
- package/dist/extensions/vendor/openviking-pi/shared/profile-inject.mjs +286 -0
- package/dist/extensions/vendor/openviking-pi/shared/profile-inject.mjs.map +1 -0
- package/dist/extensions/vendor/openviking-pi/shared/recall-compress-core.mjs +189 -0
- package/dist/extensions/vendor/openviking-pi/shared/recall-compress-core.mjs.map +1 -0
- package/dist/extensions/vendor/openviking-pi/shared/recall-core.mjs +580 -0
- package/dist/extensions/vendor/openviking-pi/shared/recall-core.mjs.map +1 -0
- package/dist/extensions/vendor/openviking-pi/shared/retryable.mjs +11 -0
- package/dist/extensions/vendor/openviking-pi/shared/retryable.mjs.map +1 -0
- package/dist/extensions/vendor/openviking-pi/shared/session-model.mjs +63 -0
- package/dist/extensions/vendor/openviking-pi/shared/session-model.mjs.map +1 -0
- package/dist/extensions/vendor/openviking-pi/shared/uri-guard.mjs +58 -0
- package/dist/extensions/vendor/openviking-pi/shared/uri-guard.mjs.map +1 -0
- package/dist/extensions/vendor/openviking-pi/shared/workspace-peer.mjs +16 -0
- package/dist/extensions/vendor/openviking-pi/shared/workspace-peer.mjs.map +1 -0
- package/dist/extensions/vendor/openviking-pi/sync.js +127 -0
- package/dist/extensions/vendor/openviking-pi/sync.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/takeover.js +25 -0
- package/dist/extensions/vendor/openviking-pi/takeover.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/tools.js +417 -0
- package/dist/extensions/vendor/openviking-pi/tools.js.map +1 -0
- package/dist/src/anomalies.js +483 -0
- package/dist/src/anomalies.js.map +1 -0
- package/dist/src/answerworker.js +180 -0
- package/dist/src/answerworker.js.map +1 -0
- package/dist/src/appstamp.js +68 -0
- package/dist/src/appstamp.js.map +1 -0
- package/dist/src/archive/admission.js +3 -0
- package/dist/src/archive/admission.js.map +1 -0
- package/dist/src/archive/archivedb.js +474 -0
- package/dist/src/archive/archivedb.js.map +1 -0
- package/dist/src/archive/archiver.js +1770 -0
- package/dist/src/archive/archiver.js.map +1 -0
- package/dist/src/archive/archivewrite.js +198 -0
- package/dist/src/archive/archivewrite.js.map +1 -0
- package/dist/src/archive/bundle.js +266 -0
- package/dist/src/archive/bundle.js.map +1 -0
- package/dist/src/archive/dbmove.js +638 -0
- package/dist/src/archive/dbmove.js.map +1 -0
- package/dist/src/archive/duplicatecounts.js +80 -0
- package/dist/src/archive/duplicatecounts.js.map +1 -0
- package/dist/src/archive/eligibility.js +113 -0
- package/dist/src/archive/eligibility.js.map +1 -0
- package/dist/src/archive/extract.js +288 -0
- package/dist/src/archive/extract.js.map +1 -0
- package/dist/src/archive/fsutil.js +330 -0
- package/dist/src/archive/fsutil.js.map +1 -0
- package/dist/src/archive/gzfile.js +277 -0
- package/dist/src/archive/gzfile.js.map +1 -0
- package/dist/src/archive/kinds.js +22 -0
- package/dist/src/archive/kinds.js.map +1 -0
- package/dist/src/archive/livecheckpoint.js +178 -0
- package/dist/src/archive/livecheckpoint.js.map +1 -0
- package/dist/src/archive/liveretention.js +145 -0
- package/dist/src/archive/liveretention.js.map +1 -0
- package/dist/src/archive/manifest.js +284 -0
- package/dist/src/archive/manifest.js.map +1 -0
- package/dist/src/archive/overseer.js +283 -0
- package/dist/src/archive/overseer.js.map +1 -0
- package/dist/src/archive/piglobal.js +129 -0
- package/dist/src/archive/piglobal.js.map +1 -0
- package/dist/src/archive/plan.js +398 -0
- package/dist/src/archive/plan.js.map +1 -0
- package/dist/src/archive/prarchive.js +831 -0
- package/dist/src/archive/prarchive.js.map +1 -0
- package/dist/src/archive/prfiles.js +303 -0
- package/dist/src/archive/prfiles.js.map +1 -0
- package/dist/src/archive/report.js +169 -0
- package/dist/src/archive/report.js.map +1 -0
- package/dist/src/archive/restore.js +276 -0
- package/dist/src/archive/restore.js.map +1 -0
- package/dist/src/archive/rows.js +106 -0
- package/dist/src/archive/rows.js.map +1 -0
- package/dist/src/archive/seal.js +480 -0
- package/dist/src/archive/seal.js.map +1 -0
- package/dist/src/archive/status.js +528 -0
- package/dist/src/archive/status.js.map +1 -0
- package/dist/src/archive/storepass.js +409 -0
- package/dist/src/archive/storepass.js.map +1 -0
- package/dist/src/backfill.js +217 -0
- package/dist/src/backfill.js.map +1 -0
- package/dist/src/bench/cost.js +87 -0
- package/dist/src/bench/exec.js +76 -0
- package/dist/src/bench/values.js +14 -0
- package/dist/src/botcontrol.js +127 -0
- package/dist/src/botcontrol.js.map +1 -0
- package/dist/src/botworker.js +805 -0
- package/dist/src/botworker.js.map +1 -0
- package/dist/src/capture.js +234 -0
- package/dist/src/capture.js.map +1 -0
- package/dist/src/cases.js +1491 -0
- package/dist/src/cases.js.map +1 -0
- package/dist/src/channelfs.js +27 -0
- package/dist/src/channelfs.js.map +1 -0
- package/dist/src/checklist.js +312 -0
- package/dist/src/checklist.js.map +1 -0
- package/dist/src/ciretry.js +91 -0
- package/dist/src/ciretry.js.map +1 -0
- package/dist/src/cli.js +1916 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/cliops.js +333 -0
- package/dist/src/cliops.js.map +1 -0
- package/dist/src/clonedir.js +15 -0
- package/dist/src/clonedir.js.map +1 -0
- package/dist/src/commandeffects.js +43 -0
- package/dist/src/commandeffects.js.map +1 -0
- package/dist/src/commands.js +1312 -0
- package/dist/src/commands.js.map +1 -0
- package/dist/src/companioninbox.js +203 -0
- package/dist/src/companioninbox.js.map +1 -0
- package/dist/src/compare.js +673 -0
- package/dist/src/compare.js.map +1 -0
- package/dist/src/config.js +1027 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/configcheck.js +111 -0
- package/dist/src/configcheck.js.map +1 -0
- package/dist/src/coreext.js +43 -0
- package/dist/src/coreext.js.map +1 -0
- package/dist/src/daemon.js +929 -0
- package/dist/src/daemon.js.map +1 -0
- package/dist/src/debuground.js +731 -0
- package/dist/src/debuground.js.map +1 -0
- package/dist/src/debugroundpi.js +219 -0
- package/dist/src/debugroundpi.js.map +1 -0
- package/dist/src/diagnostics.js +14 -0
- package/dist/src/diagnostics.js.map +1 -0
- package/dist/src/digest.js +139 -0
- package/dist/src/digest.js.map +1 -0
- package/dist/src/dispatch.js +1726 -0
- package/dist/src/dispatch.js.map +1 -0
- package/dist/src/engine/answerguard.js +18 -0
- package/dist/src/engine/answerguard.js.map +1 -0
- package/dist/src/engine/botsdk.js +262 -0
- package/dist/src/engine/botsdk.js.map +1 -0
- package/dist/src/engine/caserunner-sdk.js +332 -0
- package/dist/src/engine/caserunner-sdk.js.map +1 -0
- package/dist/src/engine/debugpi.js +194 -0
- package/dist/src/engine/debugpi.js.map +1 -0
- package/dist/src/eod.js +473 -0
- package/dist/src/eod.js.map +1 -0
- package/dist/src/eodpinwatch.js +391 -0
- package/dist/src/eodpinwatch.js.map +1 -0
- package/dist/src/escalation.js +1009 -0
- package/dist/src/escalation.js.map +1 -0
- package/dist/src/eventloop.js +779 -0
- package/dist/src/eventloop.js.map +1 -0
- package/dist/src/execboundary.js +430 -0
- package/dist/src/execboundary.js.map +1 -0
- package/dist/src/feedback.js +915 -0
- package/dist/src/feedback.js.map +1 -0
- package/dist/src/findingscheck.js +200 -0
- package/dist/src/findingscheck.js.map +1 -0
- package/dist/src/fsdenied.js +26 -0
- package/dist/src/fsdenied.js.map +1 -0
- package/dist/src/ghread.js +181 -0
- package/dist/src/ghread.js.map +1 -0
- package/dist/src/github.js +1063 -0
- package/dist/src/github.js.map +1 -0
- package/dist/src/homes.js +103 -0
- package/dist/src/homes.js.map +1 -0
- package/dist/src/hostroute.js +24 -0
- package/dist/src/hostroute.js.map +1 -0
- package/dist/src/improvements.js +625 -0
- package/dist/src/improvements.js.map +1 -0
- package/dist/src/install.js +946 -0
- package/dist/src/install.js.map +1 -0
- package/dist/src/installkind.js +40 -0
- package/dist/src/installkind.js.map +1 -0
- package/dist/src/ipc/capability.js +286 -0
- package/dist/src/ipc/capability.js.map +1 -0
- package/dist/src/ipc/client.js +226 -0
- package/dist/src/ipc/client.js.map +1 -0
- package/dist/src/ipc/control.js +70 -0
- package/dist/src/ipc/control.js.map +1 -0
- package/dist/src/ipc/ghreadshim.js +77 -0
- package/dist/src/ipc/ghreadshim.js.map +1 -0
- package/dist/src/ipc/launcher.js +65 -0
- package/dist/src/ipc/launcher.js.map +1 -0
- package/dist/src/ipc/permits.js +293 -0
- package/dist/src/ipc/permits.js.map +1 -0
- package/dist/src/ipc/postshim.js +137 -0
- package/dist/src/ipc/postshim.js.map +1 -0
- package/dist/src/ipc/protocol.js +239 -0
- package/dist/src/ipc/protocol.js.map +1 -0
- package/dist/src/ipc/server.js +312 -0
- package/dist/src/ipc/server.js.map +1 -0
- package/dist/src/ipc/workerspec.js +148 -0
- package/dist/src/ipc/workerspec.js.map +1 -0
- package/dist/src/learner/learner.js +1272 -0
- package/dist/src/learner/learner.js.map +1 -0
- package/dist/src/learner/miner.js +948 -0
- package/dist/src/learner/miner.js.map +1 -0
- package/dist/src/learner/packet.js +216 -0
- package/dist/src/learner/packet.js.map +1 -0
- package/dist/src/learner/receipt.js +135 -0
- package/dist/src/learner/receipt.js.map +1 -0
- package/dist/src/learner/status.js +38 -0
- package/dist/src/learner/status.js.map +1 -0
- package/dist/src/ledgercheck.js +120 -0
- package/dist/src/ledgercheck.js.map +1 -0
- package/dist/src/lessonledger.js +451 -0
- package/dist/src/lessonledger.js.map +1 -0
- package/dist/src/lessonselect.js +639 -0
- package/dist/src/lessonselect.js.map +1 -0
- package/dist/src/lessonshadow.js +540 -0
- package/dist/src/lessonshadow.js.map +1 -0
- package/dist/src/levers.js +302 -0
- package/dist/src/levers.js.map +1 -0
- package/dist/src/live/projection.js +450 -0
- package/dist/src/live/projection.js.map +1 -0
- package/dist/src/live/publish.js +83 -0
- package/dist/src/live/publish.js.map +1 -0
- package/dist/src/live/read.js +60 -0
- package/dist/src/live/read.js.map +1 -0
- package/dist/src/livecli.js +288 -0
- package/dist/src/livecli.js.map +1 -0
- package/dist/src/loginhint.js +42 -0
- package/dist/src/loginhint.js.map +1 -0
- package/dist/src/loginrefresh/atomic.js +66 -0
- package/dist/src/loginrefresh/atomic.js.map +1 -0
- package/dist/src/loginrefresh/lock.js +93 -0
- package/dist/src/loginrefresh/lock.js.map +1 -0
- package/dist/src/loginrefresh/main.js +58 -0
- package/dist/src/loginrefresh/main.js.map +1 -0
- package/dist/src/loginrefresh/pirefresh.js +49 -0
- package/dist/src/loginrefresh/pirefresh.js.map +1 -0
- package/dist/src/loginrefresh/refresher.js +286 -0
- package/dist/src/loginrefresh/refresher.js.map +1 -0
- package/dist/src/loginrefresh/supervisor.js +253 -0
- package/dist/src/loginrefresh/supervisor.js.map +1 -0
- package/dist/src/loginrefresh/types.js +4 -0
- package/dist/src/loginrefresh/types.js.map +1 -0
- package/dist/src/memory/context.js +251 -0
- package/dist/src/memory/context.js.map +1 -0
- package/dist/src/memory/datafence.js +5 -0
- package/dist/src/memory/datafence.js.map +1 -0
- package/dist/src/memory/delivery.js +131 -0
- package/dist/src/memory/delivery.js.map +1 -0
- package/dist/src/memory/eventtags.js +81 -0
- package/dist/src/memory/eventtags.js.map +1 -0
- package/dist/src/memory/health.js +713 -0
- package/dist/src/memory/health.js.map +1 -0
- package/dist/src/memory/import.js +504 -0
- package/dist/src/memory/import.js.map +1 -0
- package/dist/src/memory/layout.js +252 -0
- package/dist/src/memory/layout.js.map +1 -0
- package/dist/src/memory/legacy-file.js +100 -0
- package/dist/src/memory/legacy-file.js.map +1 -0
- package/dist/src/memory/lessons.js +370 -0
- package/dist/src/memory/lessons.js.map +1 -0
- package/dist/src/memory/ovsession.js +449 -0
- package/dist/src/memory/ovsession.js.map +1 -0
- package/dist/src/memory/relayout.js +259 -0
- package/dist/src/memory/relayout.js.map +1 -0
- package/dist/src/memory/roundactivity.js +180 -0
- package/dist/src/memory/roundactivity.js.map +1 -0
- package/dist/src/memory/selection.js +690 -0
- package/dist/src/memory/selection.js.map +1 -0
- package/dist/src/memory/shadowstatus.js +69 -0
- package/dist/src/memory/shadowstatus.js.map +1 -0
- package/dist/src/memory/store.js +83 -0
- package/dist/src/memory/store.js.map +1 -0
- package/dist/src/memory/textidentity.js +8 -0
- package/dist/src/memory/textidentity.js.map +1 -0
- package/dist/src/metrics.js +773 -0
- package/dist/src/metrics.js.map +1 -0
- package/dist/src/missingconfig.js +8 -0
- package/dist/src/missingconfig.js.map +1 -0
- package/dist/src/npmverbs.js +226 -0
- package/dist/src/npmverbs.js.map +1 -0
- package/dist/src/onboard/choices.js +191 -0
- package/dist/src/onboard/choices.js.map +1 -0
- package/dist/src/onboard/engine.js +344 -0
- package/dist/src/onboard/engine.js.map +1 -0
- package/dist/src/onboard/launch.js +83 -0
- package/dist/src/onboard/launch.js.map +1 -0
- package/dist/src/onboard/steps/appslug.js +211 -0
- package/dist/src/onboard/steps/appslug.js.map +1 -0
- package/dist/src/onboard/steps/bringup.js +408 -0
- package/dist/src/onboard/steps/bringup.js.map +1 -0
- package/dist/src/onboard/steps/compose.js +343 -0
- package/dist/src/onboard/steps/compose.js.map +1 -0
- package/dist/src/onboard/steps/configdelivery.js +50 -0
- package/dist/src/onboard/steps/configdelivery.js.map +1 -0
- package/dist/src/onboard/steps/configfill.js +1148 -0
- package/dist/src/onboard/steps/configfill.js.map +1 -0
- package/dist/src/onboard/steps/deploysh.js +80 -0
- package/dist/src/onboard/steps/deploysh.js.map +1 -0
- package/dist/src/onboard/steps/embeddings.js +472 -0
- package/dist/src/onboard/steps/embeddings.js.map +1 -0
- package/dist/src/onboard/steps/escalation.js +571 -0
- package/dist/src/onboard/steps/escalation.js.map +1 -0
- package/dist/src/onboard/steps/githubapp.js +1092 -0
- package/dist/src/onboard/steps/githubapp.js.map +1 -0
- package/dist/src/onboard/steps/golive.js +506 -0
- package/dist/src/onboard/steps/golive.js.map +1 -0
- package/dist/src/onboard/steps/index.js +67 -0
- package/dist/src/onboard/steps/index.js.map +1 -0
- package/dist/src/onboard/steps/ingress.js +762 -0
- package/dist/src/onboard/steps/ingress.js.map +1 -0
- package/dist/src/onboard/steps/inventory.js +420 -0
- package/dist/src/onboard/steps/inventory.js.map +1 -0
- package/dist/src/onboard/steps/llmauth.js +893 -0
- package/dist/src/onboard/steps/llmauth.js.map +1 -0
- package/dist/src/onboard/steps/memory.js +1060 -0
- package/dist/src/onboard/steps/memory.js.map +1 -0
- package/dist/src/onboard/steps/memorykeys.js +43 -0
- package/dist/src/onboard/steps/memorykeys.js.map +1 -0
- package/dist/src/onboard/steps/preflight.js +172 -0
- package/dist/src/onboard/steps/preflight.js.map +1 -0
- package/dist/src/onboard/steps/proof.js +502 -0
- package/dist/src/onboard/steps/proof.js.map +1 -0
- package/dist/src/onboard/steps/ssh.js +300 -0
- package/dist/src/onboard/steps/ssh.js.map +1 -0
- package/dist/src/onboard/steps/topology.js +291 -0
- package/dist/src/onboard/steps/topology.js.map +1 -0
- package/dist/src/onboard/steps/transport.js +511 -0
- package/dist/src/onboard/steps/transport.js.map +1 -0
- package/dist/src/onboard/steps/verifyup.js +362 -0
- package/dist/src/onboard/steps/verifyup.js.map +1 -0
- package/dist/src/onboard/steps/vmdeploy.js +368 -0
- package/dist/src/onboard/steps/vmdeploy.js.map +1 -0
- package/dist/src/onboard/toml.js +53 -0
- package/dist/src/onboard/toml.js.map +1 -0
- package/dist/src/opencli.js +508 -0
- package/dist/src/opencli.js.map +1 -0
- package/dist/src/openpr.js +457 -0
- package/dist/src/openpr.js.map +1 -0
- package/dist/src/openviking/client.js +821 -0
- package/dist/src/openviking/client.js.map +1 -0
- package/dist/src/openviking/context.js +218 -0
- package/dist/src/openviking/context.js.map +1 -0
- package/dist/src/openviking/embedder.js +457 -0
- package/dist/src/openviking/embedder.js.map +1 -0
- package/dist/src/openviking/gapsink.js +38 -0
- package/dist/src/openviking/gapsink.js.map +1 -0
- package/dist/src/openviking/health.js +69 -0
- package/dist/src/openviking/health.js.map +1 -0
- package/dist/src/openviking/index.js +24 -0
- package/dist/src/openviking/index.js.map +1 -0
- package/dist/src/openviking/lessonfind.js +54 -0
- package/dist/src/openviking/lessonfind.js.map +1 -0
- package/dist/src/openviking/probe.js +89 -0
- package/dist/src/openviking/probe.js.map +1 -0
- package/dist/src/openviking/proc.js +95 -0
- package/dist/src/openviking/proc.js.map +1 -0
- package/dist/src/openviking/provisionreport.js +144 -0
- package/dist/src/openviking/provisionreport.js.map +1 -0
- package/dist/src/openviking/reembed.js +317 -0
- package/dist/src/openviking/reembed.js.map +1 -0
- package/dist/src/openviking/relayout.js +153 -0
- package/dist/src/openviking/relayout.js.map +1 -0
- package/dist/src/openviking/sidecar-main.js +39 -0
- package/dist/src/openviking/sidecar-main.js.map +1 -0
- package/dist/src/openviking/sidecar.js +79 -0
- package/dist/src/openviking/sidecar.js.map +1 -0
- package/dist/src/openviking/store.js +1758 -0
- package/dist/src/openviking/store.js.map +1 -0
- package/dist/src/openviking/supervisor.js +422 -0
- package/dist/src/openviking/supervisor.js.map +1 -0
- package/dist/src/openviking/types.js +5 -0
- package/dist/src/openviking/types.js.map +1 -0
- package/dist/src/operator-recovery.js +172 -0
- package/dist/src/operator-recovery.js.map +1 -0
- package/dist/src/operatormessage.js +44 -0
- package/dist/src/operatormessage.js.map +1 -0
- package/dist/src/outcomes.js +1361 -0
- package/dist/src/outcomes.js.map +1 -0
- package/dist/src/piauthcopy.js +5 -0
- package/dist/src/piauthcopy.js.map +1 -0
- package/dist/src/picli.js +45 -0
- package/dist/src/picli.js.map +1 -0
- package/dist/src/picwd.js +25 -0
- package/dist/src/picwd.js.map +1 -0
- package/dist/src/pilogin.js +35 -0
- package/dist/src/pilogin.js.map +1 -0
- package/dist/src/pingpong.js +1028 -0
- package/dist/src/pingpong.js.map +1 -0
- package/dist/src/posting-journal.js +441 -0
- package/dist/src/posting-journal.js.map +1 -0
- package/dist/src/posting-outbox.js +662 -0
- package/dist/src/posting-outbox.js.map +1 -0
- package/dist/src/posting-receipt.js +203 -0
- package/dist/src/posting-receipt.js.map +1 -0
- package/dist/src/posting.js +348 -0
- package/dist/src/posting.js.map +1 -0
- package/dist/src/postops.js +637 -0
- package/dist/src/postops.js.map +1 -0
- package/dist/src/pr-activity.js +11 -0
- package/dist/src/pr-activity.js.map +1 -0
- package/dist/src/pr-author.js +5 -0
- package/dist/src/pr-author.js.map +1 -0
- package/dist/src/pr-conversation.js +355 -0
- package/dist/src/pr-conversation.js.map +1 -0
- package/dist/src/pr-session-lifecycle.js +542 -0
- package/dist/src/pr-session-lifecycle.js.map +1 -0
- package/dist/src/preflight.js +341 -0
- package/dist/src/preflight.js.map +1 -0
- package/dist/src/priorresolve.js +275 -0
- package/dist/src/priorresolve.js.map +1 -0
- package/dist/src/qcrbin.js +180 -0
- package/dist/src/qcrbin.js.map +1 -0
- package/dist/src/recheck.js +1398 -0
- package/dist/src/recheck.js.map +1 -0
- package/dist/src/reconcile.js +820 -0
- package/dist/src/reconcile.js.map +1 -0
- package/dist/src/recordedfindings.js +15 -0
- package/dist/src/recordedfindings.js.map +1 -0
- package/dist/src/regression.js +546 -0
- package/dist/src/regression.js.map +1 -0
- package/dist/src/replay.js +598 -0
- package/dist/src/replay.js.map +1 -0
- package/dist/src/requiredpasses.js +113 -0
- package/dist/src/requiredpasses.js.map +1 -0
- package/dist/src/resumecheck.js +168 -0
- package/dist/src/resumecheck.js.map +1 -0
- package/dist/src/reviewcli.js +671 -0
- package/dist/src/reviewcli.js.map +1 -0
- package/dist/src/reviewcontext.js +22 -0
- package/dist/src/reviewcontext.js.map +1 -0
- package/dist/src/reviewdetach.js +574 -0
- package/dist/src/reviewdetach.js.map +1 -0
- package/dist/src/reviewhistory.js +284 -0
- package/dist/src/reviewhistory.js.map +1 -0
- package/dist/src/reviewhome.js +26 -0
- package/dist/src/reviewhome.js.map +1 -0
- package/dist/src/reviewmemory.js +110 -0
- package/dist/src/reviewmemory.js.map +1 -0
- package/dist/src/reviewrendercli.js +104 -0
- package/dist/src/reviewrendercli.js.map +1 -0
- package/dist/src/round-work.js +20 -0
- package/dist/src/round-work.js.map +1 -0
- package/dist/src/runhost.js +72 -0
- package/dist/src/runhost.js.map +1 -0
- package/dist/src/scanauth.js +25 -0
- package/dist/src/scanauth.js.map +1 -0
- package/dist/src/scanbox.js +846 -0
- package/dist/src/scanbox.js.map +1 -0
- package/dist/src/scancli.js +1074 -0
- package/dist/src/scancli.js.map +1 -0
- package/dist/src/scandriverlog.js +30 -0
- package/dist/src/scandriverlog.js.map +1 -0
- package/dist/src/session-bridge.js +75 -0
- package/dist/src/session-bridge.js.map +1 -0
- package/dist/src/session-command-payload.js +29 -0
- package/dist/src/session-command-payload.js.map +1 -0
- package/dist/src/session-command-supervisor.js +939 -0
- package/dist/src/session-command-supervisor.js.map +1 -0
- package/dist/src/sessionjsonl.js +85 -0
- package/dist/src/sessionjsonl.js.map +1 -0
- package/dist/src/sessions.js +541 -0
- package/dist/src/sessions.js.map +1 -0
- package/dist/src/setup/adapters.js +330 -0
- package/dist/src/setup/adapters.js.map +1 -0
- package/dist/src/setup/baselines.js +64 -0
- package/dist/src/setup/baselines.js.map +1 -0
- package/dist/src/setup/caserunner.js +554 -0
- package/dist/src/setup/caserunner.js.map +1 -0
- package/dist/src/setup/configcommit.js +358 -0
- package/dist/src/setup/configcommit.js.map +1 -0
- package/dist/src/setup/engine.js +672 -0
- package/dist/src/setup/engine.js.map +1 -0
- package/dist/src/setup/facts.js +593 -0
- package/dist/src/setup/facts.js.map +1 -0
- package/dist/src/setup/findings.js +948 -0
- package/dist/src/setup/findings.js.map +1 -0
- package/dist/src/setup/history.js +338 -0
- package/dist/src/setup/history.js.map +1 -0
- package/dist/src/setup/interview.js +185 -0
- package/dist/src/setup/interview.js.map +1 -0
- package/dist/src/setup/ledger.js +207 -0
- package/dist/src/setup/ledger.js.map +1 -0
- package/dist/src/setup/pins.js +43 -0
- package/dist/src/setup/pins.js.map +1 -0
- package/dist/src/setup/portrait.js +432 -0
- package/dist/src/setup/portrait.js.map +1 -0
- package/dist/src/setup/presentation.js +9 -0
- package/dist/src/setup/presentation.js.map +1 -0
- package/dist/src/setup/profile.js +417 -0
- package/dist/src/setup/profile.js.map +1 -0
- package/dist/src/setup/replay.js +240 -0
- package/dist/src/setup/replay.js.map +1 -0
- package/dist/src/setup/replaybuild.js +311 -0
- package/dist/src/setup/replaybuild.js.map +1 -0
- package/dist/src/setup/replayrun.js +806 -0
- package/dist/src/setup/replayrun.js.map +1 -0
- package/dist/src/setup/replaysource.js +169 -0
- package/dist/src/setup/replaysource.js.map +1 -0
- package/dist/src/setup/retune.js +333 -0
- package/dist/src/setup/retune.js.map +1 -0
- package/dist/src/setup/retunelog.js +51 -0
- package/dist/src/setup/retunelog.js.map +1 -0
- package/dist/src/setup/scan.js +725 -0
- package/dist/src/setup/scan.js.map +1 -0
- package/dist/src/setup/scanbuild.js +133 -0
- package/dist/src/setup/scanbuild.js.map +1 -0
- package/dist/src/setup/scanfacts.js +96 -0
- package/dist/src/setup/scanfacts.js.map +1 -0
- package/dist/src/setup/scanlease.js +251 -0
- package/dist/src/setup/scanlease.js.map +1 -0
- package/dist/src/setup/scanreport.js +289 -0
- package/dist/src/setup/scanreport.js.map +1 -0
- package/dist/src/setup/scanrun.js +1733 -0
- package/dist/src/setup/scanrun.js.map +1 -0
- package/dist/src/setup/scantree.js +257 -0
- package/dist/src/setup/scantree.js.map +1 -0
- package/dist/src/setup/stage2.js +617 -0
- package/dist/src/setup/stage2.js.map +1 -0
- package/dist/src/setup/stage2request.js +259 -0
- package/dist/src/setup/stage2request.js.map +1 -0
- package/dist/src/setup/survey.js +307 -0
- package/dist/src/setup/survey.js.map +1 -0
- package/dist/src/setup/surveyrun.js +549 -0
- package/dist/src/setup/surveyrun.js.map +1 -0
- package/dist/src/setup/textsniff.js +89 -0
- package/dist/src/setup/textsniff.js.map +1 -0
- package/dist/src/setup/trackersink.js +395 -0
- package/dist/src/setup/trackersink.js.map +1 -0
- package/dist/src/setup/triage.js +636 -0
- package/dist/src/setup/triage.js.map +1 -0
- package/dist/src/setup/tuning.js +585 -0
- package/dist/src/setup/tuning.js.map +1 -0
- package/dist/src/setup/unitcut.js +437 -0
- package/dist/src/setup/unitcut.js.map +1 -0
- package/dist/src/singleflight.js +72 -0
- package/dist/src/singleflight.js.map +1 -0
- package/dist/src/slack/announce.js +725 -0
- package/dist/src/slack/announce.js.map +1 -0
- package/dist/src/slack/api.js +227 -0
- package/dist/src/slack/api.js.map +1 -0
- package/dist/src/slack/messages.js +158 -0
- package/dist/src/slack/messages.js.map +1 -0
- package/dist/src/slack/origin.js +550 -0
- package/dist/src/slack/origin.js.map +1 -0
- package/dist/src/slack/policy.js +225 -0
- package/dist/src/slack/policy.js.map +1 -0
- package/dist/src/slack/reply.js +153 -0
- package/dist/src/slack/reply.js.map +1 -0
- package/dist/src/slack/router.js +189 -0
- package/dist/src/slack/router.js.map +1 -0
- package/dist/src/slack/socket.js +323 -0
- package/dist/src/slack/socket.js.map +1 -0
- package/dist/src/slack/types.js +11 -0
- package/dist/src/slack/types.js.map +1 -0
- package/dist/src/sqlitehint.js +30 -0
- package/dist/src/sqlitehint.js.map +1 -0
- package/dist/src/ssh-observation.js +25 -0
- package/dist/src/ssh-observation.js.map +1 -0
- package/dist/src/state.js +5869 -0
- package/dist/src/state.js.map +1 -0
- package/dist/src/statemachine.js +220 -0
- package/dist/src/statemachine.js.map +1 -0
- package/dist/src/stubs.js +52 -0
- package/dist/src/stubs.js.map +1 -0
- package/dist/src/takeover.js +506 -0
- package/dist/src/takeover.js.map +1 -0
- package/dist/src/takeoverpi.js +249 -0
- package/dist/src/takeoverpi.js.map +1 -0
- package/dist/src/terminal-failure.js +26 -0
- package/dist/src/terminal-failure.js.map +1 -0
- package/dist/src/thread-resolution-refusal.js +4 -0
- package/dist/src/thread-resolution-refusal.js.map +1 -0
- package/dist/src/tmux.js +126 -0
- package/dist/src/tmux.js.map +1 -0
- package/dist/src/trackerid.js +43 -0
- package/dist/src/trackerid.js.map +1 -0
- package/dist/src/transcriptindex.js +512 -0
- package/dist/src/transcriptindex.js.map +1 -0
- package/dist/src/tui/actions.js +404 -0
- package/dist/src/tui/actions.js.map +1 -0
- package/dist/src/tui/alerts.js +523 -0
- package/dist/src/tui/alerts.js.map +1 -0
- package/dist/src/tui/app.js +1835 -0
- package/dist/src/tui/app.js.map +1 -0
- package/dist/src/tui/archiveops.js +164 -0
- package/dist/src/tui/archiveops.js.map +1 -0
- package/dist/src/tui/autofile.js +191 -0
- package/dist/src/tui/autofile.js.map +1 -0
- package/dist/src/tui/board.js +1143 -0
- package/dist/src/tui/board.js.map +1 -0
- package/dist/src/tui/candidates.js +699 -0
- package/dist/src/tui/candidates.js.map +1 -0
- package/dist/src/tui/cellwidth.js +151 -0
- package/dist/src/tui/cellwidth.js.map +1 -0
- package/dist/src/tui/changelog-store.js +404 -0
- package/dist/src/tui/changelog-store.js.map +1 -0
- package/dist/src/tui/chord.js +26 -0
- package/dist/src/tui/chord.js.map +1 -0
- package/dist/src/tui/clipboard.js +81 -0
- package/dist/src/tui/clipboard.js.map +1 -0
- package/dist/src/tui/cmdbar.js +253 -0
- package/dist/src/tui/cmdbar.js.map +1 -0
- package/dist/src/tui/companion.js +1275 -0
- package/dist/src/tui/companion.js.map +1 -0
- package/dist/src/tui/companionplan.js +200 -0
- package/dist/src/tui/companionplan.js.map +1 -0
- package/dist/src/tui/companiontab.js +61 -0
- package/dist/src/tui/companiontab.js.map +1 -0
- package/dist/src/tui/confighome.js +105 -0
- package/dist/src/tui/confighome.js.map +1 -0
- package/dist/src/tui/connect.js +44 -0
- package/dist/src/tui/connect.js.map +1 -0
- package/dist/src/tui/deploy-ceremony.js +75 -0
- package/dist/src/tui/deploy-ceremony.js.map +1 -0
- package/dist/src/tui/deploy-drain.js +93 -0
- package/dist/src/tui/deploy-drain.js.map +1 -0
- package/dist/src/tui/deploy-process.js +234 -0
- package/dist/src/tui/deploy-process.js.map +1 -0
- package/dist/src/tui/deploytab.js +1263 -0
- package/dist/src/tui/deploytab.js.map +1 -0
- package/dist/src/tui/design.js +111 -0
- package/dist/src/tui/design.js.map +1 -0
- package/dist/src/tui/digesttab.js +148 -0
- package/dist/src/tui/digesttab.js.map +1 -0
- package/dist/src/tui/emptystate.js +32 -0
- package/dist/src/tui/emptystate.js.map +1 -0
- package/dist/src/tui/evidence.js +790 -0
- package/dist/src/tui/evidence.js.map +1 -0
- package/dist/src/tui/feed.js +415 -0
- package/dist/src/tui/feed.js.map +1 -0
- package/dist/src/tui/feedbacktab.js +907 -0
- package/dist/src/tui/feedbacktab.js.map +1 -0
- package/dist/src/tui/filters.js +72 -0
- package/dist/src/tui/filters.js.map +1 -0
- package/dist/src/tui/ghread.js +272 -0
- package/dist/src/tui/ghread.js.map +1 -0
- package/dist/src/tui/historypull.js +61 -0
- package/dist/src/tui/historypull.js.map +1 -0
- package/dist/src/tui/inboxpull.js +197 -0
- package/dist/src/tui/inboxpull.js.map +1 -0
- package/dist/src/tui/index.js +1425 -0
- package/dist/src/tui/index.js.map +1 -0
- package/dist/src/tui/issuesink.js +206 -0
- package/dist/src/tui/issuesink.js.map +1 -0
- package/dist/src/tui/jsonfile.js +32 -0
- package/dist/src/tui/jsonfile.js.map +1 -0
- package/dist/src/tui/keys.js +97 -0
- package/dist/src/tui/keys.js.map +1 -0
- package/dist/src/tui/layout.js +70 -0
- package/dist/src/tui/layout.js.map +1 -0
- package/dist/src/tui/learning.js +469 -0
- package/dist/src/tui/learning.js.map +1 -0
- package/dist/src/tui/lessonswitch.js +103 -0
- package/dist/src/tui/lessonswitch.js.map +1 -0
- package/dist/src/tui/livecontrol.js +167 -0
- package/dist/src/tui/livecontrol.js.map +1 -0
- package/dist/src/tui/memforward.js +310 -0
- package/dist/src/tui/memforward.js.map +1 -0
- package/dist/src/tui/memincident.js +287 -0
- package/dist/src/tui/memincident.js.map +1 -0
- package/dist/src/tui/memory.js +485 -0
- package/dist/src/tui/memory.js.map +1 -0
- package/dist/src/tui/memtab.js +513 -0
- package/dist/src/tui/memtab.js.map +1 -0
- package/dist/src/tui/metricstab.js +979 -0
- package/dist/src/tui/metricstab.js.map +1 -0
- package/dist/src/tui/model.js +104 -0
- package/dist/src/tui/model.js.map +1 -0
- package/dist/src/tui/mouse.js +83 -0
- package/dist/src/tui/mouse.js.map +1 -0
- package/dist/src/tui/msg.js +196 -0
- package/dist/src/tui/msg.js.map +1 -0
- package/dist/src/tui/nativecompanion.js +321 -0
- package/dist/src/tui/nativecompanion.js.map +1 -0
- package/dist/src/tui/openurl.js +17 -0
- package/dist/src/tui/openurl.js.map +1 -0
- package/dist/src/tui/paint.js +501 -0
- package/dist/src/tui/paint.js.map +1 -0
- package/dist/src/tui/panes.js +74 -0
- package/dist/src/tui/panes.js.map +1 -0
- package/dist/src/tui/pipane.js +166 -0
- package/dist/src/tui/pipane.js.map +1 -0
- package/dist/src/tui/poll-hooks.js +11 -0
- package/dist/src/tui/poll-hooks.js.map +1 -0
- package/dist/src/tui/poll.js +113 -0
- package/dist/src/tui/poll.js.map +1 -0
- package/dist/src/tui/proposals.js +715 -0
- package/dist/src/tui/proposals.js.map +1 -0
- package/dist/src/tui/prpause.js +132 -0
- package/dist/src/tui/prpause.js.map +1 -0
- package/dist/src/tui/reload.js +351 -0
- package/dist/src/tui/reload.js.map +1 -0
- package/dist/src/tui/remote.js +312 -0
- package/dist/src/tui/remote.js.map +1 -0
- package/dist/src/tui/rollover.js +767 -0
- package/dist/src/tui/rollover.js.map +1 -0
- package/dist/src/tui/scantab.js +1053 -0
- package/dist/src/tui/scantab.js.map +1 -0
- package/dist/src/tui/setupentry.js +81 -0
- package/dist/src/tui/setupentry.js.map +1 -0
- package/dist/src/tui/sidebar.js +602 -0
- package/dist/src/tui/sidebar.js.map +1 -0
- package/dist/src/tui/sshtriage.js +393 -0
- package/dist/src/tui/sshtriage.js.map +1 -0
- package/dist/src/tui/startup.js +172 -0
- package/dist/src/tui/startup.js.map +1 -0
- package/dist/src/tui/tabs.js +41 -0
- package/dist/src/tui/tabs.js.map +1 -0
- package/dist/src/tui/termmirror.js +217 -0
- package/dist/src/tui/termmirror.js.map +1 -0
- package/dist/src/tui/theme.js +95 -0
- package/dist/src/tui/theme.js.map +1 -0
- package/dist/src/tui/ticketstab.js +224 -0
- package/dist/src/tui/ticketstab.js.map +1 -0
- package/dist/src/tui/tmuxsocket.js +101 -0
- package/dist/src/tui/tmuxsocket.js.map +1 -0
- package/dist/src/tui/trackertickets.js +77 -0
- package/dist/src/tui/trackertickets.js.map +1 -0
- package/dist/src/tui/unwedge.js +245 -0
- package/dist/src/tui/unwedge.js.map +1 -0
- package/dist/src/tui/viewer.js +479 -0
- package/dist/src/tui/viewer.js.map +1 -0
- package/dist/src/tui/viewerinput.js +125 -0
- package/dist/src/tui/viewerinput.js.map +1 -0
- package/dist/src/updatenotice.js +143 -0
- package/dist/src/updatenotice.js.map +1 -0
- package/dist/src/usage.js +1007 -0
- package/dist/src/usage.js.map +1 -0
- package/dist/src/verdicts.js +3848 -0
- package/dist/src/verdicts.js.map +1 -0
- package/dist/src/version.js +58 -0
- package/dist/src/version.js.map +1 -0
- package/dist/src/wakes.js +411 -0
- package/dist/src/wakes.js.map +1 -0
- package/dist/src/webhook.js +725 -0
- package/dist/src/webhook.js.map +1 -0
- package/dist/src/webhookheal.js +238 -0
- package/dist/src/webhookheal.js.map +1 -0
- package/dist/src/wire.js +1801 -0
- package/dist/src/wire.js.map +1 -0
- package/dist/src/worker-session-host.js +531 -0
- package/dist/src/worker-session-host.js.map +1 -0
- package/dist/src/workerbreaker.js +112 -0
- package/dist/src/workerbreaker.js.map +1 -0
- package/dist/src/workercontrol.js +49 -0
- package/dist/src/workercontrol.js.map +1 -0
- package/dist/src/workerlease.js +351 -0
- package/dist/src/workerlease.js.map +1 -0
- package/dist/src/workerliveness.js +84 -0
- package/dist/src/workerliveness.js.map +1 -0
- package/dist/src/workerlog.js +65 -0
- package/dist/src/workerlog.js.map +1 -0
- package/dist/src/workerruntime.js +10 -0
- package/dist/src/workerruntime.js.map +1 -0
- package/dist/src/workersupervisor.js +1206 -0
- package/dist/src/workersupervisor.js.map +1 -0
- package/dist/src/workerusage.js +23 -0
- package/dist/src/workerusage.js.map +1 -0
- package/dist/src/workerworld.js +332 -0
- package/dist/src/workerworld.js.map +1 -0
- package/dist/src/worktrees.js +984 -0
- package/dist/src/worktrees.js.map +1 -0
- package/docker/Dockerfile +284 -0
- package/docker/Dockerfile.dockerignore +35 -0
- package/docker/README.md +221 -0
- package/docker/companion/Dockerfile +139 -0
- package/docker/companion/agents/evidence.md +34 -0
- package/docker/companion/agents/explore.md +26 -0
- package/docker/companion/agents/pass-survey.md +49 -0
- package/docker/companion/agents/study.md +26 -0
- package/docker/companion/entrypoint.sh +343 -0
- package/docker/companion/memory-health.mjs +269 -0
- package/docker/entrypoint.sh +782 -0
- package/docker/memory/Dockerfile +191 -0
- package/docker/memory/Dockerfile.dockerignore +20 -0
- package/docker/memory/README.md +60 -0
- package/docker/memory/entrypoint.sh +163 -0
- package/docker/setid/qcr-setid.c +109 -0
- package/docker/tunnel/Dockerfile.cloudflared +51 -0
- package/docker/tunnel/Dockerfile.ngrok +40 -0
- package/docker/tunnel/sidecar.mjs +411 -0
- package/docker/vm/tmux.conf +26 -0
- package/docker-compose.yml +348 -0
- package/extensions/cli/bash-guard-ambient.ts +71 -0
- package/extensions/cli/bash-guard.ts +12 -0
- package/extensions/cli/companion-guard.ts +8 -0
- package/extensions/cli/debug-round.ts +9 -0
- package/extensions/cli/inject-bridge.ts +8 -0
- package/extensions/cli/ledger-pin.ts +13 -0
- package/extensions/cli/lens.ts +22 -0
- package/extensions/cli/muted-theme.ts +9 -0
- package/extensions/cli/onboarding.ts +8 -0
- package/extensions/cli/setup.ts +8 -0
- package/extensions/cli/tui-view.ts +8 -0
- package/extensions/companion-guard.ts +411 -0
- package/extensions/debug-round.ts +281 -0
- package/extensions/inject-bridge.ts +630 -0
- package/extensions/inline-bash.ts +102 -0
- package/extensions/muted-theme.json +68 -0
- package/extensions/muted-theme.ts +23 -0
- package/extensions/onboarding.ts +233 -0
- package/extensions/setup-replay.ts +336 -0
- package/extensions/setup-scan.ts +1236 -0
- package/extensions/setup-triage.ts +190 -0
- package/extensions/setup.ts +1428 -0
- package/extensions/tui-view.ts +489 -0
- package/extensions/vendor/openviking-pi/LICENSE +201 -0
- package/extensions/vendor/openviking-pi/NOTICE +68 -0
- package/extensions/vendor/openviking-pi/README.md +550 -0
- package/extensions/vendor/openviking-pi/client.ts +588 -0
- package/extensions/vendor/openviking-pi/config.json +28 -0
- package/extensions/vendor/openviking-pi/config.ts +271 -0
- package/extensions/vendor/openviking-pi/index.ts +523 -0
- package/extensions/vendor/openviking-pi/lib/capture-adapter.d.mts +10 -0
- package/extensions/vendor/openviking-pi/lib/capture-adapter.mjs +88 -0
- package/extensions/vendor/openviking-pi/lib/takeover-core.d.mts +80 -0
- package/extensions/vendor/openviking-pi/lib/takeover-core.mjs +406 -0
- package/extensions/vendor/openviking-pi/lib/uri-guard-adapter.d.mts +1 -0
- package/extensions/vendor/openviking-pi/lib/uri-guard-adapter.mjs +42 -0
- package/extensions/vendor/openviking-pi/patches/0000-vendoring-and-build-conformance.patch +726 -0
- package/extensions/vendor/openviking-pi/patches/0001-remove-session-before-compact-handler.patch +101 -0
- package/extensions/vendor/openviking-pi/patches/0002-no-ambient-work-in-subagent-leaves.patch +202 -0
- package/extensions/vendor/openviking-pi/patches/0003-never-auto-create-openviking-sessions.patch +120 -0
- package/extensions/vendor/openviking-pi/patches/0004-viking-write-durable-batch-write.patch +521 -0
- package/extensions/vendor/openviking-pi/patches/0005-second-memory-namespace.patch +786 -0
- package/extensions/vendor/openviking-pi/patches/0006-writes-that-stick-and-honest-errors.patch +996 -0
- package/extensions/vendor/openviking-pi/patches/0007-bounded-recall-no-commit.patch +794 -0
- package/extensions/vendor/openviking-pi/patches/0008-pr-derived-recall.patch +379 -0
- package/extensions/vendor/openviking-pi/patches/0009-pr-session-tools-in-round-areas.patch +375 -0
- package/extensions/vendor/openviking-pi/patches/0010-typed-lessons.patch +448 -0
- package/extensions/vendor/openviking-pi/patches/0011-review-session-memory-boundary.patch +382 -0
- package/extensions/vendor/openviking-pi/patches/0012-typecheck-against-the-real-pi.patch +129 -0
- package/extensions/vendor/openviking-pi/patches/0013-createopenviking-factory.patch +1168 -0
- package/extensions/vendor/openviking-pi/qcr-hint.ts +313 -0
- package/extensions/vendor/openviking-pi/qcr-honest.ts +183 -0
- package/extensions/vendor/openviking-pi/qcr-hooks.ts +87 -0
- package/extensions/vendor/openviking-pi/qcr-leaf.ts +78 -0
- package/extensions/vendor/openviking-pi/qcr-lesson.ts +231 -0
- package/extensions/vendor/openviking-pi/qcr-namespaces.ts +198 -0
- package/extensions/vendor/openviking-pi/qcr-recall.ts +288 -0
- package/extensions/vendor/openviking-pi/qcr-shim.ts +142 -0
- package/extensions/vendor/openviking-pi/qcr-write.ts +427 -0
- package/extensions/vendor/openviking-pi/recall.ts +149 -0
- package/extensions/vendor/openviking-pi/shared/capture-utils.d.mts +9 -0
- package/extensions/vendor/openviking-pi/shared/capture-utils.mjs +515 -0
- package/extensions/vendor/openviking-pi/shared/credentials.d.mts +14 -0
- package/extensions/vendor/openviking-pi/shared/credentials.mjs +228 -0
- package/extensions/vendor/openviking-pi/shared/pending-queue.d.mts +19 -0
- package/extensions/vendor/openviking-pi/shared/pending-queue.mjs +458 -0
- package/extensions/vendor/openviking-pi/shared/profile-inject.d.mts +17 -0
- package/extensions/vendor/openviking-pi/shared/profile-inject.mjs +294 -0
- package/extensions/vendor/openviking-pi/shared/recall-compress-core.mjs +195 -0
- package/extensions/vendor/openviking-pi/shared/recall-core.d.mts +13 -0
- package/extensions/vendor/openviking-pi/shared/recall-core.mjs +609 -0
- package/extensions/vendor/openviking-pi/shared/retryable.mjs +9 -0
- package/extensions/vendor/openviking-pi/shared/session-model.d.mts +3 -0
- package/extensions/vendor/openviking-pi/shared/session-model.mjs +57 -0
- package/extensions/vendor/openviking-pi/shared/uri-guard.mjs +56 -0
- package/extensions/vendor/openviking-pi/shared/workspace-peer.d.mts +5 -0
- package/extensions/vendor/openviking-pi/shared/workspace-peer.mjs +16 -0
- package/extensions/vendor/openviking-pi/sync.ts +163 -0
- package/extensions/vendor/openviking-pi/takeover.ts +36 -0
- package/extensions/vendor/openviking-pi/tools.ts +479 -0
- package/extensions/vendor/openviking-pi/upstream.json +48 -0
- package/package.json +138 -4
- package/profiles/default/README.md +21 -0
- package/profiles/default/addenda/.gitkeep +0 -0
- package/profiles/default/profile.toml +17 -0
- package/profiles/example-django/README.md +21 -0
- package/profiles/example-django/addenda/10-migration-discipline.md +13 -0
- package/profiles/example-django/addenda/20-new-column-nullability.md +12 -0
- package/profiles/example-django/addenda/30-test-doctrine.md +12 -0
- package/profiles/example-django/addenda/40-orm-usage.md +19 -0
- package/profiles/example-django/addenda/50-declared-shape.md +10 -0
- package/profiles/example-django/addenda/60-config-placement.md +14 -0
- package/profiles/example-django/heuristics.toml +24 -0
- package/profiles/example-django/profile.toml +9 -0
- package/profiles/fast/README.md +24 -0
- package/profiles/fast/profile.toml +59 -0
- package/profiles/minimal/README.md +17 -0
- package/profiles/minimal/addenda/.gitkeep +0 -0
- package/profiles/minimal/profile.toml +8 -0
- package/profiles/snappy/README.md +24 -0
- package/profiles/snappy/addenda/10-race-as-question.md +6 -0
- package/profiles/snappy/profile.toml +46 -0
- package/prompts/companion-day-open.md +46 -0
- package/prompts/companion-first-run.md +104 -0
- package/prompts/companion.md +821 -0
- package/prompts/conversation-system.md +117 -0
- package/prompts/eod-sweep.md +120 -0
- package/prompts/onboarding.md +74 -0
- package/prompts/pass-common.md +52 -0
- package/prompts/pass-quality-common.md +22 -0
- package/prompts/round-first.md +874 -0
- package/prompts/round-follow-up.md +991 -0
- package/prompts/round-re-review.md +889 -0
- package/prompts/round-recheck.md +196 -0
- package/prompts/round-staleness.md +856 -0
- package/schemas/review-result.v1.json +391 -0
- package/schemas/scan-result.v1.json +421 -0
- package/schemas/version.v1.json +57 -0
- package/scripts/approve-pr.sh +16 -0
- package/scripts/build-stamp.mjs +67 -0
- package/scripts/commit-verdict.sh +155 -0
- package/scripts/companion-e2e.sh +250 -0
- package/scripts/companion-memory-forward.mjs +1966 -0
- package/scripts/config-fingerprint.sh +28 -0
- package/scripts/deploy-manifest.mjs +141 -0
- package/scripts/deploy.sh +1435 -0
- package/scripts/directory-lock.mjs +193 -0
- package/scripts/heartbeat-fresh.mjs +54 -0
- package/scripts/install-locator.sh +328 -0
- package/scripts/install-wrapper.sh +76 -0
- package/scripts/memory-search-check.mjs +98 -0
- package/scripts/mint-token.sh +212 -0
- package/scripts/openviking-embedder.py +392 -0
- package/scripts/openviking-ov-conf.mjs +161 -0
- package/scripts/openviking-provision.mjs +1122 -0
- package/scripts/openviking-reembed.mjs +1154 -0
- package/scripts/post-review.sh +16 -0
- package/scripts/pr-checks.sh +256 -0
- package/scripts/pr-context.py +174 -0
- package/scripts/pr-context.sh +63 -0
- package/scripts/pr-self.sh +144 -0
- package/scripts/pr-threads.sh +145 -0
- package/scripts/qcr +15 -0
- package/scripts/qcr-host.sh +769 -0
- package/scripts/render-review.sh +24 -0
- package/scripts/reply-thread.sh +16 -0
- package/scripts/required-passes.py +112 -0
- package/scripts/smoke-companion.sh +211 -0
- package/scripts/tui-deploy-runner.mjs +61 -0
- package/scripts/verify-dist.mjs +201 -0
- package/scripts/vm-ghcr-login.sh +188 -0
- package/seeds/agents/pass-baseline.md +61 -0
- package/seeds/agents/pass-contracts.md +30 -0
- package/seeds/agents/pass-data-safety.md +54 -0
- package/seeds/agents/pass-delivery.md +37 -0
- package/seeds/agents/pass-prod-risk.md +24 -0
- package/seeds/agents/pass-security.md +31 -0
- package/seeds/agents/pass-statelessness.md +27 -0
- package/seeds/agents/pass-ui.md +23 -0
- package/seeds/agents/pass-validate.md +50 -0
- package/seeds/prompts/conversation-system.md +117 -0
- package/seeds/prompts/eod-sweep.md +120 -0
- package/seeds/prompts/pass-common.md +52 -0
- package/skills/companion/adjudicate/SKILL.md +247 -0
- package/skills/companion/evidence/SKILL.md +150 -0
- package/skills/companion/land/SKILL.md +200 -0
- package/skills/companion/replay/SKILL.md +138 -0
- package/skills/companion/setup/SKILL.md +167 -0
- package/skills/companion/survey/SKILL.md +160 -0
- package/skills/selfimprove/README.md +34 -0
- package/skills/selfimprove/SKILL.md +127 -0
- package/skills/selfimprove/agents/openai.yaml +2 -0
- package/skills/selfimprove/commands/selfimprove.md +6 -0
- package/skills/selfimprove/references/fix-loop.md +49 -0
- package/skills/selfimprove/references/ledger.md +20 -0
- package/skills/selfimprove/references/setup.md +40 -0
- package/src/anomalies.ts +566 -0
- package/src/answerworker.ts +251 -0
- package/src/appstamp.ts +65 -0
- package/src/archive/admission.ts +3 -0
- package/src/archive/archivedb.ts +548 -0
- package/src/archive/archiver.ts +1963 -0
- package/src/archive/archivewrite.ts +279 -0
- package/src/archive/bundle.ts +333 -0
- package/src/archive/dbmove.ts +911 -0
- package/src/archive/duplicatecounts.ts +98 -0
- package/src/archive/eligibility.ts +181 -0
- package/src/archive/extract.ts +342 -0
- package/src/archive/fsutil.ts +415 -0
- package/src/archive/gzfile.ts +370 -0
- package/src/archive/kinds.ts +21 -0
- package/src/archive/livecheckpoint.ts +217 -0
- package/src/archive/liveretention.ts +180 -0
- package/src/archive/manifest.ts +519 -0
- package/src/archive/overseer.ts +336 -0
- package/src/archive/piglobal.ts +174 -0
- package/src/archive/plan.ts +612 -0
- package/src/archive/prarchive.ts +984 -0
- package/src/archive/prfiles.ts +406 -0
- package/src/archive/report.ts +252 -0
- package/src/archive/restore.ts +330 -0
- package/src/archive/rows.ts +187 -0
- package/src/archive/seal.ts +630 -0
- package/src/archive/status.ts +719 -0
- package/src/archive/storepass.ts +581 -0
- package/src/backfill.ts +362 -0
- package/src/bench/cost.ts +113 -0
- package/src/bench/exec.ts +90 -0
- package/src/bench/values.ts +13 -0
- package/src/botcontrol.ts +176 -0
- package/src/botworker.ts +953 -0
- package/src/capture.ts +321 -0
- package/src/cases.ts +1789 -0
- package/src/channelfs.ts +26 -0
- package/src/checklist.ts +506 -0
- package/src/ciretry.ts +141 -0
- package/src/cli.ts +2271 -0
- package/src/cliops.ts +384 -0
- package/src/clonedir.ts +15 -0
- package/src/commandeffects.ts +57 -0
- package/src/commands.ts +1678 -0
- package/src/companioninbox.ts +239 -0
- package/src/compare.ts +957 -0
- package/src/config.ts +2050 -0
- package/src/configcheck.ts +133 -0
- package/src/coreext.ts +47 -0
- package/src/daemon.ts +1302 -0
- package/src/debuground.ts +927 -0
- package/src/debugroundpi.ts +262 -0
- package/src/diagnostics.ts +14 -0
- package/src/digest.ts +183 -0
- package/src/dispatch.ts +2250 -0
- package/src/engine/answerguard.ts +27 -0
- package/src/engine/botsdk.ts +336 -0
- package/src/engine/caserunner-sdk.ts +423 -0
- package/src/engine/debugpi.ts +235 -0
- package/src/eod.ts +669 -0
- package/src/eodpinwatch.ts +556 -0
- package/src/escalation.ts +1253 -0
- package/src/eventloop.ts +1045 -0
- package/src/execboundary.ts +633 -0
- package/src/feedback.ts +1162 -0
- package/src/findingscheck.ts +298 -0
- package/src/fsdenied.ts +24 -0
- package/src/ghread.ts +242 -0
- package/src/github.ts +1497 -0
- package/src/homes.ts +175 -0
- package/src/hostroute.ts +53 -0
- package/src/improvements.ts +787 -0
- package/src/install.ts +1103 -0
- package/src/installkind.ts +50 -0
- package/src/ipc/capability.ts +333 -0
- package/src/ipc/client.ts +269 -0
- package/src/ipc/control.ts +90 -0
- package/src/ipc/ghreadshim.ts +94 -0
- package/src/ipc/launcher.ts +65 -0
- package/src/ipc/permits.ts +362 -0
- package/src/ipc/postshim.ts +164 -0
- package/src/ipc/protocol.ts +342 -0
- package/src/ipc/server.ts +371 -0
- package/src/ipc/workerspec.ts +218 -0
- package/src/learner/learner.ts +1655 -0
- package/src/learner/miner.ts +1290 -0
- package/src/learner/packet.ts +340 -0
- package/src/learner/receipt.ts +168 -0
- package/src/learner/status.ts +107 -0
- package/src/ledgercheck.ts +157 -0
- package/src/lessonledger.ts +703 -0
- package/src/lessonselect.ts +888 -0
- package/src/lessonshadow.ts +810 -0
- package/src/levers.ts +393 -0
- package/src/live/projection.ts +491 -0
- package/src/live/publish.ts +92 -0
- package/src/live/read.ts +86 -0
- package/src/livecli.ts +336 -0
- package/src/loginhint.ts +56 -0
- package/src/loginrefresh/atomic.ts +76 -0
- package/src/loginrefresh/lock.ts +104 -0
- package/src/loginrefresh/main.ts +59 -0
- package/src/loginrefresh/pirefresh.ts +57 -0
- package/src/loginrefresh/refresher.ts +326 -0
- package/src/loginrefresh/supervisor.ts +273 -0
- package/src/loginrefresh/types.ts +35 -0
- package/src/memory/context.ts +385 -0
- package/src/memory/datafence.ts +9 -0
- package/src/memory/delivery.ts +171 -0
- package/src/memory/eventtags.ts +80 -0
- package/src/memory/health.ts +953 -0
- package/src/memory/import.ts +664 -0
- package/src/memory/layout.ts +261 -0
- package/src/memory/legacy-file.ts +104 -0
- package/src/memory/lessons.ts +447 -0
- package/src/memory/ovsession.ts +601 -0
- package/src/memory/relayout.ts +335 -0
- package/src/memory/roundactivity.ts +209 -0
- package/src/memory/selection.ts +989 -0
- package/src/memory/shadowstatus.ts +125 -0
- package/src/memory/store.ts +275 -0
- package/src/memory/textidentity.ts +8 -0
- package/src/metrics.ts +1056 -0
- package/src/missingconfig.ts +7 -0
- package/src/npmverbs.ts +264 -0
- package/src/onboard/choices.ts +222 -0
- package/src/onboard/engine.ts +518 -0
- package/src/onboard/launch.ts +115 -0
- package/src/onboard/steps/appslug.ts +265 -0
- package/src/onboard/steps/bringup.ts +482 -0
- package/src/onboard/steps/compose.ts +459 -0
- package/src/onboard/steps/configdelivery.ts +58 -0
- package/src/onboard/steps/configfill.ts +1352 -0
- package/src/onboard/steps/deploysh.ts +105 -0
- package/src/onboard/steps/embeddings.ts +550 -0
- package/src/onboard/steps/escalation.ts +640 -0
- package/src/onboard/steps/githubapp.ts +1414 -0
- package/src/onboard/steps/golive.ts +577 -0
- package/src/onboard/steps/index.ts +71 -0
- package/src/onboard/steps/ingress.ts +907 -0
- package/src/onboard/steps/inventory.ts +526 -0
- package/src/onboard/steps/llmauth.ts +1095 -0
- package/src/onboard/steps/memory.ts +1290 -0
- package/src/onboard/steps/memorykeys.ts +85 -0
- package/src/onboard/steps/preflight.ts +191 -0
- package/src/onboard/steps/proof.ts +624 -0
- package/src/onboard/steps/ssh.ts +377 -0
- package/src/onboard/steps/topology.ts +337 -0
- package/src/onboard/steps/transport.ts +665 -0
- package/src/onboard/steps/verifyup.ts +448 -0
- package/src/onboard/steps/vmdeploy.ts +442 -0
- package/src/onboard/toml.ts +50 -0
- package/src/opencli.ts +591 -0
- package/src/openpr.ts +568 -0
- package/src/openviking/client.ts +1131 -0
- package/src/openviking/context.ts +263 -0
- package/src/openviking/embedder.ts +516 -0
- package/src/openviking/gapsink.ts +63 -0
- package/src/openviking/health.ts +83 -0
- package/src/openviking/index.ts +119 -0
- package/src/openviking/lessonfind.ts +92 -0
- package/src/openviking/probe.ts +129 -0
- package/src/openviking/proc.ts +110 -0
- package/src/openviking/provisionreport.ts +160 -0
- package/src/openviking/reembed.ts +412 -0
- package/src/openviking/relayout.ts +177 -0
- package/src/openviking/sidecar-main.ts +38 -0
- package/src/openviking/sidecar.ts +106 -0
- package/src/openviking/store.ts +2025 -0
- package/src/openviking/supervisor.ts +474 -0
- package/src/openviking/types.ts +174 -0
- package/src/operator-recovery.ts +237 -0
- package/src/operatormessage.ts +52 -0
- package/src/outcomes.ts +1670 -0
- package/src/piauthcopy.ts +12 -0
- package/src/picli.ts +49 -0
- package/src/picwd.ts +25 -0
- package/src/pilogin.ts +37 -0
- package/src/pingpong.ts +1339 -0
- package/src/posting-journal.ts +549 -0
- package/src/posting-outbox.ts +802 -0
- package/src/posting-receipt.ts +324 -0
- package/src/posting.ts +603 -0
- package/src/postops.ts +832 -0
- package/src/pr-activity.ts +20 -0
- package/src/pr-author.ts +6 -0
- package/src/pr-conversation.ts +432 -0
- package/src/pr-session-lifecycle.ts +631 -0
- package/src/preflight.ts +391 -0
- package/src/priorresolve.ts +343 -0
- package/src/qcrbin.ts +186 -0
- package/src/recheck.ts +1775 -0
- package/src/reconcile.ts +1030 -0
- package/src/recordedfindings.ts +27 -0
- package/src/regression.ts +833 -0
- package/src/replay.ts +816 -0
- package/src/requiredpasses.ts +148 -0
- package/src/resumecheck.ts +246 -0
- package/src/reviewcli.ts +787 -0
- package/src/reviewcontext.ts +35 -0
- package/src/reviewdetach.ts +734 -0
- package/src/reviewhistory.ts +427 -0
- package/src/reviewhome.ts +36 -0
- package/src/reviewmemory.ts +133 -0
- package/src/reviewrendercli.ts +139 -0
- package/src/round-work.ts +42 -0
- package/src/runhost.ts +108 -0
- package/src/scanauth.ts +36 -0
- package/src/scanbox.ts +1081 -0
- package/src/scancli.ts +1296 -0
- package/src/scandriverlog.ts +32 -0
- package/src/session-bridge.ts +139 -0
- package/src/session-command-payload.ts +35 -0
- package/src/session-command-supervisor.ts +1142 -0
- package/src/sessionjsonl.ts +88 -0
- package/src/sessions.ts +659 -0
- package/src/setup/adapters.ts +335 -0
- package/src/setup/baselines.ts +110 -0
- package/src/setup/caserunner.ts +611 -0
- package/src/setup/configcommit.ts +443 -0
- package/src/setup/engine.ts +1001 -0
- package/src/setup/facts.ts +627 -0
- package/src/setup/findings.ts +1264 -0
- package/src/setup/history.ts +443 -0
- package/src/setup/interview.ts +243 -0
- package/src/setup/ledger.ts +324 -0
- package/src/setup/pins.ts +66 -0
- package/src/setup/portrait.ts +635 -0
- package/src/setup/presentation.ts +18 -0
- package/src/setup/profile.ts +508 -0
- package/src/setup/replay.ts +370 -0
- package/src/setup/replaybuild.ts +392 -0
- package/src/setup/replayrun.ts +1096 -0
- package/src/setup/replaysource.ts +198 -0
- package/src/setup/retune.ts +415 -0
- package/src/setup/retunelog.ts +95 -0
- package/src/setup/scan.ts +1033 -0
- package/src/setup/scanbuild.ts +168 -0
- package/src/setup/scanfacts.ts +107 -0
- package/src/setup/scanlease.ts +297 -0
- package/src/setup/scanreport.ts +373 -0
- package/src/setup/scanrun.ts +2268 -0
- package/src/setup/scantree.ts +259 -0
- package/src/setup/stage2.ts +926 -0
- package/src/setup/stage2request.ts +347 -0
- package/src/setup/survey.ts +458 -0
- package/src/setup/surveyrun.ts +758 -0
- package/src/setup/textsniff.ts +95 -0
- package/src/setup/trackersink.ts +433 -0
- package/src/setup/triage.ts +768 -0
- package/src/setup/tuning.ts +766 -0
- package/src/setup/unitcut.ts +523 -0
- package/src/singleflight.ts +86 -0
- package/src/slack/announce.ts +911 -0
- package/src/slack/api.ts +303 -0
- package/src/slack/messages.ts +288 -0
- package/src/slack/origin.ts +599 -0
- package/src/slack/policy.ts +354 -0
- package/src/slack/reply.ts +201 -0
- package/src/slack/router.ts +285 -0
- package/src/slack/socket.ts +384 -0
- package/src/slack/types.ts +108 -0
- package/src/sqlitehint.ts +32 -0
- package/src/ssh-observation.ts +30 -0
- package/src/state.ts +8243 -0
- package/src/statemachine.ts +369 -0
- package/src/stubs.ts +68 -0
- package/src/takeover.ts +634 -0
- package/src/takeoverpi.ts +293 -0
- package/src/terminal-failure.ts +41 -0
- package/src/thread-resolution-refusal.ts +2 -0
- package/src/tmux.ts +153 -0
- package/src/trackerid.ts +41 -0
- package/src/transcriptindex.ts +665 -0
- package/src/tui/actions.ts +484 -0
- package/src/tui/alerts.ts +666 -0
- package/src/tui/app.ts +1979 -0
- package/src/tui/archiveops.ts +192 -0
- package/src/tui/autofile.ts +233 -0
- package/src/tui/board.ts +1432 -0
- package/src/tui/candidates.ts +889 -0
- package/src/tui/cellwidth.ts +148 -0
- package/src/tui/changelog-store.ts +503 -0
- package/src/tui/chord.ts +25 -0
- package/src/tui/clipboard.ts +92 -0
- package/src/tui/cmdbar.ts +296 -0
- package/src/tui/companion.ts +1427 -0
- package/src/tui/companionplan.ts +247 -0
- package/src/tui/companiontab.ts +83 -0
- package/src/tui/confighome.ts +155 -0
- package/src/tui/connect.ts +68 -0
- package/src/tui/deploy-ceremony.ts +104 -0
- package/src/tui/deploy-drain.ts +129 -0
- package/src/tui/deploy-process.ts +265 -0
- package/src/tui/deploytab.ts +1523 -0
- package/src/tui/design.ts +125 -0
- package/src/tui/digesttab.ts +214 -0
- package/src/tui/emptystate.ts +39 -0
- package/src/tui/evidence.ts +999 -0
- package/src/tui/feed.ts +604 -0
- package/src/tui/feedbacktab.ts +1077 -0
- package/src/tui/filters.ts +90 -0
- package/src/tui/ghread.ts +321 -0
- package/src/tui/historypull.ts +81 -0
- package/src/tui/inboxpull.ts +236 -0
- package/src/tui/index.ts +1565 -0
- package/src/tui/issuesink.ts +238 -0
- package/src/tui/jsonfile.ts +42 -0
- package/src/tui/keys.ts +109 -0
- package/src/tui/layout.ts +108 -0
- package/src/tui/learning.ts +627 -0
- package/src/tui/lessonswitch.ts +118 -0
- package/src/tui/livecontrol.ts +228 -0
- package/src/tui/memforward.ts +443 -0
- package/src/tui/memincident.ts +363 -0
- package/src/tui/memory.ts +620 -0
- package/src/tui/memtab.ts +606 -0
- package/src/tui/metricstab.ts +1177 -0
- package/src/tui/model.ts +131 -0
- package/src/tui/mouse.ts +98 -0
- package/src/tui/msg.ts +268 -0
- package/src/tui/nativecompanion.ts +417 -0
- package/src/tui/openurl.ts +21 -0
- package/src/tui/paint.ts +511 -0
- package/src/tui/panes.ts +98 -0
- package/src/tui/pipane.ts +219 -0
- package/src/tui/poll-hooks.ts +12 -0
- package/src/tui/poll.ts +147 -0
- package/src/tui/proposals.ts +838 -0
- package/src/tui/prpause.ts +150 -0
- package/src/tui/reload.ts +436 -0
- package/src/tui/remote.ts +413 -0
- package/src/tui/rollover.ts +969 -0
- package/src/tui/scantab.ts +1290 -0
- package/src/tui/setupentry.ts +93 -0
- package/src/tui/sidebar.ts +749 -0
- package/src/tui/sshtriage.ts +510 -0
- package/src/tui/startup.ts +231 -0
- package/src/tui/tabs.ts +45 -0
- package/src/tui/termmirror.ts +224 -0
- package/src/tui/theme.ts +95 -0
- package/src/tui/ticketstab.ts +310 -0
- package/src/tui/tmuxsocket.ts +129 -0
- package/src/tui/trackertickets.ts +84 -0
- package/src/tui/unwedge.ts +286 -0
- package/src/tui/viewer.ts +575 -0
- package/src/tui/viewerinput.ts +173 -0
- package/src/updatenotice.ts +178 -0
- package/src/usage.ts +1154 -0
- package/src/verdicts.ts +4689 -0
- package/src/version.ts +80 -0
- package/src/wakes.ts +476 -0
- package/src/webhook.ts +942 -0
- package/src/webhookheal.ts +276 -0
- package/src/wire.ts +2153 -0
- package/src/worker-session-host.ts +602 -0
- package/src/workerbreaker.ts +156 -0
- package/src/workercontrol.ts +104 -0
- package/src/workerlease.ts +441 -0
- package/src/workerliveness.ts +103 -0
- package/src/workerlog.ts +79 -0
- package/src/workerruntime.ts +30 -0
- package/src/workersupervisor.ts +1538 -0
- package/src/workerusage.ts +42 -0
- package/src/workerworld.ts +427 -0
- package/src/worktrees.ts +1138 -0
- package/tests/fixtures/regression-cases.json +795 -0
- package/tests/fixtures/seed-manifest.json +19 -0
|
@@ -0,0 +1,1963 @@
|
|
|
1
|
+
import { recoverAllDuplicateCounts } from "./duplicatecounts.js";
|
|
2
|
+
import { STORE_LABELS } from "./status.js";
|
|
3
|
+
import { archivedEventTotal } from "./rows.js";
|
|
4
|
+
// The closed-PR archiver (docs/archiving.md §10) — daemon
|
|
5
|
+
// bookkeeping on its own timer. It never spawns pi, injects, wakes or kills a
|
|
6
|
+
// session, and makes no GitHub call (root rule 9; it is not a session kind).
|
|
7
|
+
//
|
|
8
|
+
// The shape is the CaseWriter's (src/cases.ts): an injected `schedule` seam
|
|
9
|
+
// that re-arms AFTER each tick finishes (unref'd), a coalesced tick (a call
|
|
10
|
+
// while one runs joins it) and a `stopped` flag — plus a STRICT mutex, never
|
|
11
|
+
// SingleFlight: SingleFlight abandons an overdue pass and starts a second
|
|
12
|
+
// beside it, and two archivers on one PR's files is exactly the hazard. A
|
|
13
|
+
// restore takes the mutex WITH PRIORITY: a running pass checks between files,
|
|
14
|
+
// batches and phases and yields to it.
|
|
15
|
+
//
|
|
16
|
+
// - Start: never on the boot pass. No tick does any work before the
|
|
17
|
+
// daemon's first HEALTHY reconcile plus `[archive] start_delay_min`.
|
|
18
|
+
// - Holds: nothing is started while draining, while the event loop reads
|
|
19
|
+
// `stalled`, while the container is CPU-starved (throttled in at least
|
|
20
|
+
// `[daemon] event_loop_starved_throttle_ratio` of its periods), or while
|
|
21
|
+
// either mount's free space is below `disk_headroom_gb` — or UNKNOWN (a
|
|
22
|
+
// failed statfs is a hold, never a guess). An unknown load reading is not
|
|
23
|
+
// a hold, but the file phase then runs at its minimum: one PR, half the
|
|
24
|
+
// rate.
|
|
25
|
+
// - Mode `off` arms no timer at all.
|
|
26
|
+
//
|
|
27
|
+
// A tick reports the PLAN (src/archive/plan.ts) — what a pass would move —
|
|
28
|
+
// through status.json `archive`: the plan is recomputed when the last one is
|
|
29
|
+
// older than PLAN_REFRESH_MS, and always fresh for the operator's `archive-run`.
|
|
30
|
+
// A `plan` tick moves no file, no directory and no archive row; its only write
|
|
31
|
+
// is anomaly bookkeeping (the live-cap alert). In mode `on` a tick, in order:
|
|
32
|
+
//
|
|
33
|
+
// 1. once per daemon start, CRASH RECOVERY (§7.6): stray `.partial` files
|
|
34
|
+
// in the archive dirs of PRs left `archiving`/`restoring`, and stray
|
|
35
|
+
// `.restore-partial` files at their restore targets, are deleted;
|
|
36
|
+
// 2. RESTORES (§8.3) of every PR the registry no longer calls closed whose
|
|
37
|
+
// row is not `restored` — never held by a drain;
|
|
38
|
+
// 3. the holds;
|
|
39
|
+
// 4. the DB PHASE (src/archive/dbmove.ts): the reader-less event rows
|
|
40
|
+
// and old webhook deliveries into `archive/db/`;
|
|
41
|
+
// 5. the plan;
|
|
42
|
+
// 6. the FILE PHASE: the plan's eligible PRs, oldest window first, each
|
|
43
|
+
// through `prarchive.ts` — at most `max_prs_per_tick`, no new file after
|
|
44
|
+
// `tick_budget_s`, the disk checked before every file (§7.7);
|
|
45
|
+
// 7. the STORES that belong to no PR: the overseer's non-current
|
|
46
|
+
// transcripts (`overseer.ts`) and pi's global session store
|
|
47
|
+
// (`piglobal.ts`), under the same budget, holds and disk checks;
|
|
48
|
+
// 8. SEALING finished months (src/archive/seal.ts).
|
|
49
|
+
//
|
|
50
|
+
// The operator's two verbs (`archive-run` and `archive-restore`, valid
|
|
51
|
+
// only from the root-sealed `commands/operator/`) reach `operatorRun` — one
|
|
52
|
+
// immediate tick, a fresh plan, every hold still binding — and
|
|
53
|
+
// `operatorRestore` — one PR's archive back to its live paths, the mutex
|
|
54
|
+
// taken WITH PRIORITY so a running pass yields at its next file boundary.
|
|
55
|
+
// Each answers within OPERATOR_WAIT_MS (the operator channel is served one
|
|
56
|
+
// command at a time); a tick or restore still running then says so and goes
|
|
57
|
+
// on. Before the daemon's first healthy reconcile each answers at once — the
|
|
58
|
+
// run with its hold, the restore going on in the background — because the
|
|
59
|
+
// operator watcher's startup sweep (awaited by the daemon's start) serves a
|
|
60
|
+
// file left while the daemon was down, and a verb must never hold the boot.
|
|
61
|
+
//
|
|
62
|
+
// A refusal that needs a person (a copy or a row read back different, a
|
|
63
|
+
// manifest that cannot be read, a month whose counts or gzip do not verify)
|
|
64
|
+
// is the quiet anomaly `archive_verify_failed` — raised once per incident and
|
|
65
|
+
// again only after `ANOMALY_REPEAT_MS`. Two are WAKES (src/anomalies.ts):
|
|
66
|
+
// `archive_disk_low` (free space below the headroom, once per incident and
|
|
67
|
+
// every 6 h) and `archive_restore_failed` (a reopened PR's archive did not
|
|
68
|
+
// come back; its session is born anyway, and the receipt guard keeps the
|
|
69
|
+
// archived window unknown).
|
|
70
|
+
import { statfs as fsStatfs } from "node:fs/promises";
|
|
71
|
+
import { join } from "node:path";
|
|
72
|
+
import { performance } from "node:perf_hooks";
|
|
73
|
+
import type { ArchiveConfig } from "../config.js";
|
|
74
|
+
import type { FileOutcome } from "../improvements.js";
|
|
75
|
+
import type { DaemonLoadStatus } from "../eventloop.js";
|
|
76
|
+
import { DEFAULT_EVENT_LOOP_STARVED_THROTTLE_RATIO } from "../eventloop.js";
|
|
77
|
+
import type { StateStore } from "../state.js";
|
|
78
|
+
import { withinBudget } from "@quickchatai/pi-quickreview/src/budget.js";
|
|
79
|
+
import { yieldToLoop } from "@quickchatai/pi-quickreview/src/yieldloop.js";
|
|
80
|
+
import { ArchiveDb, ManifestUnreadable, archiveDbDir } from "./archivedb.js";
|
|
81
|
+
import { DELIVERIES_TABLE, DbMover, type DbMoveResult } from "./dbmove.js";
|
|
82
|
+
import { atNearestExisting } from "./fsutil.js";
|
|
83
|
+
import type { OutsideFacts } from "./eligibility.js";
|
|
84
|
+
import { removeRestorePartial, sweepPartials, type StepFn } from "./gzfile.js";
|
|
85
|
+
import {
|
|
86
|
+
LIVE_SESSIONS_DEADLINE_MS,
|
|
87
|
+
askLiveSessions,
|
|
88
|
+
computePlan,
|
|
89
|
+
type ArchivePlan,
|
|
90
|
+
type PlanMemo,
|
|
91
|
+
} from "./plan.js";
|
|
92
|
+
import { OVERSEER_STORE, archiveOverseer } from "./overseer.js";
|
|
93
|
+
import {
|
|
94
|
+
overLiveCap,
|
|
95
|
+
sweepLiveTrees,
|
|
96
|
+
type LiveSweepOptions,
|
|
97
|
+
type LiveSweepResult,
|
|
98
|
+
} from "./liveretention.js";
|
|
99
|
+
import { PI_GLOBAL_STORE, archivePiGlobal } from "./piglobal.js";
|
|
100
|
+
import { archivePr, type PrPassResult } from "./prarchive.js";
|
|
101
|
+
import { SpanMeter, injectDirOf, listLiveTree, knownInjectFiles } from "./prfiles.js";
|
|
102
|
+
import {
|
|
103
|
+
RESTORE_RETRY_MS,
|
|
104
|
+
RESTORE_WAIT_MS,
|
|
105
|
+
restorePr,
|
|
106
|
+
restoreTargets,
|
|
107
|
+
type RestoreResult,
|
|
108
|
+
} from "./restore.js";
|
|
109
|
+
import { archiveRow, bumpMark, inFlightRows, markNumber, reopenedRows } from "./rows.js";
|
|
110
|
+
import { SEAL_PROBLEMS, recoverSeals, sealDue, type SealResult } from "./seal.js";
|
|
111
|
+
import {
|
|
112
|
+
OPERATOR_WAIT_MS,
|
|
113
|
+
pendingArchiveStatus,
|
|
114
|
+
type ArchiveDbStatus,
|
|
115
|
+
type ArchiveDisk,
|
|
116
|
+
type ArchiveFilesStatus,
|
|
117
|
+
type ArchiveHoldReason,
|
|
118
|
+
type ArchiveLiveStatus,
|
|
119
|
+
type ArchiveState,
|
|
120
|
+
type ArchiveStatus,
|
|
121
|
+
type ArchiveStoreStatus,
|
|
122
|
+
type ArchiveTotals,
|
|
123
|
+
} from "./status.js";
|
|
124
|
+
import { emptyStoreResult, type StorePassResult } from "./storepass.js";
|
|
125
|
+
|
|
126
|
+
export { OPERATOR_WAIT_MS };
|
|
127
|
+
|
|
128
|
+
const MIN_MS = 60_000;
|
|
129
|
+
const DAY_MS = 24 * 60 * MIN_MS;
|
|
130
|
+
const GB = 1024 ** 3;
|
|
131
|
+
|
|
132
|
+
/** A plan is recomputed at most this often — the inventory is a few thousand
|
|
133
|
+
* async stats, and in plan mode nothing changes it but time. */
|
|
134
|
+
export const PLAN_REFRESH_MS = 15 * MIN_MS;
|
|
135
|
+
|
|
136
|
+
/** status()'s database figures (totals, restores, free pages) are re-read at
|
|
137
|
+
* most this often; the heartbeat asks every 30 s. */
|
|
138
|
+
export const STATUS_DB_TTL_MS = MIN_MS;
|
|
139
|
+
|
|
140
|
+
/** One incident raises `archive_verify_failed` once, and again only this long
|
|
141
|
+
* after — the memory wakes' 6 h. */
|
|
142
|
+
export const ANOMALY_REPEAT_MS = 6 * 60 * MIN_MS;
|
|
143
|
+
|
|
144
|
+
/** `verify_failures_24h`: the daemon-wide anomalies (pr NULL — the archiver's
|
|
145
|
+
* are), newest first down the (kind, pr, id) index, read only back to 24 h
|
|
146
|
+
* ago — never the whole anomaly history. */
|
|
147
|
+
export const VERIFY_FAILURES_SQL =
|
|
148
|
+
"SELECT ts, json_extract(payload, '$.kind') AS k FROM events WHERE kind = 'anomaly' AND pr IS NULL ORDER BY id DESC";
|
|
149
|
+
|
|
150
|
+
/** Past this many rows inside 24 h the count is unknown (null), never a
|
|
151
|
+
* guess. */
|
|
152
|
+
export const VERIFY_FAILURES_SCAN_CAP = 20_000;
|
|
153
|
+
|
|
154
|
+
/** Every `[archive]` knob, in the units the archiver works in. */
|
|
155
|
+
export interface ArchivePolicy {
|
|
156
|
+
mode: ArchiveConfig["mode"];
|
|
157
|
+
afterDays: number;
|
|
158
|
+
eventsAfterDays: number;
|
|
159
|
+
intervalMs: number;
|
|
160
|
+
startDelayMs: number;
|
|
161
|
+
tickBudgetMs: number;
|
|
162
|
+
maxPrsPerTick: number;
|
|
163
|
+
maxBytesPerS: number;
|
|
164
|
+
diskHeadroomBytes: number;
|
|
165
|
+
dbBatchRows: number;
|
|
166
|
+
gzipLevel: number;
|
|
167
|
+
liveRetentionMs: number;
|
|
168
|
+
/** 0 = no cap. */
|
|
169
|
+
liveCapBytes: number;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function archivePolicy(c: ArchiveConfig): ArchivePolicy {
|
|
173
|
+
return {
|
|
174
|
+
mode: c.mode,
|
|
175
|
+
afterDays: c.after_days,
|
|
176
|
+
eventsAfterDays: c.events_after_days,
|
|
177
|
+
intervalMs: c.interval_s * 1000,
|
|
178
|
+
startDelayMs: c.start_delay_min * MIN_MS,
|
|
179
|
+
tickBudgetMs: c.tick_budget_s * 1000,
|
|
180
|
+
maxPrsPerTick: c.max_prs_per_tick,
|
|
181
|
+
maxBytesPerS: Math.round(c.max_mb_per_s * 1024 * 1024),
|
|
182
|
+
diskHeadroomBytes: Math.round(c.disk_headroom_gb * GB),
|
|
183
|
+
dbBatchRows: c.db_batch_rows,
|
|
184
|
+
gzipLevel: c.gzip_level,
|
|
185
|
+
liveRetentionMs: c.live_retention_days * DAY_MS,
|
|
186
|
+
liveCapBytes: Math.round(c.live_cap_gb * GB),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* A strict mutex: holders run one at a time — in arrival order, except that a
|
|
192
|
+
* PRIORITY waiter (a restore) is served before every ordinary one. The holder
|
|
193
|
+
* is never interrupted; it asks `priorityWaiting()` at its own boundaries and
|
|
194
|
+
* releases early.
|
|
195
|
+
*/
|
|
196
|
+
export class StrictMutex {
|
|
197
|
+
private locked = false;
|
|
198
|
+
private readonly waiters: Array<{ grant: () => void; priority: boolean }> = [];
|
|
199
|
+
|
|
200
|
+
acquire(priority = false): Promise<() => void> {
|
|
201
|
+
return new Promise((resolve) => {
|
|
202
|
+
const grant = (): void => {
|
|
203
|
+
this.locked = true;
|
|
204
|
+
let done = false;
|
|
205
|
+
resolve(() => {
|
|
206
|
+
if (done) return;
|
|
207
|
+
done = true;
|
|
208
|
+
this.handOn();
|
|
209
|
+
});
|
|
210
|
+
};
|
|
211
|
+
if (!this.locked) {
|
|
212
|
+
grant();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const waiter = { grant, priority };
|
|
216
|
+
if (priority) {
|
|
217
|
+
// After the priority waiters already queued, before every ordinary one.
|
|
218
|
+
const at = this.waiters.findIndex((w) => !w.priority);
|
|
219
|
+
if (at === -1) this.waiters.push(waiter);
|
|
220
|
+
else this.waiters.splice(at, 0, waiter);
|
|
221
|
+
} else this.waiters.push(waiter);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private handOn(): void {
|
|
226
|
+
const next = this.waiters.shift();
|
|
227
|
+
if (next) next.grant();
|
|
228
|
+
else this.locked = false;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
held(): boolean {
|
|
232
|
+
return this.locked;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Is a priority waiter (a restore) queued behind the holder? */
|
|
236
|
+
priorityWaiting(): boolean {
|
|
237
|
+
return this.waiters.some((w) => w.priority);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** What `statfs` reports that the guard reads. */
|
|
242
|
+
export interface StatfsLike {
|
|
243
|
+
bavail: number | bigint;
|
|
244
|
+
bsize: number | bigint;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Free bytes on the mount `path` is (or will be) on: the nearest existing
|
|
248
|
+
* ancestor answers for a directory not created yet. Throws when statfs fails
|
|
249
|
+
* for any reason but a missing path. */
|
|
250
|
+
export async function freeBytes(
|
|
251
|
+
path: string,
|
|
252
|
+
statfs: (p: string) => Promise<StatfsLike>,
|
|
253
|
+
): Promise<number> {
|
|
254
|
+
const s = await atNearestExisting(path, statfs);
|
|
255
|
+
return Number(s.bavail) * Number(s.bsize);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* The load half of the holds — synchronous, from what the daemon already
|
|
260
|
+
* measured. Draining first, then a stalled loop, then CPU starvation; an
|
|
261
|
+
* unknown reading (no sample yet, no cgroup figures) is NOT a hold.
|
|
262
|
+
*/
|
|
263
|
+
export function loadHold(input: {
|
|
264
|
+
draining: boolean;
|
|
265
|
+
load: DaemonLoadStatus | undefined;
|
|
266
|
+
starvedRatio: number;
|
|
267
|
+
}): ArchiveHoldReason | null {
|
|
268
|
+
if (input.draining) return "draining";
|
|
269
|
+
const l = input.load;
|
|
270
|
+
if (l?.event_loop?.state === "stalled") return "loop_stalled";
|
|
271
|
+
if (l?.cpu?.state === "measured" && l.cpu.throttled_ratio >= input.starvedRatio)
|
|
272
|
+
return "cpu_starved";
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export interface ArchiverDeps {
|
|
277
|
+
store: StateStore;
|
|
278
|
+
cfg: ArchiveConfig;
|
|
279
|
+
/** The sandbox root. */
|
|
280
|
+
root: string;
|
|
281
|
+
/** `<root>/<[pi] session_dir>`. */
|
|
282
|
+
sessionsRoot: string;
|
|
283
|
+
/** When the daemon's first healthy reconcile pass completed; null before. */
|
|
284
|
+
firstHealthyReconcileAt: () => Date | null;
|
|
285
|
+
/** The daemon's own load reading (status.json `daemon`); undefined = not
|
|
286
|
+
* measured. */
|
|
287
|
+
load?: () => DaemonLoadStatus | undefined;
|
|
288
|
+
starvedRatio?: number;
|
|
289
|
+
/** Session names (`pr-<n>`) a worker runs for; null = could not be asked.
|
|
290
|
+
* Default: none answer — every candidate holds `worker_unknown`. */
|
|
291
|
+
liveSessions?: () => Promise<readonly string[] | null>;
|
|
292
|
+
/** The daemon's in-flight session births. Absent = not observable: the
|
|
293
|
+
* file phase then moves nothing (`births_unknown`), never a guess. */
|
|
294
|
+
birthInFlight?: (pr: number) => boolean;
|
|
295
|
+
statfs?: (path: string) => Promise<StatfsLike>;
|
|
296
|
+
/** When the overseer session was born (its birth record); null = unknown —
|
|
297
|
+
* the overseer store then moves nothing. Absent = no overseer pass. */
|
|
298
|
+
overseerBornAt?: () => Date | null;
|
|
299
|
+
/** pi's global session store (`~/.pi/agent/sessions`). Absent = no
|
|
300
|
+
* pi-global pass. */
|
|
301
|
+
piGlobalDir?: string;
|
|
302
|
+
/** How long `beforeBirth` waits for a restore (default RESTORE_WAIT_MS). */
|
|
303
|
+
restoreWaitMs?: number;
|
|
304
|
+
/** How long an operator verb waits before answering (default
|
|
305
|
+
* OPERATOR_WAIT_MS). */
|
|
306
|
+
operatorWaitMs?: number;
|
|
307
|
+
/** Crash-injection seam (tests): every file-protocol step. */
|
|
308
|
+
step?: StepFn;
|
|
309
|
+
/** Seam: the plan computation (default: src/archive/plan.ts). */
|
|
310
|
+
plan?: (opts: { signal: AbortSignal; memo: PlanMemo }) => Promise<ArchivePlan>;
|
|
311
|
+
/** Seam: the worker-log sweep (default: `sweepLiveTrees`). */
|
|
312
|
+
sweepLive?: (o: LiveSweepOptions) => Promise<LiveSweepResult>;
|
|
313
|
+
/** The DB mover's pause between batches (default: a real 100 ms timer). */
|
|
314
|
+
sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
315
|
+
/** The DB mover's span clock (default `performance.now`). */
|
|
316
|
+
clock?: () => number;
|
|
317
|
+
now?: () => Date;
|
|
318
|
+
log?: (msg: string, extra?: unknown) => void;
|
|
319
|
+
schedule?: (fn: () => void, ms: number) => () => void;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** One tick's outcome. */
|
|
323
|
+
export interface ArchiveTick {
|
|
324
|
+
state: ArchiveState;
|
|
325
|
+
held_reason: ArchiveHoldReason | null;
|
|
326
|
+
/** Whether this tick computed a plan (inventory work happened). */
|
|
327
|
+
planned: boolean;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const defaultSchedule = (fn: () => void, ms: number): (() => void) => {
|
|
331
|
+
const t = setTimeout(fn, ms);
|
|
332
|
+
t.unref?.();
|
|
333
|
+
return () => clearTimeout(t);
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
export class Archiver {
|
|
337
|
+
private readonly policy: ArchivePolicy;
|
|
338
|
+
private readonly now: () => Date;
|
|
339
|
+
private readonly log: (msg: string, extra?: unknown) => void;
|
|
340
|
+
private readonly schedule: (fn: () => void, ms: number) => () => void;
|
|
341
|
+
private readonly mutex = new StrictMutex();
|
|
342
|
+
private readonly memo: PlanMemo = {};
|
|
343
|
+
private cancel: (() => void) | null = null;
|
|
344
|
+
private started = false;
|
|
345
|
+
private stopped = false;
|
|
346
|
+
private running: Promise<ArchiveTick> | null = null;
|
|
347
|
+
private abort = new AbortController();
|
|
348
|
+
|
|
349
|
+
private state: ArchiveState = "unknown";
|
|
350
|
+
private heldReason: ArchiveHoldReason | null = null;
|
|
351
|
+
private heldUntil: string | null = null;
|
|
352
|
+
private runningSince: string | null = null;
|
|
353
|
+
private lastTickAt: string | null = null;
|
|
354
|
+
private lastSuccessAt: string | null = null;
|
|
355
|
+
private lastError: string | null = null;
|
|
356
|
+
private plan: ArchivePlan | null = null;
|
|
357
|
+
private disk: ArchiveDisk | null = null;
|
|
358
|
+
private maxSyncSpanMs: number | null = null;
|
|
359
|
+
/** The archive databases — opened lazily, only in mode `on`. */
|
|
360
|
+
private adb: ArchiveDb | null = null;
|
|
361
|
+
private mover: DbMover | null = null;
|
|
362
|
+
/** The disk reason that stopped this tick's DB phase between batches. */
|
|
363
|
+
private dbRoomHold: "disk_low" | "disk_unknown" | null = null;
|
|
364
|
+
/** The stores whose manifest a pass has settled since this start (a crash's
|
|
365
|
+
* unstamped removals are stamped once, even with nothing to move). */
|
|
366
|
+
private readonly storesSettled = new Set<"overseer" | "pi_global">();
|
|
367
|
+
/** The disk reason a month's gzip found no room under this tick. */
|
|
368
|
+
private sealRoomHold: "disk_low" | "disk_unknown" | null = null;
|
|
369
|
+
private sealsRecovered = false;
|
|
370
|
+
private db: ArchiveDbStatus | null = null;
|
|
371
|
+
/** When each incident's anomaly was last raised (key → ms). */
|
|
372
|
+
private readonly raised = new Map<string, number>();
|
|
373
|
+
/** Rows moved since the plan last counted them, so the backlog status
|
|
374
|
+
* shows falls between plans. */
|
|
375
|
+
private movedSincePlan = { events: 0, deliveries: 0 };
|
|
376
|
+
/** The last per-tick line's SHAPE for each phase (outcome + hold reason,
|
|
377
|
+
* never the timings). An idle tick at the same shape logs nothing, so a
|
|
378
|
+
* caught-up archiver does not write a line a minute forever; work done, or a
|
|
379
|
+
* changed outcome or hold, always logs. */
|
|
380
|
+
private lastTickLine = new Map<string, string>();
|
|
381
|
+
/** The file phase's last run. */
|
|
382
|
+
private files: ArchiveFilesStatus | null = null;
|
|
383
|
+
/** The worker trees' last sweep, and when it ran (ms). */
|
|
384
|
+
private live: ArchiveLiveStatus | null = null;
|
|
385
|
+
private liveSweptAtMs: number | null = null;
|
|
386
|
+
/** Crash recovery (§7.6) has run once this start. */
|
|
387
|
+
private filesRecovered = false;
|
|
388
|
+
/** PRs whose crash recovery failed (an unreadable archive or live dir), by
|
|
389
|
+
* why: retried every tick, and their archive pass held meanwhile — one
|
|
390
|
+
* unreadable dir never stops the restores or another PR's work. */
|
|
391
|
+
private readonly recoveryOwed = new Map<number, string>();
|
|
392
|
+
/** Plan candidates this plan's file phase is done with (archived, held, or
|
|
393
|
+
* failed to verify — the next plan offers them again). */
|
|
394
|
+
private doneSincePlan = new Set<number>();
|
|
395
|
+
/** What the file phase archived since the plan counted it. */
|
|
396
|
+
private filesSincePlan = { prs: 0, bytes: 0 };
|
|
397
|
+
/** Why a planned PR was not archived when its pass ran (its fresh
|
|
398
|
+
* eligibility, an unreadable manifest, …). */
|
|
399
|
+
private readonly fileHeld = new Map<number, string>();
|
|
400
|
+
/** Restores started by a birth, by PR: shared by every caller, each waiting
|
|
401
|
+
* until the flight's own deadline. */
|
|
402
|
+
private readonly restoring = new Map<
|
|
403
|
+
number,
|
|
404
|
+
{ promise: Promise<RestoreResult | null>; deadline: number }
|
|
405
|
+
>();
|
|
406
|
+
/** When each PR's last restore attempt ran (ms) — a failed one is retried
|
|
407
|
+
* only after RESTORE_RETRY_MS. */
|
|
408
|
+
private readonly restoreAttempted = new Map<number, number>();
|
|
409
|
+
/** The disk-low incident: when its wake last fired, and the archive-mount
|
|
410
|
+
* free space it needs to be over (the headroom plus the largest file that
|
|
411
|
+
* did not fit in it). */
|
|
412
|
+
private diskIncident: { raisedAt: number; need: number } | null = null;
|
|
413
|
+
private birthsUnknownLogged = false;
|
|
414
|
+
/** The stores' last passes. */
|
|
415
|
+
private readonly stores: {
|
|
416
|
+
overseer: ArchiveStoreStatus | null;
|
|
417
|
+
pi_global: ArchiveStoreStatus | null;
|
|
418
|
+
} = { overseer: null, pi_global: null };
|
|
419
|
+
/** The next tick recomputes the plan whatever its age (an operator's run). */
|
|
420
|
+
private forcePlan = false;
|
|
421
|
+
private dbCache: {
|
|
422
|
+
at: number;
|
|
423
|
+
totals: ArchiveTotals | null;
|
|
424
|
+
restores: ArchiveStatus["restores"];
|
|
425
|
+
freelist: number | null;
|
|
426
|
+
verifyFailures: number | null;
|
|
427
|
+
} | null = null;
|
|
428
|
+
|
|
429
|
+
constructor(private readonly deps: ArchiverDeps) {
|
|
430
|
+
this.policy = archivePolicy(deps.cfg);
|
|
431
|
+
this.now = deps.now ?? (() => new Date());
|
|
432
|
+
this.log = deps.log ?? (() => {});
|
|
433
|
+
this.schedule = deps.schedule ?? defaultSchedule;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** Arm the timer — none at all under mode `off`. */
|
|
437
|
+
start(): void {
|
|
438
|
+
if (this.stopped || this.started) return;
|
|
439
|
+
this.started = true;
|
|
440
|
+
if (this.policy.mode === "off") {
|
|
441
|
+
this.state = "off";
|
|
442
|
+
this.log('archive: [archive] mode = "off" — no timer armed');
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (this.policy.mode === "on")
|
|
446
|
+
this.log(
|
|
447
|
+
"archive: mode on — reopened PRs are restored, event and delivery rows move to archive/db, closed PRs' files to archive/pr-<n>/",
|
|
448
|
+
);
|
|
449
|
+
const loop = (): void => {
|
|
450
|
+
if (this.stopped) return;
|
|
451
|
+
void this.tick().finally(() => {
|
|
452
|
+
if (!this.stopped) this.cancel = this.schedule(loop, this.policy.intervalMs);
|
|
453
|
+
});
|
|
454
|
+
};
|
|
455
|
+
this.cancel = this.schedule(loop, this.policy.intervalMs);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** Disarm, and cut any plan or batch in flight (a batch stops before its
|
|
459
|
+
* live delete; the rows stay in both places and the next start converges).
|
|
460
|
+
* No re-arm after this. The archive connections close now, or as the tick
|
|
461
|
+
* in flight unwinds. */
|
|
462
|
+
stop(): void {
|
|
463
|
+
this.stopped = true;
|
|
464
|
+
this.cancel?.();
|
|
465
|
+
this.cancel = null;
|
|
466
|
+
this.abort.abort();
|
|
467
|
+
// The daemon connection's automatic checkpoint back now, synchronously:
|
|
468
|
+
// the daemon may close the store right after this returns.
|
|
469
|
+
this.mover?.release();
|
|
470
|
+
if (!this.running) this.closeDb();
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
private closeDb(): void {
|
|
474
|
+
this.mover?.release();
|
|
475
|
+
this.adb?.close();
|
|
476
|
+
this.adb = null;
|
|
477
|
+
this.mover = null;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** One tick. Coalesced (a call while one runs joins it); never rejects. */
|
|
481
|
+
tick(): Promise<ArchiveTick> {
|
|
482
|
+
if (this.running) return this.running;
|
|
483
|
+
this.running = this.runTick()
|
|
484
|
+
.catch((err): ArchiveTick => {
|
|
485
|
+
if (this.stopped && this.abort.signal.aborted) {
|
|
486
|
+
// Cut by stop(): not a failure — the next start plans afresh.
|
|
487
|
+
this.state = this.plan ? "plan" : "unknown";
|
|
488
|
+
return { state: this.state, held_reason: null, planned: false };
|
|
489
|
+
}
|
|
490
|
+
this.state = "failed";
|
|
491
|
+
this.lastError = err instanceof Error ? err.message : String(err);
|
|
492
|
+
this.log("archive: tick failed", err);
|
|
493
|
+
return { state: "failed", held_reason: null, planned: false };
|
|
494
|
+
})
|
|
495
|
+
.finally(() => {
|
|
496
|
+
this.running = null;
|
|
497
|
+
this.runningSince = null;
|
|
498
|
+
if (this.stopped) this.closeDb();
|
|
499
|
+
});
|
|
500
|
+
return this.running;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** The latest plan (null before the first completes). */
|
|
504
|
+
lastPlan(): ArchivePlan | null {
|
|
505
|
+
return this.plan;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/** End the tick held. `planned` says whether this tick computed a plan
|
|
509
|
+
* before the hold came up. */
|
|
510
|
+
private hold(
|
|
511
|
+
reason: ArchiveHoldReason,
|
|
512
|
+
opts: { until?: string | null; planned?: boolean } = {},
|
|
513
|
+
): ArchiveTick {
|
|
514
|
+
this.state = "held";
|
|
515
|
+
this.heldReason = reason;
|
|
516
|
+
this.heldUntil = opts.until ?? null;
|
|
517
|
+
return { state: "held", held_reason: reason, planned: opts.planned ?? false };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
private async runTick(): Promise<ArchiveTick> {
|
|
521
|
+
const release = await this.mutex.acquire();
|
|
522
|
+
try {
|
|
523
|
+
const at = this.now();
|
|
524
|
+
this.lastTickAt = at.toISOString();
|
|
525
|
+
if (this.policy.mode === "off") {
|
|
526
|
+
this.state = "off";
|
|
527
|
+
return { state: "off", held_reason: null, planned: false };
|
|
528
|
+
}
|
|
529
|
+
// Never on the boot pass: the first healthy reconcile, then the delay.
|
|
530
|
+
const healthy = this.deps.firstHealthyReconcileAt();
|
|
531
|
+
if (!healthy) return this.hold("awaiting_reconcile");
|
|
532
|
+
const opens = healthy.getTime() + this.policy.startDelayMs;
|
|
533
|
+
if (at.getTime() < opens)
|
|
534
|
+
return this.hold("start_delay", { until: new Date(opens).toISOString() });
|
|
535
|
+
const on = this.policy.mode === "on";
|
|
536
|
+
// Mode on: crash recovery (once per start), then the restores of reopened
|
|
537
|
+
// PRs — before the tick's holds, because a drain never holds a restore
|
|
538
|
+
// (a stalled loop or a starved CPU does: `tickRestores` checks those).
|
|
539
|
+
if (on) {
|
|
540
|
+
this.state = "running";
|
|
541
|
+
this.runningSince = at.toISOString();
|
|
542
|
+
await this.recoverFiles();
|
|
543
|
+
await this.tickRestores();
|
|
544
|
+
if (this.mutex.priorityWaiting()) return this.yieldToRestore(at);
|
|
545
|
+
}
|
|
546
|
+
// The load holds — synchronous, nothing touched.
|
|
547
|
+
const loadReason = this.loadReason();
|
|
548
|
+
if (loadReason) return this.hold(loadReason);
|
|
549
|
+
// The disk: statfs on both mounts. Unknown holds, never a guess.
|
|
550
|
+
const diskReason = await this.checkDisk();
|
|
551
|
+
if (diskReason) return this.hold(diskReason);
|
|
552
|
+
this.heldReason = null;
|
|
553
|
+
this.heldUntil = null;
|
|
554
|
+
// The DB phase (mode on only) runs before everything else that moves.
|
|
555
|
+
let failure: string | null = null;
|
|
556
|
+
if (on) {
|
|
557
|
+
this.state = "running";
|
|
558
|
+
this.runningSince ??= at.toISOString();
|
|
559
|
+
failure = await this.dbPhase();
|
|
560
|
+
// A hold that came up during it (the mover stops on one, the disk
|
|
561
|
+
// included) ends the tick.
|
|
562
|
+
const midway = failure === null ? (this.loadReason() ?? this.dbRoomHold) : null;
|
|
563
|
+
if (midway) return this.hold(midway);
|
|
564
|
+
if (this.mutex.priorityWaiting()) return this.yieldToRestore(at);
|
|
565
|
+
}
|
|
566
|
+
// The plan — recomputed at most every PLAN_REFRESH_MS.
|
|
567
|
+
let planned = false;
|
|
568
|
+
const force = this.forcePlan;
|
|
569
|
+
this.forcePlan = false;
|
|
570
|
+
if (
|
|
571
|
+
force ||
|
|
572
|
+
!this.plan ||
|
|
573
|
+
at.getTime() - Date.parse(this.plan.computed_at) >= PLAN_REFRESH_MS
|
|
574
|
+
) {
|
|
575
|
+
this.state = "running";
|
|
576
|
+
this.runningSince ??= at.toISOString();
|
|
577
|
+
const plan = await this.computePlan();
|
|
578
|
+
this.plan = plan;
|
|
579
|
+
this.movedSincePlan = { events: 0, deliveries: 0 };
|
|
580
|
+
this.filesSincePlan = { prs: 0, bytes: 0 };
|
|
581
|
+
this.doneSincePlan = new Set();
|
|
582
|
+
this.fileHeld.clear();
|
|
583
|
+
this.foldSpan(plan.max_sync_span_ms);
|
|
584
|
+
this.dbCache = null;
|
|
585
|
+
planned = true;
|
|
586
|
+
this.log(
|
|
587
|
+
`archive: plan — ${plan.backlog.prs} PR(s), ${plan.backlog.bytes} bytes, ` +
|
|
588
|
+
`${plan.backlog.event_rows} event row(s), ${plan.backlog.delivery_rows} delivery row(s); ` +
|
|
589
|
+
`${plan.held.length} held, ${plan.orphans.length} orphan dir(s); took ${plan.took_ms} ms ` +
|
|
590
|
+
`(longest sync span ${plan.max_sync_span_ms} ms)${on ? "" : "; nothing moved"}`,
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
// The file phase (mode on): the eligible PRs' files into archive/pr-<n>/.
|
|
594
|
+
if (on && failure === null) {
|
|
595
|
+
if (this.mutex.priorityWaiting()) return this.yieldToRestore(at, planned);
|
|
596
|
+
const files = await this.filePhase(at);
|
|
597
|
+
if (files.failure) failure = files.failure;
|
|
598
|
+
else if (files.hold) return this.hold(files.hold, { planned });
|
|
599
|
+
if (this.mutex.priorityWaiting()) return this.yieldToRestore(at, planned);
|
|
600
|
+
// The stores that belong to no PR: after the PRs (§10).
|
|
601
|
+
if (failure === null) {
|
|
602
|
+
const hold = await this.storesPhase(at);
|
|
603
|
+
if (hold) return this.hold(hold, { planned });
|
|
604
|
+
if (this.mutex.priorityWaiting()) return this.yieldToRestore(at, planned);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
// The worker trees' retention and cap: at the plan's cadence, and
|
|
608
|
+
// never a reason to fail or hold a tick.
|
|
609
|
+
if (failure === null) await this.livePhase(at, on, force || planned);
|
|
610
|
+
// Sealing comes last (§10), and only after a DB phase that did not fail.
|
|
611
|
+
// It starts nothing under a hold (re-read now, the disk included) or once
|
|
612
|
+
// the tick budget is spent.
|
|
613
|
+
if (on && failure === null) {
|
|
614
|
+
const before = this.loadReason() ?? (await this.checkDisk());
|
|
615
|
+
if (before) return this.hold(before, { planned });
|
|
616
|
+
if (this.now().getTime() - at.getTime() >= this.policy.tickBudgetMs)
|
|
617
|
+
this.log("archive: sealing waits for the next tick — the tick budget is spent");
|
|
618
|
+
else {
|
|
619
|
+
failure = await this.sealPhase();
|
|
620
|
+
// No room for a month's gzip: a mount short of the headroom (or a
|
|
621
|
+
// failed reading) holds the tick; one month too large for the
|
|
622
|
+
// archive mount only waits, as one large PR file does.
|
|
623
|
+
const room = failure === null ? this.sealRoomHold : null;
|
|
624
|
+
if (room && !this.oneFileTooLarge(room)) return this.hold(room, { planned });
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
if (failure !== null) {
|
|
628
|
+
this.state = "failed";
|
|
629
|
+
this.lastError = failure;
|
|
630
|
+
return { state: "failed", held_reason: null, planned };
|
|
631
|
+
}
|
|
632
|
+
this.state = on ? "idle" : "plan";
|
|
633
|
+
this.lastError = null;
|
|
634
|
+
this.lastSuccessAt = at.toISOString();
|
|
635
|
+
return { state: this.state, held_reason: null, planned };
|
|
636
|
+
} finally {
|
|
637
|
+
release();
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/** A restore is waiting for the mutex: end this tick here (nothing is cut
|
|
642
|
+
* mid-file; the next tick resumes). */
|
|
643
|
+
private yieldToRestore(at: Date, planned = false): ArchiveTick {
|
|
644
|
+
this.log("archive: yielding the rest of this tick to a restore");
|
|
645
|
+
this.state = "idle";
|
|
646
|
+
this.lastSuccessAt = at.toISOString();
|
|
647
|
+
return { state: "idle", held_reason: null, planned };
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// ---- the operator's verbs (§11) ------------------------------------------
|
|
651
|
+
|
|
652
|
+
/** Is the strict mutex held right now (a tick, or a restore)? */
|
|
653
|
+
busy(): boolean {
|
|
654
|
+
return this.mutex.held();
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* `archive-run` — one immediate tick, under the strict mutex, with a fresh
|
|
659
|
+
* plan. Every hold still binds (the start delay, a drain, a stalled loop, a
|
|
660
|
+
* starved CPU, the disk): the answer names the one that held it. A tick
|
|
661
|
+
* already running is let finish first, so the tick this answers for began
|
|
662
|
+
* after the request. Answers within OPERATOR_WAIT_MS; a tick still running
|
|
663
|
+
* then says so (`done: false`) and goes on. Mode `off` refuses. Before the
|
|
664
|
+
* daemon's first healthy reconcile it answers that hold at once.
|
|
665
|
+
*/
|
|
666
|
+
async operatorRun(): Promise<FileOutcome> {
|
|
667
|
+
if (this.stopped) return { ok: false, reason: "the archiver is stopping" };
|
|
668
|
+
if (this.policy.mode === "off")
|
|
669
|
+
return {
|
|
670
|
+
ok: false,
|
|
671
|
+
reason: '[archive] mode = "off" — nothing runs; set it to "plan" or "on" first',
|
|
672
|
+
};
|
|
673
|
+
// Before the daemon's first healthy reconcile no tick may do anything, so
|
|
674
|
+
// the answer is that hold, given at once — never after queueing behind
|
|
675
|
+
// the mutex (a boot restore may hold it). The operator watcher's startup
|
|
676
|
+
// sweep, which the daemon's start awaits, serves a file left while the
|
|
677
|
+
// daemon was down: an operator verb must never hold the boot.
|
|
678
|
+
if (this.deps.firstHealthyReconcileAt() === null) {
|
|
679
|
+
const s = this.status();
|
|
680
|
+
return {
|
|
681
|
+
ok: true,
|
|
682
|
+
done: true,
|
|
683
|
+
mode: this.policy.mode,
|
|
684
|
+
state: "held",
|
|
685
|
+
held_reason: "awaiting_reconcile",
|
|
686
|
+
planned: false,
|
|
687
|
+
note: "no tick runs before the daemon's first healthy reconcile",
|
|
688
|
+
backlog: s.backlog,
|
|
689
|
+
db: s.db,
|
|
690
|
+
files: s.files,
|
|
691
|
+
stores: s.stores,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
const run = (this.running ?? Promise.resolve()).then(() => {
|
|
695
|
+
this.forcePlan = true;
|
|
696
|
+
return this.tick();
|
|
697
|
+
});
|
|
698
|
+
const waited = await withinBudget(run, this.deps.operatorWaitMs ?? OPERATOR_WAIT_MS);
|
|
699
|
+
if (waited.kind === "budget")
|
|
700
|
+
return {
|
|
701
|
+
ok: true,
|
|
702
|
+
done: false,
|
|
703
|
+
state: "running",
|
|
704
|
+
note: `the tick is still running after ${Math.round((this.deps.operatorWaitMs ?? OPERATOR_WAIT_MS) / 1000)} s and goes on — see \`qcr archive status\``,
|
|
705
|
+
};
|
|
706
|
+
const t = waited.value;
|
|
707
|
+
const s = this.status();
|
|
708
|
+
return {
|
|
709
|
+
ok: t.state !== "failed",
|
|
710
|
+
...(t.state === "failed" ? { reason: this.lastError ?? "the tick failed" } : {}),
|
|
711
|
+
done: true,
|
|
712
|
+
mode: this.policy.mode,
|
|
713
|
+
state: t.state,
|
|
714
|
+
held_reason: t.held_reason,
|
|
715
|
+
...(t.state === "held" && s.held_until ? { held_until: s.held_until } : {}),
|
|
716
|
+
planned: t.planned,
|
|
717
|
+
backlog: s.backlog,
|
|
718
|
+
db: s.db,
|
|
719
|
+
files: s.files,
|
|
720
|
+
stores: s.stores,
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* `archive-restore <pr>` — one PR's archive back to its live paths (the
|
|
726
|
+
* repair verb, §11): its transcripts and session-command payloads, verified,
|
|
727
|
+
* at their original mtimes, never over a live file. In any mode, on a
|
|
728
|
+
* closed PR too (it becomes eligible again `after_days` after the restore),
|
|
729
|
+
* and a failed restore is retried at once (the 6 h retry spacing binds
|
|
730
|
+
* births and ticks, not the operator). The mutex is taken WITH PRIORITY: a
|
|
731
|
+
* running pass yields at its next file boundary. A restore a birth already
|
|
732
|
+
* started is joined, never run twice. Answers within OPERATOR_WAIT_MS; one
|
|
733
|
+
* still running then says so (`done: false`) and goes on. Before the
|
|
734
|
+
* daemon's first healthy reconcile it answers at once (`done: false`) and
|
|
735
|
+
* the restore goes on in the background.
|
|
736
|
+
*/
|
|
737
|
+
async operatorRestore(pr: number): Promise<FileOutcome> {
|
|
738
|
+
if (this.stopped) return { ok: false, reason: "the archiver is stopping" };
|
|
739
|
+
let row: ReturnType<typeof archiveRow>;
|
|
740
|
+
try {
|
|
741
|
+
row = archiveRow(this.deps.store, pr);
|
|
742
|
+
} catch (err) {
|
|
743
|
+
return {
|
|
744
|
+
ok: false,
|
|
745
|
+
reason: `pr-${pr}'s archive row could not be read: ${(err as Error).message}`,
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
if (!row)
|
|
749
|
+
return {
|
|
750
|
+
ok: true,
|
|
751
|
+
done: true,
|
|
752
|
+
outcome: "none",
|
|
753
|
+
note: `pr-${pr} has no archive row — nothing of it was archived`,
|
|
754
|
+
};
|
|
755
|
+
if (row.state === "restored" && !this.restoring.has(pr))
|
|
756
|
+
return {
|
|
757
|
+
ok: true,
|
|
758
|
+
done: true,
|
|
759
|
+
outcome: "none",
|
|
760
|
+
note: `pr-${pr} is restored already (at ${row.restored_at ?? "an unrecorded time"})`,
|
|
761
|
+
};
|
|
762
|
+
const flight = this.restoring.get(pr) ?? this.startRestore(pr, "operator");
|
|
763
|
+
// Served before the daemon's first healthy reconcile — a file the
|
|
764
|
+
// operator left while the daemon was down, met by the operator watcher's
|
|
765
|
+
// startup sweep, which the daemon's start awaits: the restore goes on in
|
|
766
|
+
// the background and the answer comes at once, so it never holds the
|
|
767
|
+
// boot (or the first reconcile) behind it. Its outcome is the row's, the
|
|
768
|
+
// events' and `qcr archive status`'s.
|
|
769
|
+
if (this.deps.firstHealthyReconcileAt() === null)
|
|
770
|
+
return {
|
|
771
|
+
ok: true,
|
|
772
|
+
done: false,
|
|
773
|
+
outcome: "restoring",
|
|
774
|
+
note: "served while the daemon starts (before its first healthy reconcile): the restore goes on in the background — see `qcr archive status`",
|
|
775
|
+
};
|
|
776
|
+
const waited = await withinBudget(flight.promise, this.deps.operatorWaitMs ?? OPERATOR_WAIT_MS);
|
|
777
|
+
if (waited.kind === "budget")
|
|
778
|
+
return {
|
|
779
|
+
ok: true,
|
|
780
|
+
done: false,
|
|
781
|
+
outcome: "restoring",
|
|
782
|
+
note: `the restore is still running after ${Math.round((this.deps.operatorWaitMs ?? OPERATOR_WAIT_MS) / 1000)} s and goes on — see \`qcr archive status\``,
|
|
783
|
+
};
|
|
784
|
+
const r = waited.value;
|
|
785
|
+
if (r === null) {
|
|
786
|
+
// Joined nothing that ran (stopping, or restored meanwhile): the row
|
|
787
|
+
// says what is so.
|
|
788
|
+
const now = archiveRow(this.deps.store, pr);
|
|
789
|
+
return now?.state === "restored"
|
|
790
|
+
? { ok: true, done: true, outcome: "restored", note: "restored (by another caller)" }
|
|
791
|
+
: { ok: false, reason: `the restore did not run (row ${now?.state ?? "gone"})` };
|
|
792
|
+
}
|
|
793
|
+
if (r.outcome === "failed")
|
|
794
|
+
return {
|
|
795
|
+
ok: false,
|
|
796
|
+
reason: r.error ?? "the restore failed",
|
|
797
|
+
done: true,
|
|
798
|
+
outcome: "failed",
|
|
799
|
+
restored: r.restored,
|
|
800
|
+
present: r.present,
|
|
801
|
+
};
|
|
802
|
+
return {
|
|
803
|
+
ok: true,
|
|
804
|
+
done: true,
|
|
805
|
+
outcome: r.outcome,
|
|
806
|
+
restored: r.restored,
|
|
807
|
+
present: r.present,
|
|
808
|
+
live_differs: r.liveDiffers,
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// ---- restores (§8.2–§8.4) ------------------------------------------------
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Awaited by the session manager before a PR session is born (§8.3). With
|
|
816
|
+
* no archive row, or a `restored` one, it returns at once (a synchronous
|
|
817
|
+
* read). Otherwise it starts the PR's restore (or joins the one running)
|
|
818
|
+
* and waits for it — at most `restoreWaitMs` from when that restore began.
|
|
819
|
+
* It never throws and never refuses a birth: a failed or slow restore lets
|
|
820
|
+
* the birth go ahead, and the receipt guard keeps the archived window
|
|
821
|
+
* unknown meanwhile. Mode `on` only (file work); a restore failed within
|
|
822
|
+
* RESTORE_RETRY_MS is not retried by a birth.
|
|
823
|
+
*/
|
|
824
|
+
async beforeBirth(pr: number): Promise<void> {
|
|
825
|
+
if (this.stopped || this.policy.mode !== "on") return;
|
|
826
|
+
let flight = this.restoring.get(pr);
|
|
827
|
+
if (!flight) {
|
|
828
|
+
let row: ReturnType<typeof archiveRow>;
|
|
829
|
+
try {
|
|
830
|
+
row = archiveRow(this.deps.store, pr);
|
|
831
|
+
if (!row || row.state === "restored") return;
|
|
832
|
+
if (this.deps.store.getPr(pr)?.state === "closed") return;
|
|
833
|
+
} catch (err) {
|
|
834
|
+
this.log(`archive: beforeBirth could not read pr-${pr}'s archive row`, err);
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
if (row.state === "restore_failed" && !this.restoreRetryDue(pr)) return;
|
|
838
|
+
flight = this.startRestore(pr);
|
|
839
|
+
}
|
|
840
|
+
await waitAtMost(flight.promise, flight.deadline - performance.now());
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
private restoreRetryDue(pr: number): boolean {
|
|
844
|
+
const last = this.restoreAttempted.get(pr);
|
|
845
|
+
return last === undefined || this.now().getTime() - last >= RESTORE_RETRY_MS;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/** The daemon's load reading; a reader that throws is UNKNOWN (undefined),
|
|
849
|
+
* never a hold by itself. */
|
|
850
|
+
private readLoad(): DaemonLoadStatus | undefined {
|
|
851
|
+
try {
|
|
852
|
+
return this.deps.load?.();
|
|
853
|
+
} catch {
|
|
854
|
+
return undefined;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/** The load holds a tick's restores obey: every one but the drain (§10). */
|
|
859
|
+
private restoreLoadHold(): ArchiveHoldReason | null {
|
|
860
|
+
return loadHold({
|
|
861
|
+
draining: false,
|
|
862
|
+
load: this.readLoad(),
|
|
863
|
+
starvedRatio: this.deps.starvedRatio ?? DEFAULT_EVENT_LOOP_STARVED_THROTTLE_RATIO,
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/** Room for a restored file of `bytes` on the LIVE (state) mount, plus the
|
|
868
|
+
* headroom (§7.7): null = room; `disk_low`, or `disk_unknown` when statfs
|
|
869
|
+
* failed — never a guess. */
|
|
870
|
+
private async liveRoomFor(bytes: number): Promise<"disk_low" | "disk_unknown" | null> {
|
|
871
|
+
const statfs = this.deps.statfs ?? ((p: string) => fsStatfs(p));
|
|
872
|
+
let free: number;
|
|
873
|
+
try {
|
|
874
|
+
free = await freeBytes(join(this.deps.root, "state"), statfs);
|
|
875
|
+
} catch (err) {
|
|
876
|
+
this.log(
|
|
877
|
+
"archive: statfs of the live mount failed before a restored file — disk UNKNOWN",
|
|
878
|
+
err,
|
|
879
|
+
);
|
|
880
|
+
return "disk_unknown";
|
|
881
|
+
}
|
|
882
|
+
return free >= bytes + this.policy.diskHeadroomBytes ? null : "disk_low";
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/** A restore outside a tick: the mutex taken WITH PRIORITY (a running pass
|
|
886
|
+
* yields to it at its next file boundary). */
|
|
887
|
+
private startRestore(
|
|
888
|
+
pr: number,
|
|
889
|
+
trigger: "reopen" | "operator" = "reopen",
|
|
890
|
+
): { promise: Promise<RestoreResult | null>; deadline: number } {
|
|
891
|
+
const promise = (async (): Promise<RestoreResult | null> => {
|
|
892
|
+
const release = await this.mutex.acquire(true);
|
|
893
|
+
try {
|
|
894
|
+
if (this.stopped) return null;
|
|
895
|
+
const row = archiveRow(this.deps.store, pr);
|
|
896
|
+
if (!row || row.state === "restored") return null;
|
|
897
|
+
return await this.restoreOne(pr, trigger);
|
|
898
|
+
} catch (err) {
|
|
899
|
+
if (!this.abort.signal.aborted) this.log(`archive: pr-${pr} restore threw`, err);
|
|
900
|
+
return null;
|
|
901
|
+
} finally {
|
|
902
|
+
release();
|
|
903
|
+
}
|
|
904
|
+
})();
|
|
905
|
+
const flight = {
|
|
906
|
+
promise,
|
|
907
|
+
deadline: performance.now() + (this.deps.restoreWaitMs ?? RESTORE_WAIT_MS),
|
|
908
|
+
};
|
|
909
|
+
this.restoring.set(pr, flight);
|
|
910
|
+
void promise.finally(() => {
|
|
911
|
+
if (this.restoring.get(pr) === flight) this.restoring.delete(pr);
|
|
912
|
+
});
|
|
913
|
+
return flight;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/** One restore; the mutex is held by the caller. */
|
|
917
|
+
private async restoreOne(pr: number, trigger: "reopen" | "operator"): Promise<RestoreResult> {
|
|
918
|
+
this.restoreAttempted.set(pr, this.now().getTime());
|
|
919
|
+
const r = await restorePr(
|
|
920
|
+
{
|
|
921
|
+
store: this.deps.store,
|
|
922
|
+
root: this.deps.root,
|
|
923
|
+
sessionsRoot: this.deps.sessionsRoot,
|
|
924
|
+
now: this.now,
|
|
925
|
+
bytesPerS: this.policy.maxBytesPerS,
|
|
926
|
+
signal: this.abort.signal,
|
|
927
|
+
log: this.log,
|
|
928
|
+
roomFor: (bytes) => this.liveRoomFor(bytes),
|
|
929
|
+
...(this.deps.step ? { step: this.deps.step } : {}),
|
|
930
|
+
},
|
|
931
|
+
pr,
|
|
932
|
+
trigger,
|
|
933
|
+
);
|
|
934
|
+
this.dbCache = null;
|
|
935
|
+
if (r.outcome === "restored") {
|
|
936
|
+
this.raised.delete(`restore:${pr}`);
|
|
937
|
+
this.deps.store.logEvent("archive_pr_restored", pr, {
|
|
938
|
+
trigger,
|
|
939
|
+
restored: r.restored,
|
|
940
|
+
present: r.present,
|
|
941
|
+
live_differs: r.liveDiffers,
|
|
942
|
+
});
|
|
943
|
+
} else if (r.outcome === "failed")
|
|
944
|
+
this.raiseWake(
|
|
945
|
+
"archive_restore_failed",
|
|
946
|
+
pr,
|
|
947
|
+
`restore:${pr}`,
|
|
948
|
+
trigger === "operator"
|
|
949
|
+
? `The operator's restore of PR #${pr}'s archive failed: ${r.error ?? "unknown"}. ` +
|
|
950
|
+
"Nothing archived was lost; receipts of its archived wakes read UNKNOWN (never absent) until a restore succeeds."
|
|
951
|
+
: `PR #${pr} was reopened but its archived files could not be restored: ${r.error ?? "unknown"}. ` +
|
|
952
|
+
"Its session is born anyway; receipts of its pre-close wakes read UNKNOWN (never absent) until a restore succeeds.",
|
|
953
|
+
);
|
|
954
|
+
return r;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/** The tick's restores, first and before any other work: every PR the
|
|
958
|
+
* registry no longer calls closed whose row is not `restored`. */
|
|
959
|
+
private async tickRestores(): Promise<void> {
|
|
960
|
+
let rows: ReturnType<typeof reopenedRows>;
|
|
961
|
+
try {
|
|
962
|
+
rows = reopenedRows(this.deps.store);
|
|
963
|
+
} catch (err) {
|
|
964
|
+
this.log("archive: could not list the PRs to restore", err);
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
for (const row of rows) {
|
|
968
|
+
// A birth's (or the operator's) restore waiting with priority goes
|
|
969
|
+
// first: the rest are the next tick's.
|
|
970
|
+
if (this.interruption()) return;
|
|
971
|
+
if (row.state === "restore_failed" && !this.restoreRetryDue(row.pr)) continue;
|
|
972
|
+
// A drain never holds a restore (§10); a stalled loop or a starved CPU
|
|
973
|
+
// does — the tick's restores wait for the next tick (a birth's own
|
|
974
|
+
// restore is not held: the birth is waiting on it).
|
|
975
|
+
const load = this.restoreLoadHold();
|
|
976
|
+
if (load) {
|
|
977
|
+
this.log(`archive: the restores wait for the next tick — ${load}`);
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
try {
|
|
981
|
+
await this.restoreOne(row.pr, "reopen");
|
|
982
|
+
} catch (err) {
|
|
983
|
+
if (this.abort.signal.aborted) throw err;
|
|
984
|
+
this.log(`archive: pr-${row.pr} restore threw`, err);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/** Crash recovery (§7.6), once per daemon start before any file work: the
|
|
990
|
+
* stray temporaries of PRs a crash left `archiving` or `restoring` (or a
|
|
991
|
+
* restore left `restore_failed`). The archive gzips with no entry are
|
|
992
|
+
* settled by the PR's next pass. A PR whose dirs cannot be read is that
|
|
993
|
+
* PR's problem alone: the quiet anomaly, its pass held (`recovery_failed`)
|
|
994
|
+
* and its recovery retried every tick — the restores, the other PRs and
|
|
995
|
+
* every later phase go on. */
|
|
996
|
+
private async recoverFiles(): Promise<void> {
|
|
997
|
+
const counted = await recoverAllDuplicateCounts(this.deps.store, this.deps.root, (pr, error) =>
|
|
998
|
+
this.log(`archive: pr-${pr} duplicate accounting recovery deferred`, error),
|
|
999
|
+
);
|
|
1000
|
+
if (counted > 0) this.dbCache = null;
|
|
1001
|
+
if (this.filesRecovered && this.recoveryOwed.size === 0) return;
|
|
1002
|
+
let rows: ReturnType<typeof inFlightRows>;
|
|
1003
|
+
try {
|
|
1004
|
+
rows = inFlightRows(this.deps.store);
|
|
1005
|
+
} catch (err) {
|
|
1006
|
+
this.log(
|
|
1007
|
+
"archive: crash recovery could not list the rows in flight — retried next tick",
|
|
1008
|
+
err,
|
|
1009
|
+
);
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
for (const pr of [...this.recoveryOwed.keys()])
|
|
1013
|
+
if (!rows.some((r) => r.pr === pr)) this.recoveryOwed.delete(pr);
|
|
1014
|
+
const todo = this.filesRecovered ? rows.filter((r) => this.recoveryOwed.has(r.pr)) : rows;
|
|
1015
|
+
let removed = 0;
|
|
1016
|
+
for (const row of todo) {
|
|
1017
|
+
this.abort.signal.throwIfAborted();
|
|
1018
|
+
try {
|
|
1019
|
+
removed += await sweepPartials(join(this.deps.root, "archive", `pr-${row.pr}`));
|
|
1020
|
+
for (const t of await restoreTargets(this.deps.root, this.deps.sessionsRoot, row.pr))
|
|
1021
|
+
if (await removeRestorePartial(t)) removed += 1;
|
|
1022
|
+
this.recoveryOwed.delete(row.pr);
|
|
1023
|
+
} catch (err) {
|
|
1024
|
+
if (this.abort.signal.aborted) throw err;
|
|
1025
|
+
const why = err instanceof Error ? err.message : String(err);
|
|
1026
|
+
this.recoveryOwed.set(row.pr, why);
|
|
1027
|
+
this.raiseQuiet(
|
|
1028
|
+
`recovery:${row.pr}`,
|
|
1029
|
+
`archive crash recovery: pr-${row.pr}: ${why} — its temporaries are left as found and its archive pass is held until recovery succeeds`,
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
await yieldToLoop();
|
|
1033
|
+
}
|
|
1034
|
+
this.filesRecovered = true;
|
|
1035
|
+
if (removed > 0) this.log(`archive: crash recovery removed ${removed} stray temporary file(s)`);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// ---- the file phase (§7) ---------------------------------------------------
|
|
1039
|
+
|
|
1040
|
+
/** Why no new file may start now: stopping, a restore waiting, the tick
|
|
1041
|
+
* budget spent, or a hold. */
|
|
1042
|
+
private fileStop(at: Date): string | null {
|
|
1043
|
+
const interrupted = this.interruption();
|
|
1044
|
+
if (interrupted) return interrupted;
|
|
1045
|
+
if (this.now().getTime() - at.getTime() >= this.policy.tickBudgetMs) return "budget";
|
|
1046
|
+
return this.loadReason();
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
/** What ends any pass at its next boundary: stopping, or a restore waiting
|
|
1050
|
+
* for the mutex. */
|
|
1051
|
+
private interruption(): "stopping" | "restore_pending" | null {
|
|
1052
|
+
if (this.stopped) return "stopping";
|
|
1053
|
+
if (this.mutex.priorityWaiting()) return "restore_pending";
|
|
1054
|
+
return null;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
/** The eligibility facts outside the database, asked fresh for one PR. */
|
|
1058
|
+
private async outsideFacts(pr: number): Promise<OutsideFacts> {
|
|
1059
|
+
const names = await askLiveSessions(
|
|
1060
|
+
this.deps.liveSessions ?? (async () => null),
|
|
1061
|
+
LIVE_SESSIONS_DEADLINE_MS,
|
|
1062
|
+
this.abort.signal,
|
|
1063
|
+
);
|
|
1064
|
+
const injectFiles = await knownInjectFiles(injectDirOf(this.deps.root, pr));
|
|
1065
|
+
return {
|
|
1066
|
+
workerAlive: names === null ? null : names.includes(`pr-${pr}`),
|
|
1067
|
+
birthInFlight: false,
|
|
1068
|
+
injectFiles,
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/** Room for one file of `bytes` (§7.7): the archive mount needs the file
|
|
1073
|
+
* plus the headroom, the state mount the headroom. */
|
|
1074
|
+
private async diskFor(bytes: number): Promise<"disk_low" | "disk_unknown" | null> {
|
|
1075
|
+
return this.checkDisk(bytes);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/** Is `reason` a `disk_low` that only one item was too large for — the
|
|
1079
|
+
* last disk reading has the headroom itself on both mounts? That item
|
|
1080
|
+
* waits; the phase goes on (a low disk would hold the tick). */
|
|
1081
|
+
private oneFileTooLarge(reason: string | null | undefined): boolean {
|
|
1082
|
+
const d = this.disk;
|
|
1083
|
+
return (
|
|
1084
|
+
reason === "disk_low" &&
|
|
1085
|
+
d !== null &&
|
|
1086
|
+
d.archive_free >= d.headroom &&
|
|
1087
|
+
d.state_free >= d.headroom
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* The plan's eligible PRs, oldest window first (the later of the close
|
|
1093
|
+
* and a restore). Returns a failure (the live tree could not be listed),
|
|
1094
|
+
* or the hold that stopped it, or neither.
|
|
1095
|
+
*/
|
|
1096
|
+
private async filePhase(
|
|
1097
|
+
at: Date,
|
|
1098
|
+
): Promise<{ failure: string | null; hold: ArchiveHoldReason | null }> {
|
|
1099
|
+
const startedAt = this.now().toISOString();
|
|
1100
|
+
const birthInFlight = this.deps.birthInFlight;
|
|
1101
|
+
const blank = (outcome: string, held: string | null): ArchiveFilesStatus => ({
|
|
1102
|
+
last_run_at: startedAt,
|
|
1103
|
+
outcome,
|
|
1104
|
+
held_reason: held,
|
|
1105
|
+
prs_started: 0,
|
|
1106
|
+
prs_archived: 0,
|
|
1107
|
+
prs_partial: 0,
|
|
1108
|
+
prs_held: 0,
|
|
1109
|
+
transcripts: 0,
|
|
1110
|
+
state_files: 0,
|
|
1111
|
+
bytes_in: 0,
|
|
1112
|
+
bytes_out: 0,
|
|
1113
|
+
removed: 0,
|
|
1114
|
+
duplicates_removed: 0,
|
|
1115
|
+
verify_failures: 0,
|
|
1116
|
+
});
|
|
1117
|
+
if (!birthInFlight) {
|
|
1118
|
+
if (!this.birthsUnknownLogged)
|
|
1119
|
+
this.log("archive: session births are not observable here — no per-PR file is moved");
|
|
1120
|
+
this.birthsUnknownLogged = true;
|
|
1121
|
+
this.files = blank("held", "births_unknown");
|
|
1122
|
+
return { failure: null, hold: null };
|
|
1123
|
+
}
|
|
1124
|
+
const plan = this.plan;
|
|
1125
|
+
const queue = plan
|
|
1126
|
+
? [...plan.candidates.map((c) => c.pr), ...plan.empty].filter(
|
|
1127
|
+
(pr) => !this.doneSincePlan.has(pr),
|
|
1128
|
+
)
|
|
1129
|
+
: [];
|
|
1130
|
+
if (queue.length === 0) {
|
|
1131
|
+
this.files = blank("none", null);
|
|
1132
|
+
return { failure: null, hold: null };
|
|
1133
|
+
}
|
|
1134
|
+
// An UNKNOWN load reading runs at the minimum: one PR, half the rate.
|
|
1135
|
+
const { maxPrs, bytesPerS } = this.fileRate();
|
|
1136
|
+
const meter = new SpanMeter();
|
|
1137
|
+
let listing: Awaited<ReturnType<typeof listLiveTree>>;
|
|
1138
|
+
try {
|
|
1139
|
+
listing = await listLiveTree(this.deps.root, this.deps.sessionsRoot, meter);
|
|
1140
|
+
} catch (err) {
|
|
1141
|
+
if (this.abort.signal.aborted) throw err;
|
|
1142
|
+
this.files = blank("failed", null);
|
|
1143
|
+
return {
|
|
1144
|
+
failure: `archive files: the live tree could not be listed: ${(err as Error).message}`,
|
|
1145
|
+
hold: null,
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
const run = blank("done", null);
|
|
1149
|
+
let hold: ArchiveHoldReason | null = null;
|
|
1150
|
+
for (const pr of queue) {
|
|
1151
|
+
const owed = this.recoveryOwed.get(pr);
|
|
1152
|
+
if (owed !== undefined) {
|
|
1153
|
+
// Its crash recovery failed: nothing of it moves until that succeeds.
|
|
1154
|
+
this.fileHeld.set(pr, "recovery_failed");
|
|
1155
|
+
this.doneSincePlan.add(pr);
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
const stop = this.fileStop(at);
|
|
1159
|
+
if (stop) {
|
|
1160
|
+
run.outcome =
|
|
1161
|
+
stop === "budget" ? "budget" : stop === "restore_pending" ? "yielded" : "held";
|
|
1162
|
+
run.held_reason = stop;
|
|
1163
|
+
if (isHoldReason(stop)) hold = stop;
|
|
1164
|
+
break;
|
|
1165
|
+
}
|
|
1166
|
+
if (run.prs_started >= maxPrs) break;
|
|
1167
|
+
run.prs_started += 1;
|
|
1168
|
+
let r: PrPassResult;
|
|
1169
|
+
try {
|
|
1170
|
+
r = await archivePr(
|
|
1171
|
+
{
|
|
1172
|
+
store: this.deps.store,
|
|
1173
|
+
root: this.deps.root,
|
|
1174
|
+
sessionsRoot: this.deps.sessionsRoot,
|
|
1175
|
+
listing,
|
|
1176
|
+
now: this.now,
|
|
1177
|
+
afterDays: this.policy.afterDays,
|
|
1178
|
+
level: this.policy.gzipLevel,
|
|
1179
|
+
bytesPerS,
|
|
1180
|
+
signal: this.abort.signal,
|
|
1181
|
+
outside: (n) => this.outsideFacts(n),
|
|
1182
|
+
birthInFlight,
|
|
1183
|
+
diskFor: (bytes) => this.diskFor(bytes),
|
|
1184
|
+
stopReason: () => this.fileStop(at),
|
|
1185
|
+
verifyFailed: (key, detail) => this.raiseQuiet(key, detail),
|
|
1186
|
+
log: this.log,
|
|
1187
|
+
meter,
|
|
1188
|
+
...(this.deps.step ? { step: this.deps.step } : {}),
|
|
1189
|
+
},
|
|
1190
|
+
pr,
|
|
1191
|
+
);
|
|
1192
|
+
} catch (err) {
|
|
1193
|
+
if (this.abort.signal.aborted) throw err;
|
|
1194
|
+
// One PR's I/O failure is that PR's: its row stays `archiving` (the
|
|
1195
|
+
// guard armed) and the next plan offers it again.
|
|
1196
|
+
this.log(`archive: pr-${pr} pass failed`, err);
|
|
1197
|
+
this.fileHeld.set(pr, "error");
|
|
1198
|
+
this.doneSincePlan.add(pr);
|
|
1199
|
+
run.prs_held += 1;
|
|
1200
|
+
continue;
|
|
1201
|
+
}
|
|
1202
|
+
run.transcripts += r.transcripts;
|
|
1203
|
+
run.state_files += r.stateFiles;
|
|
1204
|
+
run.bytes_in += r.bytesIn;
|
|
1205
|
+
run.bytes_out += r.bytesOut;
|
|
1206
|
+
run.removed += r.removed;
|
|
1207
|
+
run.duplicates_removed += r.duplicatesRemoved;
|
|
1208
|
+
run.verify_failures += r.verifyFailures;
|
|
1209
|
+
if (r.outcome === "archived") {
|
|
1210
|
+
run.prs_archived += 1;
|
|
1211
|
+
this.doneSincePlan.add(pr);
|
|
1212
|
+
this.fileHeld.delete(pr);
|
|
1213
|
+
const plannedPr = plan?.candidates.find((c) => c.pr === pr);
|
|
1214
|
+
this.filesSincePlan.prs += 1;
|
|
1215
|
+
this.filesSincePlan.bytes += plannedPr?.bytes ?? 0;
|
|
1216
|
+
if (r.removed + r.duplicatesRemoved + r.transcripts + r.stateFiles > 0)
|
|
1217
|
+
this.deps.store.logEvent("archive_pr_archived", pr, {
|
|
1218
|
+
transcripts: r.transcripts,
|
|
1219
|
+
reused: r.reused,
|
|
1220
|
+
state_files: r.stateFiles,
|
|
1221
|
+
bytes_in: r.bytesIn,
|
|
1222
|
+
bytes_out: r.bytesOut,
|
|
1223
|
+
removed: r.removed,
|
|
1224
|
+
duplicates_removed: r.duplicatesRemoved,
|
|
1225
|
+
stale_inject: r.staleInjects,
|
|
1226
|
+
});
|
|
1227
|
+
} else if (r.outcome === "held") {
|
|
1228
|
+
run.prs_held += 1;
|
|
1229
|
+
this.fileHeld.set(pr, r.reason ?? "held");
|
|
1230
|
+
this.doneSincePlan.add(pr);
|
|
1231
|
+
} else if (this.oneFileTooLarge(r.reason)) {
|
|
1232
|
+
// The mounts have their headroom, just not room for THIS PR's next
|
|
1233
|
+
// file: that PR waits (held disk_low, the wake raised once for the
|
|
1234
|
+
// incident) and the phase goes on to the next PR — one large file
|
|
1235
|
+
// never stops every other PR's smaller ones.
|
|
1236
|
+
run.prs_partial += 1;
|
|
1237
|
+
this.fileHeld.set(pr, "disk_low");
|
|
1238
|
+
this.doneSincePlan.add(pr);
|
|
1239
|
+
} else {
|
|
1240
|
+
run.prs_partial += 1;
|
|
1241
|
+
// Stopped by the tick (budget, a hold, a restore): resumed next tick.
|
|
1242
|
+
// Stopped by the PR itself (a verify failure, a changed source): the
|
|
1243
|
+
// next plan offers it again.
|
|
1244
|
+
if (
|
|
1245
|
+
!["budget", "stopping", "restore_pending"].includes(r.reason ?? "") &&
|
|
1246
|
+
!isHoldReason(r.reason ?? "")
|
|
1247
|
+
) {
|
|
1248
|
+
this.fileHeld.set(pr, r.reason ?? "incomplete");
|
|
1249
|
+
this.doneSincePlan.add(pr);
|
|
1250
|
+
}
|
|
1251
|
+
if (r.reason && isHoldReason(r.reason)) {
|
|
1252
|
+
hold = r.reason;
|
|
1253
|
+
run.outcome = "held";
|
|
1254
|
+
run.held_reason = r.reason;
|
|
1255
|
+
break;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
await yieldToLoop();
|
|
1259
|
+
}
|
|
1260
|
+
this.foldSpan(meter.max);
|
|
1261
|
+
this.files = run;
|
|
1262
|
+
if (run.prs_started > 0) this.dbCache = null;
|
|
1263
|
+
if (run.removed + run.transcripts + run.state_files + run.duplicates_removed > 0)
|
|
1264
|
+
this.deps.store.logEvent("archive_pass", null, {
|
|
1265
|
+
phase: "files",
|
|
1266
|
+
outcome: run.outcome,
|
|
1267
|
+
prs_archived: run.prs_archived,
|
|
1268
|
+
prs_partial: run.prs_partial,
|
|
1269
|
+
prs_held: run.prs_held,
|
|
1270
|
+
transcripts: run.transcripts,
|
|
1271
|
+
state_files: run.state_files,
|
|
1272
|
+
bytes_in: run.bytes_in,
|
|
1273
|
+
bytes_out: run.bytes_out,
|
|
1274
|
+
removed: run.removed,
|
|
1275
|
+
duplicates_removed: run.duplicates_removed,
|
|
1276
|
+
verify_failures: run.verify_failures,
|
|
1277
|
+
});
|
|
1278
|
+
this.logTickLine(
|
|
1279
|
+
"files",
|
|
1280
|
+
`${run.outcome}:${run.held_reason ?? ""}:${run.prs_held}:${run.prs_partial}`,
|
|
1281
|
+
run.prs_archived + run.transcripts + run.state_files + run.removed + run.verify_failures > 0,
|
|
1282
|
+
`archive: files — ${run.prs_archived} PR(s) archived, ${run.prs_partial} partial, ${run.prs_held} held; ` +
|
|
1283
|
+
`${run.transcripts} transcript(s), ${run.state_files} state file(s), ${run.removed} removed` +
|
|
1284
|
+
(run.duplicates_removed ? `, ${run.duplicates_removed} legacy duplicate(s) removed` : "") +
|
|
1285
|
+
(run.verify_failures ? `, ${run.verify_failures} VERIFY FAILURE(S)` : "") +
|
|
1286
|
+
` (${run.outcome}${run.held_reason ? `: ${run.held_reason}` : ""})`,
|
|
1287
|
+
);
|
|
1288
|
+
return { failure: null, hold };
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/** Log a per-tick line only when work was done or its shape changed. */
|
|
1292
|
+
private logTickLine(phase: string, shape: string, worked: boolean, line: string): void {
|
|
1293
|
+
if (!worked && this.lastTickLine.get(phase) === shape) return;
|
|
1294
|
+
this.lastTickLine.set(phase, shape);
|
|
1295
|
+
this.log(line);
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
/** The rate the file work runs at: an UNKNOWN load reading runs at the
|
|
1299
|
+
* minimum — one PR, half the MB/s (§10). */
|
|
1300
|
+
private fileRate(): { maxPrs: number; bytesPerS: number } {
|
|
1301
|
+
const load = this.readLoad();
|
|
1302
|
+
const unknown = !load || load.event_loop?.state !== "ok" || load.cpu?.state !== "measured";
|
|
1303
|
+
return unknown
|
|
1304
|
+
? { maxPrs: 1, bytesPerS: Math.max(1, Math.floor(this.policy.maxBytesPerS / 2)) }
|
|
1305
|
+
: { maxPrs: this.policy.maxPrsPerTick, bytesPerS: this.policy.maxBytesPerS };
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
/**
|
|
1309
|
+
* The stores that belong to no PR (§5): the overseer's non-current
|
|
1310
|
+
* transcripts, then pi's global store — each only when wired, under the
|
|
1311
|
+
* tick's budget, holds and disk checks. Returns the hold that stopped it,
|
|
1312
|
+
* or null. One store's I/O failure is that store's (`failed`, retried next
|
|
1313
|
+
* tick); it never fails the tick.
|
|
1314
|
+
*/
|
|
1315
|
+
private async storesPhase(at: Date): Promise<ArchiveHoldReason | null> {
|
|
1316
|
+
const passes: Array<{
|
|
1317
|
+
key: "overseer" | "pi_global";
|
|
1318
|
+
run: (common: StoreCommon) => Promise<StorePassResult>;
|
|
1319
|
+
}> = [];
|
|
1320
|
+
const bornAt = this.deps.overseerBornAt;
|
|
1321
|
+
if (bornAt)
|
|
1322
|
+
passes.push({
|
|
1323
|
+
key: "overseer",
|
|
1324
|
+
run: (common) =>
|
|
1325
|
+
archiveOverseer({
|
|
1326
|
+
...common,
|
|
1327
|
+
root: this.deps.root,
|
|
1328
|
+
sessionsRoot: this.deps.sessionsRoot,
|
|
1329
|
+
bornAt,
|
|
1330
|
+
}),
|
|
1331
|
+
});
|
|
1332
|
+
const piGlobalDir = this.deps.piGlobalDir;
|
|
1333
|
+
if (piGlobalDir)
|
|
1334
|
+
passes.push({
|
|
1335
|
+
key: "pi_global",
|
|
1336
|
+
run: (common) =>
|
|
1337
|
+
archivePiGlobal({ ...common, root: this.deps.root, storeDir: piGlobalDir }),
|
|
1338
|
+
});
|
|
1339
|
+
const meter = new SpanMeter();
|
|
1340
|
+
const fold = (): void => {
|
|
1341
|
+
if (meter.max > 0) this.foldSpan(meter.max);
|
|
1342
|
+
};
|
|
1343
|
+
for (const p of passes) {
|
|
1344
|
+
const stop = this.fileStop(at);
|
|
1345
|
+
if (stop) {
|
|
1346
|
+
fold();
|
|
1347
|
+
return isHoldReason(stop) ? stop : null;
|
|
1348
|
+
}
|
|
1349
|
+
const startedAt = this.now().toISOString();
|
|
1350
|
+
const common: StoreCommon = {
|
|
1351
|
+
now: this.now,
|
|
1352
|
+
level: this.policy.gzipLevel,
|
|
1353
|
+
bytesPerS: this.fileRate().bytesPerS,
|
|
1354
|
+
signal: this.abort.signal,
|
|
1355
|
+
diskFor: (bytes) => this.diskFor(bytes),
|
|
1356
|
+
stopReason: () => this.fileStop(at),
|
|
1357
|
+
verifyFailed: (key, detail) => this.raiseQuiet(key, detail),
|
|
1358
|
+
log: this.log,
|
|
1359
|
+
duplicateRemoved: () => bumpMark(this.deps.store, "duplicates.removed", 1),
|
|
1360
|
+
// Every synchronous span over the store's manifest (its shards'
|
|
1361
|
+
// parses, the index, each write's copy, check and stringify).
|
|
1362
|
+
meter,
|
|
1363
|
+
settle: !this.storesSettled.has(p.key),
|
|
1364
|
+
...(this.deps.step ? { step: this.deps.step } : {}),
|
|
1365
|
+
};
|
|
1366
|
+
let r: StorePassResult;
|
|
1367
|
+
try {
|
|
1368
|
+
r = await p.run(common);
|
|
1369
|
+
} catch (err) {
|
|
1370
|
+
if (this.abort.signal.aborted) throw err;
|
|
1371
|
+
this.log(`archive: the ${p.key} store's pass failed`, err);
|
|
1372
|
+
r = emptyStoreResult("failed", (err as Error).message);
|
|
1373
|
+
}
|
|
1374
|
+
if (r.outcome !== "held" && r.outcome !== "failed") this.storesSettled.add(p.key);
|
|
1375
|
+
this.stores[p.key] = {
|
|
1376
|
+
last_run_at: startedAt,
|
|
1377
|
+
outcome: r.outcome,
|
|
1378
|
+
reason: r.reason,
|
|
1379
|
+
archived: r.archived,
|
|
1380
|
+
bytes_in: r.bytesIn,
|
|
1381
|
+
bytes_out: r.bytesOut,
|
|
1382
|
+
removed: r.removed,
|
|
1383
|
+
duplicates_removed: r.duplicatesRemoved,
|
|
1384
|
+
verify_failures: r.verifyFailures,
|
|
1385
|
+
kept_live: r.keptLive,
|
|
1386
|
+
};
|
|
1387
|
+
if (r.archived + r.removed + r.duplicatesRemoved > 0) {
|
|
1388
|
+
this.dbCache = null;
|
|
1389
|
+
this.deps.store.logEvent("archive_pass", null, {
|
|
1390
|
+
phase: p.key === "overseer" ? OVERSEER_STORE : PI_GLOBAL_STORE,
|
|
1391
|
+
outcome: r.outcome,
|
|
1392
|
+
archived: r.archived,
|
|
1393
|
+
bytes_in: r.bytesIn,
|
|
1394
|
+
bytes_out: r.bytesOut,
|
|
1395
|
+
removed: r.removed,
|
|
1396
|
+
duplicates_removed: r.duplicatesRemoved,
|
|
1397
|
+
verify_failures: r.verifyFailures,
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
if (r.outcome !== "none")
|
|
1401
|
+
this.logTickLine(
|
|
1402
|
+
`store:${p.key}`,
|
|
1403
|
+
`${r.outcome}:${this.stores[p.key]?.reason ?? ""}`,
|
|
1404
|
+
r.archived + r.removed + r.duplicatesRemoved + r.verifyFailures > 0,
|
|
1405
|
+
`archive: ${storePhraseOf(p.key, this.stores[p.key])}`,
|
|
1406
|
+
);
|
|
1407
|
+
// A hold ends the tick — except one file too large for the archive
|
|
1408
|
+
// mount while both mounts keep their headroom: that store waits (its
|
|
1409
|
+
// status says disk_low, the wake was raised once for the incident) and
|
|
1410
|
+
// the next store goes on, as one large PR file never stops the others.
|
|
1411
|
+
if (r.reason && isHoldReason(r.reason) && !this.oneFileTooLarge(r.reason)) {
|
|
1412
|
+
fold();
|
|
1413
|
+
return r.reason;
|
|
1414
|
+
}
|
|
1415
|
+
await yieldToLoop();
|
|
1416
|
+
}
|
|
1417
|
+
fold();
|
|
1418
|
+
return null;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
/** The archive databases, opened on first use (mode `on` only). */
|
|
1422
|
+
private async archiveDb(): Promise<ArchiveDb> {
|
|
1423
|
+
if (this.adb) return this.adb;
|
|
1424
|
+
const adb = new ArchiveDb(archiveDbDir(this.deps.root), {
|
|
1425
|
+
now: this.now,
|
|
1426
|
+
log: this.log,
|
|
1427
|
+
});
|
|
1428
|
+
await adb.open();
|
|
1429
|
+
this.adb = adb;
|
|
1430
|
+
return adb;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
/** The load half of the holds, as it reads now. */
|
|
1434
|
+
private loadReason(): ArchiveHoldReason | null {
|
|
1435
|
+
return loadHold({
|
|
1436
|
+
draining: this.deps.store.drainState().draining,
|
|
1437
|
+
load: this.readLoad(),
|
|
1438
|
+
starvedRatio: this.deps.starvedRatio ?? DEFAULT_EVENT_LOOP_STARVED_THROTTLE_RATIO,
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
/** The hold, re-read before every batch and every seal. */
|
|
1443
|
+
private batchHold(): string | null {
|
|
1444
|
+
return this.interruption() ?? this.loadReason();
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
/** Is incident `key` to be raised now — its first time, or ANOMALY_REPEAT_MS
|
|
1448
|
+
* after the last? Admitting it starts that window. */
|
|
1449
|
+
private admitIncident(key: string): boolean {
|
|
1450
|
+
const nowMs = this.now().getTime();
|
|
1451
|
+
const last = this.raised.get(key);
|
|
1452
|
+
if (last !== undefined && nowMs - last < ANOMALY_REPEAT_MS) return false;
|
|
1453
|
+
this.raised.set(key, nowMs);
|
|
1454
|
+
return true;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/** Fold a measured synchronous span (ms) into the longest seen, to 0.1 ms. */
|
|
1458
|
+
private foldSpan(ms: number): void {
|
|
1459
|
+
this.maxSyncSpanMs = Math.max(this.maxSyncSpanMs ?? 0, Math.round(ms * 10) / 10);
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
/** The quiet `archive_verify_failed` anomaly, once per incident (`key`) and
|
|
1463
|
+
* again only after ANOMALY_REPEAT_MS; a repeat inside it is logged only. */
|
|
1464
|
+
private raiseQuiet(key: string, detail: string): void {
|
|
1465
|
+
if (this.raiseAnomaly("archive_verify_failed", null, key, detail)) this.dbCache = null;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
/** The incident protocol both emitters share: admit `key` (or log the
|
|
1469
|
+
* repeat), write the anomaly, and un-admit `key` when the write fails so the
|
|
1470
|
+
* next occurrence raises it. True when the anomaly was written. */
|
|
1471
|
+
private raiseAnomaly(kind: string, pr: number | null, key: string, detail: string): boolean {
|
|
1472
|
+
if (!this.admitIncident(key)) {
|
|
1473
|
+
this.log(`archive: ${detail} (${kind} already raised for ${key})`);
|
|
1474
|
+
return false;
|
|
1475
|
+
}
|
|
1476
|
+
try {
|
|
1477
|
+
this.deps.store.logAnomaly(kind, pr, null, detail);
|
|
1478
|
+
return true;
|
|
1479
|
+
} catch (err) {
|
|
1480
|
+
// Not recorded, so it holds no window: the next occurrence raises it.
|
|
1481
|
+
this.raised.delete(key);
|
|
1482
|
+
this.log(`archive: could not raise ${kind}`, err);
|
|
1483
|
+
return false;
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
/**
|
|
1488
|
+
* Move the reader-less event rows and the old deliveries (§9.2). A batch a
|
|
1489
|
+
* lock or a check refused deletes nothing and is REPORTED (status `db`,
|
|
1490
|
+
* the log); only an unexpected failure fails the tick. Returns the failure,
|
|
1491
|
+
* or null.
|
|
1492
|
+
*/
|
|
1493
|
+
private async dbPhase(): Promise<string | null> {
|
|
1494
|
+
const startedAt = this.now().toISOString();
|
|
1495
|
+
this.dbRoomHold = null;
|
|
1496
|
+
let adb: ArchiveDb;
|
|
1497
|
+
try {
|
|
1498
|
+
adb = await this.archiveDb();
|
|
1499
|
+
if (!this.sealsRecovered) {
|
|
1500
|
+
const rec = await recoverSeals({
|
|
1501
|
+
adb,
|
|
1502
|
+
maxBytesPerS: this.policy.maxBytesPerS,
|
|
1503
|
+
signal: this.abort.signal,
|
|
1504
|
+
now: this.now,
|
|
1505
|
+
log: this.log,
|
|
1506
|
+
});
|
|
1507
|
+
this.sealsRecovered = true;
|
|
1508
|
+
for (const p of rec.problems)
|
|
1509
|
+
this.raiseQuiet(`${p.code}:${p.file}`, `archive seal recovery: ${p.detail}`);
|
|
1510
|
+
}
|
|
1511
|
+
} catch (err) {
|
|
1512
|
+
if (this.abort.signal.aborted) throw err;
|
|
1513
|
+
const why =
|
|
1514
|
+
err instanceof ManifestUnreadable
|
|
1515
|
+
? `archive db: ${err.message} — nothing is moved until it is readable`
|
|
1516
|
+
: `archive db: ${(err as Error).message}`;
|
|
1517
|
+
this.log(`archive: ${why}`, err);
|
|
1518
|
+
if (err instanceof ManifestUnreadable) this.raiseQuiet("db_manifest_unreadable", why);
|
|
1519
|
+
return why;
|
|
1520
|
+
}
|
|
1521
|
+
this.mover ??= new DbMover({
|
|
1522
|
+
store: this.deps.store,
|
|
1523
|
+
adb,
|
|
1524
|
+
now: this.now,
|
|
1525
|
+
eventsAfterDays: this.policy.eventsAfterDays,
|
|
1526
|
+
afterDays: this.policy.afterDays,
|
|
1527
|
+
batchRows: this.policy.dbBatchRows,
|
|
1528
|
+
wallBudgetMs: this.policy.tickBudgetMs,
|
|
1529
|
+
signal: this.abort.signal,
|
|
1530
|
+
hold: () => this.batchHold(),
|
|
1531
|
+
room: async () => (this.dbRoomHold = await this.checkDisk()),
|
|
1532
|
+
...(this.deps.sleep ? { sleep: this.deps.sleep } : {}),
|
|
1533
|
+
...(this.deps.clock ? { clock: this.deps.clock } : {}),
|
|
1534
|
+
log: this.log,
|
|
1535
|
+
});
|
|
1536
|
+
const r = await this.mover.run();
|
|
1537
|
+
// Stopped: the daemon may close the store right after stop(), so nothing
|
|
1538
|
+
// is written; the rows of the cut batch are in both places and converge.
|
|
1539
|
+
if (r.outcome === "aborted" || this.abort.signal.aborted) throw new Error("stopped");
|
|
1540
|
+
this.recordDb(startedAt, adb, r);
|
|
1541
|
+
if (r.outcome === "failed") return `archive db: ${r.error ?? "the mover failed"}`;
|
|
1542
|
+
return null;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
private recordDb(at: string, adb: ArchiveDb, r: DbMoveResult): void {
|
|
1546
|
+
const counts = adb.counts();
|
|
1547
|
+
this.db = {
|
|
1548
|
+
last_run_at: at,
|
|
1549
|
+
outcome: r.outcome,
|
|
1550
|
+
skipped_reason: r.skipped_reason,
|
|
1551
|
+
events_moved: r.events.moved,
|
|
1552
|
+
deliveries_moved: r.deliveries.moved,
|
|
1553
|
+
batch_rows: r.batch_rows,
|
|
1554
|
+
sync_ms: r.sync_ms,
|
|
1555
|
+
months_open: counts.open,
|
|
1556
|
+
months_sealed: counts.sealed,
|
|
1557
|
+
};
|
|
1558
|
+
this.foldSpan(r.max_span_ms);
|
|
1559
|
+
this.movedSincePlan.events += r.events.moved;
|
|
1560
|
+
this.movedSincePlan.deliveries += r.deliveries.moved;
|
|
1561
|
+
if (r.events.moved + r.deliveries.moved > 0) this.dbCache = null;
|
|
1562
|
+
const undatable = r.events.undatable + r.deliveries.undatable;
|
|
1563
|
+
if (r.outcome === "skipped")
|
|
1564
|
+
this.log(
|
|
1565
|
+
r.skipped_reason === "checkpoint_failed"
|
|
1566
|
+
? `archive: db run STOPPED (checkpoint_failed) after its last batch — the rows it moved stay moved, the live automatic checkpoint is back on; the next tick retries: ${r.error}`
|
|
1567
|
+
: `archive: db batch SKIPPED (${r.skipped_reason}) — nothing deleted; the next tick retries: ${r.error}`,
|
|
1568
|
+
);
|
|
1569
|
+
for (const sk of r.skips)
|
|
1570
|
+
if (sk.reason === "verify_mismatch")
|
|
1571
|
+
this.raiseQuiet(
|
|
1572
|
+
`verify_mismatch:${sk.file ?? "?"}:${sk.table}`,
|
|
1573
|
+
`archive db: ${sk.table} rows read back from the archive absent or different; nothing deleted (${sk.error})`,
|
|
1574
|
+
);
|
|
1575
|
+
if (r.events.moved + r.deliveries.moved > 0 || undatable > 0)
|
|
1576
|
+
this.deps.store.logEvent("archive_pass", null, {
|
|
1577
|
+
phase: "db",
|
|
1578
|
+
outcome: r.outcome,
|
|
1579
|
+
events_moved: r.events.moved,
|
|
1580
|
+
deliveries_moved: r.deliveries.moved,
|
|
1581
|
+
undatable_kept: undatable,
|
|
1582
|
+
sync_ms: r.sync_ms,
|
|
1583
|
+
max_span_ms: r.max_span_ms,
|
|
1584
|
+
batch_rows: r.batch_rows,
|
|
1585
|
+
live_checkpoints: r.live_checkpoints,
|
|
1586
|
+
live_checkpoint_max_ms: r.live_checkpoint_max_ms,
|
|
1587
|
+
});
|
|
1588
|
+
this.logTickLine(
|
|
1589
|
+
"db",
|
|
1590
|
+
`${r.outcome}:${r.skipped_reason ?? ""}`,
|
|
1591
|
+
r.events.moved + r.deliveries.moved + undatable + r.live_checkpoints > 0,
|
|
1592
|
+
`archive: db — ${r.events.moved} event / ${r.deliveries.moved} delivery row(s) moved ` +
|
|
1593
|
+
`(${r.outcome}${r.skipped_reason ? `: ${r.skipped_reason}` : ""}); ` +
|
|
1594
|
+
`${r.sync_ms} ms synchronous, longest ${r.max_span_ms} ms; next batch ${r.batch_rows} rows` +
|
|
1595
|
+
(r.live_checkpoints > 0
|
|
1596
|
+
? `; ${r.live_checkpoints} live checkpoint(s) off the loop, longest ${r.live_checkpoint_max_ms} ms`
|
|
1597
|
+
: "") +
|
|
1598
|
+
(undatable > 0 ? `; ${undatable} undatable row(s) kept live` : ""),
|
|
1599
|
+
);
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
/** Seal the months whose rows can no longer arrive (§9.5). A seal the rule
|
|
1603
|
+
* or a check refuses is reported and retried later; only an unexpected
|
|
1604
|
+
* failure fails the tick. */
|
|
1605
|
+
private async sealPhase(): Promise<string | null> {
|
|
1606
|
+
this.sealRoomHold = null;
|
|
1607
|
+
const adb = this.adb;
|
|
1608
|
+
if (!adb) return null;
|
|
1609
|
+
let results: SealResult[];
|
|
1610
|
+
try {
|
|
1611
|
+
results = await sealDue({
|
|
1612
|
+
store: this.deps.store,
|
|
1613
|
+
adb,
|
|
1614
|
+
now: this.now,
|
|
1615
|
+
eventsAfterDays: this.policy.eventsAfterDays,
|
|
1616
|
+
afterDays: this.policy.afterDays,
|
|
1617
|
+
gzipLevel: this.policy.gzipLevel,
|
|
1618
|
+
maxBytesPerS: this.policy.maxBytesPerS,
|
|
1619
|
+
signal: this.abort.signal,
|
|
1620
|
+
hold: () => this.batchHold(),
|
|
1621
|
+
roomFor: async (bytes) => (this.sealRoomHold = await this.checkDisk(bytes)),
|
|
1622
|
+
log: this.log,
|
|
1623
|
+
});
|
|
1624
|
+
} catch (err) {
|
|
1625
|
+
if (this.abort.signal.aborted) throw err;
|
|
1626
|
+
// A seal that failed partway (its .gz placed, the manifest not written)
|
|
1627
|
+
// is what recovery settles: run it again before the next tick's work.
|
|
1628
|
+
this.sealsRecovered = false;
|
|
1629
|
+
this.log("archive: sealing failed", err);
|
|
1630
|
+
return `archive seal: ${(err as Error).message}`;
|
|
1631
|
+
}
|
|
1632
|
+
if (this.abort.signal.aborted) throw new Error("stopped");
|
|
1633
|
+
for (const r of results) {
|
|
1634
|
+
if (r.outcome === "sealed") {
|
|
1635
|
+
this.deps.store.logEvent("archive_month_sealed", null, {
|
|
1636
|
+
file: r.file,
|
|
1637
|
+
gz_file: r.gz_file,
|
|
1638
|
+
tables: r.tables,
|
|
1639
|
+
});
|
|
1640
|
+
continue;
|
|
1641
|
+
}
|
|
1642
|
+
const line = `archive: ${r.file} not sealed (${r.outcome}): ${r.detail ?? ""}`;
|
|
1643
|
+
// A month waiting for room says so once, not every tick.
|
|
1644
|
+
if (r.outcome === "held") this.logTickLine(`seal:${r.file}`, line, false, line);
|
|
1645
|
+
else this.log(line);
|
|
1646
|
+
if (SEAL_PROBLEMS.has(r.outcome))
|
|
1647
|
+
this.raiseQuiet(
|
|
1648
|
+
`${r.outcome}:${r.file}`,
|
|
1649
|
+
`archive seal ${r.file}: ${r.outcome} — ${r.detail ?? ""}; nothing is removed`,
|
|
1650
|
+
);
|
|
1651
|
+
}
|
|
1652
|
+
if (results.length > 0) this.dbCache = null;
|
|
1653
|
+
const counts = adb.counts();
|
|
1654
|
+
if (this.db) this.db = { ...this.db, months_open: counts.open, months_sealed: counts.sealed };
|
|
1655
|
+
return null;
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
private computePlan(): Promise<ArchivePlan> {
|
|
1659
|
+
const signal = this.abort.signal;
|
|
1660
|
+
if (this.deps.plan) return this.deps.plan({ signal, memo: this.memo });
|
|
1661
|
+
return computePlan({
|
|
1662
|
+
store: this.deps.store,
|
|
1663
|
+
root: this.deps.root,
|
|
1664
|
+
sessionsRoot: this.deps.sessionsRoot,
|
|
1665
|
+
now: this.now,
|
|
1666
|
+
afterDays: this.policy.afterDays,
|
|
1667
|
+
eventsAfterDays: this.policy.eventsAfterDays,
|
|
1668
|
+
liveSessions: this.deps.liveSessions ?? (async () => null),
|
|
1669
|
+
...(this.deps.birthInFlight ? { birthInFlight: this.deps.birthInFlight } : {}),
|
|
1670
|
+
...(this.deps.overseerBornAt ? { overseerBornAt: this.deps.overseerBornAt } : {}),
|
|
1671
|
+
...(this.deps.piGlobalDir ? { piGlobalDir: this.deps.piGlobalDir } : {}),
|
|
1672
|
+
memo: this.memo,
|
|
1673
|
+
signal,
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
/**
|
|
1678
|
+
* Free space on both mounts against the headroom — plus, for a file about
|
|
1679
|
+
* to be archived, its size on the archive mount (§7.7); records the
|
|
1680
|
+
* reading. A failed statfs is UNKNOWN (a hold, never a guess). In mode `on`
|
|
1681
|
+
* a low reading is the `archive_disk_low` wake, once per incident (the
|
|
1682
|
+
* incident ends at the first reading with room for what raised it) and
|
|
1683
|
+
* again every ANOMALY_REPEAT_MS while it lasts.
|
|
1684
|
+
*/
|
|
1685
|
+
private async checkDisk(fileBytes = 0): Promise<"disk_low" | "disk_unknown" | null> {
|
|
1686
|
+
const statfs = this.deps.statfs ?? ((p: string) => fsStatfs(p));
|
|
1687
|
+
const headroom = this.policy.diskHeadroomBytes;
|
|
1688
|
+
let archiveFree: number;
|
|
1689
|
+
let stateFree: number;
|
|
1690
|
+
try {
|
|
1691
|
+
[archiveFree, stateFree] = await Promise.all([
|
|
1692
|
+
freeBytes(join(this.deps.root, "archive"), statfs),
|
|
1693
|
+
freeBytes(join(this.deps.root, "state"), statfs),
|
|
1694
|
+
]);
|
|
1695
|
+
} catch (err) {
|
|
1696
|
+
this.disk = null;
|
|
1697
|
+
this.log("archive: statfs failed — disk UNKNOWN, holding", err);
|
|
1698
|
+
return "disk_unknown";
|
|
1699
|
+
}
|
|
1700
|
+
this.disk = { archive_free: archiveFree, state_free: stateFree, headroom };
|
|
1701
|
+
const low = archiveFree < fileBytes + headroom || stateFree < headroom;
|
|
1702
|
+
if (this.policy.mode === "on")
|
|
1703
|
+
this.noteDisk(low, { archiveFree, stateFree, headroom, fileBytes });
|
|
1704
|
+
return low ? "disk_low" : null;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
private noteDisk(
|
|
1708
|
+
low: boolean,
|
|
1709
|
+
r: { archiveFree: number; stateFree: number; headroom: number; fileBytes: number },
|
|
1710
|
+
): void {
|
|
1711
|
+
const nowMs = this.now().getTime();
|
|
1712
|
+
const need = r.fileBytes + r.headroom;
|
|
1713
|
+
if (!low) {
|
|
1714
|
+
// An incident ends only at a reading with room for what raised it. A
|
|
1715
|
+
// reading that tested less — the tick's headroom-only check, or a
|
|
1716
|
+
// smaller file — says nothing about the file that did not fit, and
|
|
1717
|
+
// ending the incident on it would re-raise the wake every tick.
|
|
1718
|
+
if (this.diskIncident && r.archiveFree >= this.diskIncident.need) {
|
|
1719
|
+
this.log("archive: free disk is back above the headroom");
|
|
1720
|
+
this.diskIncident = null;
|
|
1721
|
+
}
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
if (this.diskIncident) {
|
|
1725
|
+
this.diskIncident.need = Math.max(this.diskIncident.need, need);
|
|
1726
|
+
if (nowMs - this.diskIncident.raisedAt < ANOMALY_REPEAT_MS) return;
|
|
1727
|
+
}
|
|
1728
|
+
this.diskIncident = { raisedAt: nowMs, need: Math.max(this.diskIncident?.need ?? 0, need) };
|
|
1729
|
+
const detail =
|
|
1730
|
+
`archive held: free disk below the headroom — archive mount ${r.archiveFree} B free` +
|
|
1731
|
+
(r.fileBytes > 0 ? ` (a ${r.fileBytes} B file needs ${r.fileBytes + r.headroom})` : "") +
|
|
1732
|
+
`, state mount ${r.stateFree} B free, headroom ${r.headroom} B. No file is started until there is room.`;
|
|
1733
|
+
try {
|
|
1734
|
+
this.deps.store.logAnomaly("archive_disk_low", null, null, detail);
|
|
1735
|
+
} catch (err) {
|
|
1736
|
+
// Not recorded, so it holds no window: the next low reading raises it.
|
|
1737
|
+
this.diskIncident.raisedAt = Number.NEGATIVE_INFINITY;
|
|
1738
|
+
this.log("archive: could not raise archive_disk_low", err);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
/**
|
|
1743
|
+
* `state/live/` and `state/lens/`: measure them against the cap
|
|
1744
|
+
* and, in mode `on`, apply the retention (a file leaves only when the PR's
|
|
1745
|
+
* archive holds it verified, `liveretention.ts`). Past the cap the
|
|
1746
|
+
* `archive_live_cap` wake fires, once per incident and every 6 h; nothing is
|
|
1747
|
+
* ever deleted to get under it. Runs when a plan was just computed and
|
|
1748
|
+
* otherwise at the plan's cadence. A failure is logged and retried; it never
|
|
1749
|
+
* fails the tick.
|
|
1750
|
+
*/
|
|
1751
|
+
private async livePhase(at: Date, on: boolean, due: boolean): Promise<void> {
|
|
1752
|
+
const nowMs = at.getTime();
|
|
1753
|
+
if (!due && this.liveSweptAtMs !== null && nowMs - this.liveSweptAtMs < PLAN_REFRESH_MS) return;
|
|
1754
|
+
try {
|
|
1755
|
+
const r = await (this.deps.sweepLive ?? sweepLiveTrees)({
|
|
1756
|
+
root: this.deps.root,
|
|
1757
|
+
now: this.now(),
|
|
1758
|
+
retentionMs: this.policy.liveRetentionMs,
|
|
1759
|
+
remove: on,
|
|
1760
|
+
signal: this.abort.signal,
|
|
1761
|
+
});
|
|
1762
|
+
this.liveSweptAtMs = nowMs;
|
|
1763
|
+
const cap = this.policy.liveCapBytes;
|
|
1764
|
+
const over = overLiveCap(r.bytes_after, cap);
|
|
1765
|
+
this.live = {
|
|
1766
|
+
at: r.at,
|
|
1767
|
+
bytes: r.bytes_after,
|
|
1768
|
+
files: r.files - r.removed_files,
|
|
1769
|
+
cap_bytes: cap > 0 ? cap : null,
|
|
1770
|
+
over_cap: over,
|
|
1771
|
+
removed_files: r.removed_files,
|
|
1772
|
+
removed_bytes: r.removed_bytes,
|
|
1773
|
+
unarchived_files: r.unarchived_files,
|
|
1774
|
+
unarchived_bytes: r.unarchived_bytes,
|
|
1775
|
+
};
|
|
1776
|
+
if (r.removed_files > 0 || over)
|
|
1777
|
+
this.log(
|
|
1778
|
+
`archive: worker logs — ${r.removed_files} file(s), ${r.removed_bytes} bytes past retention and archived removed; ` +
|
|
1779
|
+
`${r.bytes_after} bytes left under state/live + state/lens${over ? `, OVER the ${cap} byte cap` : ""}`,
|
|
1780
|
+
);
|
|
1781
|
+
for (const pr of r.manifest_unreadable)
|
|
1782
|
+
this.raiseQuiet(
|
|
1783
|
+
`live:${pr}`,
|
|
1784
|
+
`archive pr-${pr}: manifest unreadable (${r.manifest_errors[String(pr)] ?? "unknown"}) — its worker logs were kept`,
|
|
1785
|
+
);
|
|
1786
|
+
if (over) {
|
|
1787
|
+
this.raiseWake(
|
|
1788
|
+
"archive_live_cap",
|
|
1789
|
+
null,
|
|
1790
|
+
"live_cap",
|
|
1791
|
+
`worker logs are over the disk cap: state/live + state/lens hold ${r.bytes_after} bytes in ` +
|
|
1792
|
+
`${r.files - r.removed_files} file(s), the cap is ${cap} bytes ([archive] live_cap_gb). ` +
|
|
1793
|
+
`${r.unarchived_files} file(s) (${r.unarchived_bytes} bytes) are past the retention with no verified archive copy ` +
|
|
1794
|
+
"and are kept: nothing is deleted to get under the cap. Close or archive the PRs that hold them, or raise the cap.",
|
|
1795
|
+
);
|
|
1796
|
+
} else this.raised.delete("live_cap");
|
|
1797
|
+
} catch (err) {
|
|
1798
|
+
if (this.abort.signal.aborted) return;
|
|
1799
|
+
this.log("archive: worker-log sweep failed — retried next tick", err);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
/** A WAKE kind, once per incident (`key`) and again only after
|
|
1804
|
+
* ANOMALY_REPEAT_MS. */
|
|
1805
|
+
private raiseWake(kind: string, pr: number | null, key: string, detail: string): void {
|
|
1806
|
+
this.raiseAnomaly(kind, pr, key, detail);
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
/** The database figures, re-read at most every STATUS_DB_TTL_MS; each is
|
|
1810
|
+
* null when it could not be read — never 0. */
|
|
1811
|
+
private dbFigures(): NonNullable<Archiver["dbCache"]> {
|
|
1812
|
+
const nowMs = this.now().getTime();
|
|
1813
|
+
if (this.dbCache && nowMs - this.dbCache.at < STATUS_DB_TTL_MS) return this.dbCache;
|
|
1814
|
+
const db = this.deps.store.db;
|
|
1815
|
+
let totals: ArchiveTotals | null = null;
|
|
1816
|
+
let restores: ArchiveStatus["restores"] = null;
|
|
1817
|
+
let freelist: number | null = null;
|
|
1818
|
+
let verifyFailures: number | null = null;
|
|
1819
|
+
try {
|
|
1820
|
+
const prs = db
|
|
1821
|
+
.prepare(
|
|
1822
|
+
`SELECT COUNT(*) AS prs, COALESCE(SUM(transcript_files + state_files), 0) AS files,
|
|
1823
|
+
COALESCE(SUM(bytes_in), 0) AS bytes_in, COALESCE(SUM(bytes_out), 0) AS bytes_out
|
|
1824
|
+
FROM archive_prs WHERE archived_at IS NOT NULL`,
|
|
1825
|
+
)
|
|
1826
|
+
.get() as { prs: number; files: number; bytes_in: number; bytes_out: number };
|
|
1827
|
+
totals = {
|
|
1828
|
+
...prs,
|
|
1829
|
+
event_rows: archivedEventTotal(this.deps.store),
|
|
1830
|
+
delivery_rows: markNumber(this.deps.store, DELIVERIES_TABLE.countMark!),
|
|
1831
|
+
duplicates_removed: markNumber(this.deps.store, "duplicates.removed"),
|
|
1832
|
+
};
|
|
1833
|
+
restores = db
|
|
1834
|
+
.prepare(
|
|
1835
|
+
`SELECT COALESCE(SUM(CASE WHEN a.state = 'restoring'
|
|
1836
|
+
OR (a.state IN ('archiving', 'archived') AND p.state IS NOT NULL AND p.state != 'closed')
|
|
1837
|
+
THEN 1 ELSE 0 END), 0) AS pending,
|
|
1838
|
+
COALESCE(SUM(CASE WHEN a.state = 'restored' THEN 1 ELSE 0 END), 0) AS done,
|
|
1839
|
+
COALESCE(SUM(CASE WHEN a.state = 'restore_failed' THEN 1 ELSE 0 END), 0) AS failed
|
|
1840
|
+
FROM archive_prs a LEFT JOIN pr_state p ON p.number = a.pr`,
|
|
1841
|
+
)
|
|
1842
|
+
.get() as { pending: number; done: number; failed: number };
|
|
1843
|
+
} catch (err) {
|
|
1844
|
+
this.log("archive: could not read the archive ledger", err);
|
|
1845
|
+
}
|
|
1846
|
+
try {
|
|
1847
|
+
// The quiet `archive_verify_failed` anomalies of the last 24 h: newest
|
|
1848
|
+
// first, stopping at the first row older than that.
|
|
1849
|
+
const since = new Date(nowMs - 86_400_000).toISOString();
|
|
1850
|
+
let n = 0;
|
|
1851
|
+
let scanned = 0;
|
|
1852
|
+
let complete = true;
|
|
1853
|
+
for (const row of db.prepare(VERIFY_FAILURES_SQL).iterate() as Iterable<{
|
|
1854
|
+
ts: unknown;
|
|
1855
|
+
k: unknown;
|
|
1856
|
+
}>) {
|
|
1857
|
+
if (typeof row.ts === "string" && row.ts < since) break;
|
|
1858
|
+
if (++scanned > VERIFY_FAILURES_SCAN_CAP) {
|
|
1859
|
+
complete = false;
|
|
1860
|
+
break;
|
|
1861
|
+
}
|
|
1862
|
+
if (row.k === "archive_verify_failed") n += 1;
|
|
1863
|
+
}
|
|
1864
|
+
verifyFailures = complete ? n : null;
|
|
1865
|
+
} catch {
|
|
1866
|
+
verifyFailures = null;
|
|
1867
|
+
}
|
|
1868
|
+
try {
|
|
1869
|
+
const n = db.pragma("freelist_count", { simple: true });
|
|
1870
|
+
freelist = typeof n === "number" ? n : null;
|
|
1871
|
+
} catch {
|
|
1872
|
+
freelist = null;
|
|
1873
|
+
}
|
|
1874
|
+
this.dbCache = { at: nowMs, totals, restores, freelist, verifyFailures };
|
|
1875
|
+
return this.dbCache;
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
/** status.json `archive` — synchronous, from what the last tick measured. */
|
|
1879
|
+
status(): ArchiveStatus {
|
|
1880
|
+
const base = pendingArchiveStatus(this.policy.mode);
|
|
1881
|
+
const db = this.dbFigures();
|
|
1882
|
+
const plan = this.plan;
|
|
1883
|
+
return {
|
|
1884
|
+
...base,
|
|
1885
|
+
state: this.state,
|
|
1886
|
+
held_reason: this.state === "held" ? this.heldReason : null,
|
|
1887
|
+
held_until: this.state === "held" ? this.heldUntil : null,
|
|
1888
|
+
running_since: this.runningSince,
|
|
1889
|
+
last_tick_at: this.lastTickAt,
|
|
1890
|
+
last_success_at: this.lastSuccessAt,
|
|
1891
|
+
last_error: this.lastError,
|
|
1892
|
+
plan_at: plan?.computed_at ?? null,
|
|
1893
|
+
backlog: plan
|
|
1894
|
+
? {
|
|
1895
|
+
prs: Math.max(0, plan.backlog.prs - this.filesSincePlan.prs),
|
|
1896
|
+
bytes: Math.max(0, plan.backlog.bytes - this.filesSincePlan.bytes),
|
|
1897
|
+
event_rows: Math.max(0, plan.backlog.event_rows - this.movedSincePlan.events),
|
|
1898
|
+
delivery_rows: Math.max(0, plan.backlog.delivery_rows - this.movedSincePlan.deliveries),
|
|
1899
|
+
}
|
|
1900
|
+
: null,
|
|
1901
|
+
totals: db.totals,
|
|
1902
|
+
disk: this.disk ? { ...this.disk } : null,
|
|
1903
|
+
// The quiet `archive_verify_failed` anomalies of the last 24 h (null
|
|
1904
|
+
// when they could not be counted — never a guessed 0).
|
|
1905
|
+
verify_failures_24h: db.verifyFailures,
|
|
1906
|
+
restores: db.restores,
|
|
1907
|
+
held_prs: plan ? this.heldPrs(plan) : null,
|
|
1908
|
+
orphans: plan ? { count: plan.orphans.length, first: plan.orphans.slice(0, 20) } : null,
|
|
1909
|
+
freelist_pages: db.freelist,
|
|
1910
|
+
max_sync_span_ms: this.maxSyncSpanMs,
|
|
1911
|
+
db: this.db ? { ...this.db } : null,
|
|
1912
|
+
files: this.files ? { ...this.files } : null,
|
|
1913
|
+
live: this.live ? { ...this.live } : null,
|
|
1914
|
+
stores: {
|
|
1915
|
+
overseer: this.stores.overseer ? { ...this.stores.overseer } : null,
|
|
1916
|
+
pi_global: this.stores.pi_global ? { ...this.stores.pi_global } : null,
|
|
1917
|
+
},
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
/** The plan's held PRs, and the planned ones whose own pass was held. */
|
|
1922
|
+
private heldPrs(plan: ArchivePlan): NonNullable<ArchiveStatus["held_prs"]> {
|
|
1923
|
+
const all = new Map<number, string>(plan.held.map((h) => [h.pr, h.reason]));
|
|
1924
|
+
for (const [pr, reason] of this.fileHeld) all.set(pr, reason);
|
|
1925
|
+
const list = [...all].sort((a, b) => a[0] - b[0]);
|
|
1926
|
+
return { count: list.length, first: list.slice(0, 20).map(([pr, reason]) => ({ pr, reason })) };
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
/** What every store pass is handed by the archiver. */
|
|
1931
|
+
type StoreCommon = Omit<import("./storepass.js").StorePassDeps, "dir" | "subject" | "liveOf">;
|
|
1932
|
+
|
|
1933
|
+
/** A store's last pass, for the log. */
|
|
1934
|
+
function storePhraseOf(key: "overseer" | "pi_global", st: ArchiveStoreStatus | null): string {
|
|
1935
|
+
if (!st) return `${key}: not run`;
|
|
1936
|
+
return (
|
|
1937
|
+
`${STORE_LABELS[key]} — ${st.archived} archived, ${st.removed} removed` +
|
|
1938
|
+
(st.duplicates_removed ? `, ${st.duplicates_removed} legacy duplicate(s) removed` : "") +
|
|
1939
|
+
(st.verify_failures ? `, ${st.verify_failures} VERIFY FAILURE(S)` : "") +
|
|
1940
|
+
`, ${st.kept_live} kept live (${st.outcome}${st.reason ? `: ${st.reason}` : ""})`
|
|
1941
|
+
);
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
const HOLD_REASONS: ReadonlySet<string> = new Set([
|
|
1945
|
+
"draining",
|
|
1946
|
+
"loop_stalled",
|
|
1947
|
+
"cpu_starved",
|
|
1948
|
+
"disk_low",
|
|
1949
|
+
"disk_unknown",
|
|
1950
|
+
]);
|
|
1951
|
+
const isHoldReason = (r: string): r is ArchiveHoldReason => HOLD_REASONS.has(r);
|
|
1952
|
+
|
|
1953
|
+
/** Wait for `p`, but never longer than `ms` (a real, unref'd timer). */
|
|
1954
|
+
async function waitAtMost(p: Promise<unknown>, ms: number): Promise<void> {
|
|
1955
|
+
if (!(ms > 0)) return;
|
|
1956
|
+
await withinBudget(
|
|
1957
|
+
p.then(
|
|
1958
|
+
() => undefined,
|
|
1959
|
+
() => undefined,
|
|
1960
|
+
),
|
|
1961
|
+
ms,
|
|
1962
|
+
);
|
|
1963
|
+
}
|