@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,1758 @@
|
|
|
1
|
+
// `MemoryStore` over OpenViking — the engine behind the memory seam. Nothing
|
|
2
|
+
// above this file learns that memory is an HTTP server: the TUI's verbs, the
|
|
3
|
+
// [feedback] sink and the companion feed address entries by
|
|
4
|
+
// `(id, ts, block)` + an expected hash.
|
|
5
|
+
//
|
|
6
|
+
// OpenViking keeps one file per entry under
|
|
7
|
+
// `viking://user/<user>/memories/<area>/`, so the address is the file. The
|
|
8
|
+
// seam's vocabulary maps onto it:
|
|
9
|
+
//
|
|
10
|
+
// id `<category>/<stamp>-<suffix>.md` — the path under the memories
|
|
11
|
+
// root, extension included, so `id → uri` is exact rather than
|
|
12
|
+
// guessed. Opaque to callers; only this module builds or reads it.
|
|
13
|
+
// ts parsed from the `<stamp>` half of the name, so an entry's time is a
|
|
14
|
+
// property of the entry and not of the server's mtime bookkeeping.
|
|
15
|
+
// block always 0. One entry per file; there is nothing to index into.
|
|
16
|
+
// hash `sha256:<hex>` of the stored bytes — the exact token batch-write's
|
|
17
|
+
// `replace_if_hash` precondition wants.
|
|
18
|
+
//
|
|
19
|
+
// Why `content/read` is a safe hash source here, given the `MEMORY_FIELDS`
|
|
20
|
+
// footgun: it is safe *because* of the rule above it, not despite it. Bare
|
|
21
|
+
// `content/write` appends a footer that `content/read` strips, which makes
|
|
22
|
+
// read → sha256 → write 409 forever; files written by `content/batch-write`
|
|
23
|
+
// carry no footer and round-trip byte-exact (verified). This module has no
|
|
24
|
+
// route to `content/write` — the client does not expose one — so every file it
|
|
25
|
+
// can see was written by batch-write, and the hash it computes is the hash the
|
|
26
|
+
// server holds. If that ever stops being true the symptom is a 409 on a fresh
|
|
27
|
+
// read, which surfaces as an ordinary refusal, never as a lost update.
|
|
28
|
+
//
|
|
29
|
+
// Tags come in two flavours, and this module owns neither vocabulary.
|
|
30
|
+
//
|
|
31
|
+
// * `#hash-tags` are the seam's own convention: `#pinned` rides an entry
|
|
32
|
+
// the pin verb moved (pinning itself is placement — see the layout),
|
|
33
|
+
// `#manual` marks the owner's own words, `#retired` is a retirement (and
|
|
34
|
+
// retired entries are filtered out of `read()`, which is what makes retire
|
|
35
|
+
// behave like retire). They are PREPENDED to the entry text, exactly as
|
|
36
|
+
// `src/memory/import.ts` and the [feedback] writer already spell them, so
|
|
37
|
+
// an entry means the same thing whichever store holds it.
|
|
38
|
+
// * `k=v` event tags (`kind=`, `pr=`, `day=`, `signal=`) are the vocabulary
|
|
39
|
+
// that the curator filters on and the auto-filer queries. They
|
|
40
|
+
// ride the entry header too, and they are what this store mirrors onto
|
|
41
|
+
// OpenViking's native retrieval tag list. The parser is
|
|
42
|
+
// `parseEventTags` from `src/memory/eventtags.ts` — imported, never re-implemented:
|
|
43
|
+
// it scans the HEADER only, so an untrusted PR body quoted later in the
|
|
44
|
+
// entry cannot mint itself a tag, and a second parser here would be free
|
|
45
|
+
// to disagree about exactly that.
|
|
46
|
+
//
|
|
47
|
+
// Both live in plain body text on purpose: plain text is what `content/read`
|
|
48
|
+
// returns unmodified and what every store re-parses. A `MEMORY_FIELDS`-style
|
|
49
|
+
// metadata footer is precisely what the server reserves for its own extractor
|
|
50
|
+
// and strips on read.
|
|
51
|
+
//
|
|
52
|
+
// The layout (the areas of src/memory/layout.ts). An id
|
|
53
|
+
// is `<area>/<file>`, an area one or two segments (`experiences`, `events`,
|
|
54
|
+
// `preferences`, `preferences/pinned`), so the tree is listed THREE levels
|
|
55
|
+
// deep. An entry is PINNED by placement — it sits in `preferences/pinned/` —
|
|
56
|
+
// and `pin`/`unpin` MOVE it there and back: the copy is created at the new
|
|
57
|
+
// address first, then the old file is replaced by a `#retired #moved` stub
|
|
58
|
+
// naming where it went. Never a delete (the store has no route to one), and
|
|
59
|
+
// never two live copies once a move has finished; a move whose outcome cannot
|
|
60
|
+
// be read is left as found rather than undone on a guess. The round's pinned head
|
|
61
|
+
// reads that one directory (`pinnedSweep`) — exact, and never a full sweep.
|
|
62
|
+
import { createHash } from "node:crypto";
|
|
63
|
+
import { formatEventTags, parseEventTags, parseHashTags } from "../memory/eventtags.js";
|
|
64
|
+
import { EXPERIENCES_AREA, FROM_TAG, MANUAL_TAG, PINNED_TAG, isRetired, MOVED_TAG, PINNED_AREA, PREFERENCES_AREA, RETIRED_TAG, SWEEP_EXCLUDED_AREAS, areaForNewEntry, areaOfId, isMovedStub, isPinnedId, nameOfId, pinnedIdFor, unpinTargetArea, validArea, withHeaderTags, withoutHeaderTag, } from "../memory/layout.js";
|
|
65
|
+
import { linksOf, normalizeTag, searchEntries, tagsOf, } from "../memory/store.js";
|
|
66
|
+
import { batchWriteLanded, } from "./client.js";
|
|
67
|
+
export { MANUAL_TAG, PINNED_TAG } from "../memory/layout.js";
|
|
68
|
+
/** Where a new entry lands. `experiences` is a real server quota bucket, which
|
|
69
|
+
* is what lets a context search size it. */
|
|
70
|
+
export const DEFAULT_CATEGORY = EXPERIENCES_AREA;
|
|
71
|
+
/** `sha256:<lowercase-hex>` over the exact stored bytes — batch-write's
|
|
72
|
+
* `base_hash` shape. */
|
|
73
|
+
export function sha256Prefixed(body) {
|
|
74
|
+
return `sha256:${createHash("sha256").update(Buffer.from(body, "utf8")).digest("hex")}`;
|
|
75
|
+
}
|
|
76
|
+
/** Is this a concurrency token `sha256Prefixed` could have produced? Every
|
|
77
|
+
* write entry point checks a caller's `expect` with it before sending. */
|
|
78
|
+
export function isHashToken(token) {
|
|
79
|
+
return /^sha256:[0-9a-f]{64}$/.test(token);
|
|
80
|
+
}
|
|
81
|
+
/** An id is a relative path of one to three safe segments ending in a text
|
|
82
|
+
* extension — `<file>`, `<area>/<file>` or `preferences/pinned/<file>`. Anything else is refused rather than turned
|
|
83
|
+
* into a URI: an id ultimately comes back from a caller that got it from us,
|
|
84
|
+
* and `..` in a path we interpolate is exactly the mistake worth making
|
|
85
|
+
* structurally impossible. */
|
|
86
|
+
const ID_RE = /^(?:[A-Za-z0-9._-]+\/){0,2}[A-Za-z0-9._-]+\.(?:md|txt)$/;
|
|
87
|
+
export function validEntryId(id) {
|
|
88
|
+
return ID_RE.test(id) && !id.includes("..") && !id.split("/").some((s) => s === ".");
|
|
89
|
+
}
|
|
90
|
+
/** The `#moved` stub a move leaves at the old address: retired (so every
|
|
91
|
+
* reader hides it and no recall returns it), naming where the entry went. */
|
|
92
|
+
export const MOVED_TO_TAG = "moved-to";
|
|
93
|
+
export function movedStub(to) {
|
|
94
|
+
return `#${RETIRED_TAG} #${MOVED_TAG} ${MOVED_TO_TAG}=${to}\nThis entry moved to ${to}.\n`;
|
|
95
|
+
}
|
|
96
|
+
/** `20260820T153000-a1b2c3d4.md` → `2026-08-20 15:30:00`. Null when the name
|
|
97
|
+
* carries no stamp — the caller then falls back to the server's modTime. */
|
|
98
|
+
export function tsFromName(name) {
|
|
99
|
+
const m = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})/.exec(name);
|
|
100
|
+
if (!m)
|
|
101
|
+
return null;
|
|
102
|
+
return `${m[1]}-${m[2]}-${m[3]} ${m[4]}:${m[5]}:${m[6]}`;
|
|
103
|
+
}
|
|
104
|
+
/** `2026-08-20T15:30:00.000Z` (or anything Date parses) → `2026-08-20 15:30:00`
|
|
105
|
+
* in UTC. Used only for files whose name carries no stamp. */
|
|
106
|
+
export function tsFromModTime(modTime) {
|
|
107
|
+
const d = new Date(modTime);
|
|
108
|
+
if (Number.isNaN(d.getTime()))
|
|
109
|
+
return "1970-01-01 00:00:00";
|
|
110
|
+
return d.toISOString().slice(0, 19).replace("T", " ");
|
|
111
|
+
}
|
|
112
|
+
/** `2026-08-20 15:30:00` → `20260820T153000`. */
|
|
113
|
+
export function stampOf(ts) {
|
|
114
|
+
return `${ts.slice(0, 10).replace(/-/g, "")}T${ts.slice(11).replace(/:/g, "")}`;
|
|
115
|
+
}
|
|
116
|
+
export { tagsOf, isMovedStub, MOVED_TAG, RETIRED_TAG };
|
|
117
|
+
/**
|
|
118
|
+
* The stored body for an entry: `#tags` first, then the text, then exactly one
|
|
119
|
+
* trailing newline.
|
|
120
|
+
*
|
|
121
|
+
* A tag goes at the FRONT, not into a footer line — that is where
|
|
122
|
+
* `src/memory/import.ts` puts `#manual`/`#pinned`, where the [feedback] writer
|
|
123
|
+
* puts `#feedback` and its `k=v` run, and where `parseEventTags` looks.
|
|
124
|
+
*/
|
|
125
|
+
export function renderEntryBody(text, addTags = []) {
|
|
126
|
+
const trimmed = text.trim();
|
|
127
|
+
if (trimmed === "")
|
|
128
|
+
return "";
|
|
129
|
+
// Header-only, like `pinned`: a tag quoted in the body is prose, so it
|
|
130
|
+
// must not stop the real one landing in the header where readers look.
|
|
131
|
+
const have = new Set(parseHashTags(trimmed));
|
|
132
|
+
const missing = addTags.map(normalizeTag).filter((t) => t !== "" && !have.has(t));
|
|
133
|
+
const head = missing.map((t) => `#${t}`).join(" ");
|
|
134
|
+
return `${head === "" ? trimmed : `${head} ${trimmed}`}\n`;
|
|
135
|
+
}
|
|
136
|
+
/** Prepend a tag to an entry's text, idempotently. */
|
|
137
|
+
export function withTag(text, tag) {
|
|
138
|
+
return renderEntryBody(text, [tag]);
|
|
139
|
+
}
|
|
140
|
+
/** Remove a tag from the entry's header — the leading run of `#tag` tokens
|
|
141
|
+
* `parseHashTags` reads, and where `withTag` puts one. Idempotent, never
|
|
142
|
+
* touches other tags; a `#tag` quoted in the body is prose and stays.
|
|
143
|
+
*
|
|
144
|
+
* Only the gap the token leaves is closed (the token takes its trailing
|
|
145
|
+
* spaces with it): the rest of the body — code indentation, aligned columns —
|
|
146
|
+
* is the owner's bytes, and an unpin must hand them back as `withTag` found
|
|
147
|
+
* them. */
|
|
148
|
+
export function withoutTag(text, tag) {
|
|
149
|
+
const want = `#${normalizeTag(tag)}`;
|
|
150
|
+
const tokens = text.trim().split(/(\s+)/);
|
|
151
|
+
let i = 0;
|
|
152
|
+
while (i < tokens.length && tokens[i].startsWith("#")) {
|
|
153
|
+
if (tokens[i].toLowerCase() !== want) {
|
|
154
|
+
i += 2;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const gap = (tokens[i + 1] ?? "").replace(/^[ \t]+/, "");
|
|
158
|
+
if (gap === "") {
|
|
159
|
+
tokens.splice(i, 2);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
// A line break after the token stays; the header goes on past it.
|
|
163
|
+
tokens.splice(i, 2, "", gap);
|
|
164
|
+
i += 2;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return `${tokens.join("").trim()}\n`;
|
|
168
|
+
}
|
|
169
|
+
/** Parse one stored file into an entry. Total: a file we cannot date still
|
|
170
|
+
* becomes an entry (with the server's modTime), because dropping a memory
|
|
171
|
+
* because its NAME is odd would be the worse failure. */
|
|
172
|
+
export function entryFrom(id, text, modTime) {
|
|
173
|
+
const name = nameOfId(id);
|
|
174
|
+
const ts = tsFromName(name) ?? tsFromModTime(modTime);
|
|
175
|
+
const trimmed = text.trim();
|
|
176
|
+
const tags = tagsOf(trimmed);
|
|
177
|
+
return {
|
|
178
|
+
id,
|
|
179
|
+
ts,
|
|
180
|
+
day: ts.slice(0, 10),
|
|
181
|
+
block: 0,
|
|
182
|
+
text: trimmed,
|
|
183
|
+
tags,
|
|
184
|
+
links: linksOf(trimmed),
|
|
185
|
+
// Placement, never text: a pinned entry rides the guaranteed
|
|
186
|
+
// head of every round prompt, and that head reads `preferences/pinned/`
|
|
187
|
+
// alone. An address is chosen by whoever writes the file (a `note` names
|
|
188
|
+
// its area, or its own header-only `#pinned` does — `areaForNewEntry`), so
|
|
189
|
+
// text quoted inside an entry can never pin it.
|
|
190
|
+
pinned: isPinnedId(id),
|
|
191
|
+
// The hash is over the STORED bytes, not the trimmed text — it is a
|
|
192
|
+
// precondition token for the server, not a digest of what we display.
|
|
193
|
+
hash: sha256Prefixed(text),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/** The server's `_BATCH_MAX_OPERATIONS` (storage/content_write.py, 0.4.15). */
|
|
197
|
+
export const BATCH_MAX_OPERATIONS = 256;
|
|
198
|
+
/** A listed file's change signature, or null when it cannot vouch for one. */
|
|
199
|
+
function signatureOf(file) {
|
|
200
|
+
if (file.modTime.trim() === "")
|
|
201
|
+
return null;
|
|
202
|
+
return `${file.size}:${file.modTime}`;
|
|
203
|
+
}
|
|
204
|
+
const DEFAULT_READ_CONCURRENCY = 8;
|
|
205
|
+
const DEFAULT_CACHE_TTL_MS = 15_000;
|
|
206
|
+
/** One full re-read an hour per store: the incremental sweeps in between
|
|
207
|
+
* read only what changed. */
|
|
208
|
+
export const DEFAULT_FULL_REFRESH_MS = 60 * 60_000;
|
|
209
|
+
export class OpenVikingStore {
|
|
210
|
+
client;
|
|
211
|
+
root;
|
|
212
|
+
category;
|
|
213
|
+
now;
|
|
214
|
+
log;
|
|
215
|
+
/** Category roots known to exist: one lazy mkdir per root per store
|
|
216
|
+
* instance — a second NOT_FOUND is a real error. */
|
|
217
|
+
ensuredRoots = new Set();
|
|
218
|
+
concurrency;
|
|
219
|
+
cacheTtlMs;
|
|
220
|
+
fullRefreshMs;
|
|
221
|
+
onRetired;
|
|
222
|
+
/** The per-file cache behind the incremental sweep, keyed by
|
|
223
|
+
* entry id. Survives `invalidate()` on purpose: invalidating means "list
|
|
224
|
+
* again", and the listing says which files moved. */
|
|
225
|
+
known = new Map();
|
|
226
|
+
/** When the last FULL re-read happened (ms), or null before the first. */
|
|
227
|
+
lastFullAt = null;
|
|
228
|
+
/** Sweeps of different generations can overlap; only a sweep that STARTED
|
|
229
|
+
* after the one whose file cache is adopted may replace it. */
|
|
230
|
+
sweepSeq = 0;
|
|
231
|
+
adoptedSeq = 0;
|
|
232
|
+
/** Files this store rewrote, by id → the sweep sequence current at the
|
|
233
|
+
* write: a sweep that started before it may have read the old bytes, so its
|
|
234
|
+
* record of that file is never adopted. */
|
|
235
|
+
forgotten = new Map();
|
|
236
|
+
stats = null;
|
|
237
|
+
/** Files listed directly in each area by the last adopted sweep
|
|
238
|
+
* (`areaSize`) — a count of the listing, so a file that failed to read
|
|
239
|
+
* still counts; an area whose listing failed keeps its last count (or is
|
|
240
|
+
* `unknown` without one). Null until a sweep was adopted. */
|
|
241
|
+
listedByArea = null;
|
|
242
|
+
cache = null;
|
|
243
|
+
inFlight = null;
|
|
244
|
+
/** The pinned area's own cache — same TTL and generation
|
|
245
|
+
* rules as the sweep's. */
|
|
246
|
+
pinnedCache = null;
|
|
247
|
+
pinnedInFlight = null;
|
|
248
|
+
/** Bumped by every write: a sweep started before it may have read the old
|
|
249
|
+
* bytes, so it neither fills the cache nor serves a later reader. */
|
|
250
|
+
generation = 0;
|
|
251
|
+
/** Retired URIs from the last sweep that completed un-raced by a write,
|
|
252
|
+
* plus every retire this store has written since. Never expires — see
|
|
253
|
+
* `retiredUris`. Null until one sweep has finished (or `initialRetired`
|
|
254
|
+
* seeded it). Only a sweep that read EVERYTHING may shrink it. */
|
|
255
|
+
knownRetired = null;
|
|
256
|
+
/** The list `onRetired` last accepted (the seed counts: it is the file a
|
|
257
|
+
* previous process published). Kept apart from `knownRetired`, so a failed
|
|
258
|
+
* publication is retried rather than taken for done. */
|
|
259
|
+
publishedRetired = null;
|
|
260
|
+
/** `initialRetired`, not yet taken (see `takeRetiredSeed`). */
|
|
261
|
+
retiredSeed;
|
|
262
|
+
constructor(opts) {
|
|
263
|
+
this.client = opts.client;
|
|
264
|
+
this.root = (opts.memoriesRoot ?? opts.client.memoriesRoot()).replace(/\/+$/, "");
|
|
265
|
+
this.category = opts.defaultCategory ?? DEFAULT_CATEGORY;
|
|
266
|
+
this.now = opts.now ?? (() => new Date());
|
|
267
|
+
this.log = opts.log ?? (() => { });
|
|
268
|
+
this.concurrency = Math.max(1, opts.readConcurrency ?? DEFAULT_READ_CONCURRENCY);
|
|
269
|
+
this.cacheTtlMs = opts.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
|
|
270
|
+
this.fullRefreshMs = Math.max(0, opts.fullRefreshMs ?? DEFAULT_FULL_REFRESH_MS);
|
|
271
|
+
this.onRetired = opts.onRetired;
|
|
272
|
+
const seed = opts.initialRetired;
|
|
273
|
+
this.retiredSeed =
|
|
274
|
+
seed === undefined || seed === null ? null : typeof seed === "function" ? seed : () => seed;
|
|
275
|
+
}
|
|
276
|
+
/** Start `knownRetired` from `initialRetired`, once, before anything reads
|
|
277
|
+
* or replaces it. A seed that throws is no seed. */
|
|
278
|
+
takeRetiredSeed() {
|
|
279
|
+
const seed = this.retiredSeed;
|
|
280
|
+
if (seed === null)
|
|
281
|
+
return;
|
|
282
|
+
this.retiredSeed = null;
|
|
283
|
+
if (this.knownRetired !== null)
|
|
284
|
+
return;
|
|
285
|
+
let uris = null;
|
|
286
|
+
try {
|
|
287
|
+
uris = seed();
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
uris = null;
|
|
291
|
+
}
|
|
292
|
+
if (uris) {
|
|
293
|
+
this.knownRetired = [...new Set(uris)].sort();
|
|
294
|
+
this.publishedRetired = this.knownRetired;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/** What the last COMPLETED sweep cost, or null before the first. */
|
|
298
|
+
lastSweepStats() {
|
|
299
|
+
return this.stats === null ? null : { ...this.stats };
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* How many files the last adopted sweep LISTED directly in `<root>/<area>/`
|
|
303
|
+
* — retired ones and unread ones included, because the engine indexes them
|
|
304
|
+
* all (it sizes the lesson find's `limit`). Synchronous, no
|
|
305
|
+
* I/O, never starts or waits for a sweep (the round prefetch's
|
|
306
|
+
* `retiredUris()` keeps the sweep fresh). Null before any sweep was
|
|
307
|
+
* adopted, and for an area whose listing failed with no earlier count;
|
|
308
|
+
* 0 for an area listed empty or absent. Never an area `SWEEP_EXCLUDED_AREAS`
|
|
309
|
+
* names (`finding-cases/`): the sweep does not list it, so it is unknown.
|
|
310
|
+
*/
|
|
311
|
+
areaSize(area) {
|
|
312
|
+
const dir = area.replace(/^\/+|\/+$/g, "");
|
|
313
|
+
if (this.listedByArea === null || dir === "")
|
|
314
|
+
return null;
|
|
315
|
+
if (SWEEP_EXCLUDED_AREAS.includes(dir.split("/")[0]))
|
|
316
|
+
return null;
|
|
317
|
+
const known = this.listedByArea.counts.get(dir);
|
|
318
|
+
if (known !== undefined)
|
|
319
|
+
return known;
|
|
320
|
+
for (const u of this.listedByArea.unknown)
|
|
321
|
+
if (dir === u || dir.startsWith(`${u}/`))
|
|
322
|
+
return null;
|
|
323
|
+
return 0;
|
|
324
|
+
}
|
|
325
|
+
uriOf(id) {
|
|
326
|
+
return `${this.root}/${id}`;
|
|
327
|
+
}
|
|
328
|
+
/** Drop the sweep cache. Called after every successful write so a caller
|
|
329
|
+
* that writes and immediately re-reads sees its own edit. The next sweep
|
|
330
|
+
* LISTS again and re-reads every file whose size or modification time moved
|
|
331
|
+
* — the per-file cache is not dropped, because the listing is
|
|
332
|
+
* what says which files changed; a write through THIS store also forgets
|
|
333
|
+
* the file it wrote, so even a same-size edit inside one clock tick is read. */
|
|
334
|
+
invalidate() {
|
|
335
|
+
this.cache = null;
|
|
336
|
+
this.pinnedCache = null;
|
|
337
|
+
this.generation += 1;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Materialize this namespace's category root, before anything writes to it.
|
|
341
|
+
*
|
|
342
|
+
* `create()` already self-heals a missing root, and for everything writing
|
|
343
|
+
* through THIS class that is enough. It is not enough for the companion,
|
|
344
|
+
* because the companion does not write through this class — it writes
|
|
345
|
+
* through the vendored pi extension, which chooses WHICH user namespace to
|
|
346
|
+
* write into by listing `viking://user/` and picking a directory that
|
|
347
|
+
* exists (`resolveScopeSpace`, extensions/vendor/openviking-pi/client.ts).
|
|
348
|
+
*
|
|
349
|
+
* To that resolver a namespace nobody has written to yet is not an empty
|
|
350
|
+
* namespace — it is an absent one. It silently falls back to `user/default`,
|
|
351
|
+
* whose `memories/` tree does not exist either, and every write is then
|
|
352
|
+
* refused — with an error ("access denied") that names neither the
|
|
353
|
+
* namespace nor the missing directory.
|
|
354
|
+
*
|
|
355
|
+
* So the root is created EAGERLY for each namespace, before the companion
|
|
356
|
+
* container starts and its extension caches that resolution. Idempotent
|
|
357
|
+
* (`mkdir` treats "already exists" as success) and total, like every other
|
|
358
|
+
* method here: a dead server is a refusal, never a throw.
|
|
359
|
+
*/
|
|
360
|
+
async ensureWritable() {
|
|
361
|
+
const uri = `${this.root}/${this.category}`;
|
|
362
|
+
if (this.ensuredRoots.has(uri))
|
|
363
|
+
return { ok: true, message: `${uri} ready` };
|
|
364
|
+
if (await this.client.mkdir(uri)) {
|
|
365
|
+
this.ensuredRoots.add(uri);
|
|
366
|
+
return { ok: true, message: `${uri} ready` };
|
|
367
|
+
}
|
|
368
|
+
return { ok: false, message: `could not create the memory root ${uri}` };
|
|
369
|
+
}
|
|
370
|
+
/** A write that answered NOT_FOUND on `dir`: make the area, once per store
|
|
371
|
+
* instance, and say whether the caller may retry. A fresh server holds only
|
|
372
|
+
* `viking://user/default`, and batch-write 404s on a missing area rather than
|
|
373
|
+
* creating it. Recorded only on success, so a transient mkdir failure leaves
|
|
374
|
+
* the next write free to try again; each caller keeps its own retry budget. */
|
|
375
|
+
async madeMissingArea(code, dir) {
|
|
376
|
+
if (code !== "NOT_FOUND" || this.ensuredRoots.has(dir))
|
|
377
|
+
return false;
|
|
378
|
+
if (!(await this.client.mkdir(dir)))
|
|
379
|
+
return false;
|
|
380
|
+
this.ensuredRoots.add(dir);
|
|
381
|
+
this.log(`openviking: created the memory root ${dir}`);
|
|
382
|
+
return true;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Every entry the store holds, oldest first, retirements filtered out.
|
|
386
|
+
*
|
|
387
|
+
* Total by construction: a server that is down yields `[]`, and a single
|
|
388
|
+
* unreadable file is skipped rather than losing the sweep. `[]` therefore
|
|
389
|
+
* means "nothing to show" for BOTH an empty store and an unread one — the
|
|
390
|
+
* right answer for a timeline. A caller that must tell the two apart (the
|
|
391
|
+
* round's pinned head) uses `sweep()`, which keeps the distinction.
|
|
392
|
+
*/
|
|
393
|
+
async read() {
|
|
394
|
+
return (await this.sweep()).entries;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* The same sweep, with the parts it could not read kept beside the entries.
|
|
398
|
+
* Cached and single-flight exactly like `read()` — the two share one sweep,
|
|
399
|
+
* so the round path and the TUI poll never double the server's load.
|
|
400
|
+
*/
|
|
401
|
+
async sweep() {
|
|
402
|
+
return (await this.sweepRun()).sweep;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* The URIs of every retired entry, for the round's context search to pass
|
|
406
|
+
* as `excludeUris`. `read()` hides a retirement by filtering, but the file
|
|
407
|
+
* stays under the indexed memories root, so a semantic search would still
|
|
408
|
+
* recall it.
|
|
409
|
+
*
|
|
410
|
+
* Answers from the LAST COMPLETED sweep whatever its age, and waits for a
|
|
411
|
+
* sweep only when none has ever completed; a list older than the cache TTL
|
|
412
|
+
* starts a fresh sweep in the background for the next caller
|
|
413
|
+
* (`refreshInBackground`). The cache is nearly always cold at a round, and
|
|
414
|
+
* waiting here would put a whole store sweep in series in front of the
|
|
415
|
+
* search. Retirements are rare and this store records its own (and its
|
|
416
|
+
* moves' stubs) the moment they land, so a list one round old costs at most
|
|
417
|
+
* one round's recall of an entry retired elsewhere. (A store seeded with
|
|
418
|
+
* `initialRetired` answers from that until its first sweep.)
|
|
419
|
+
*/
|
|
420
|
+
async retiredUris() {
|
|
421
|
+
this.takeRetiredSeed();
|
|
422
|
+
if (this.knownRetired !== null) {
|
|
423
|
+
this.refreshInBackground();
|
|
424
|
+
return [...this.knownRetired];
|
|
425
|
+
}
|
|
426
|
+
return [...(await this.sweepRun()).retired];
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Start a sweep for the NEXT caller when the last one is older than the
|
|
430
|
+
* cache TTL — never awaited, never thrown. The round's pinned head reads
|
|
431
|
+
* the pinned directory alone, so it is the round's context search that
|
|
432
|
+
* keeps the retired list (this store's exclusions, and the file the
|
|
433
|
+
* sessions' recall reads) fresh, through this, off its critical path.
|
|
434
|
+
*/
|
|
435
|
+
refreshInBackground() {
|
|
436
|
+
if (this.inFlight && this.inFlight.gen === this.generation)
|
|
437
|
+
return;
|
|
438
|
+
if (this.cache && this.now().getTime() - this.cache.at < this.cacheTtlMs)
|
|
439
|
+
return;
|
|
440
|
+
void this.sweepRun().catch(() => {
|
|
441
|
+
/* total by construction; a sweep that failed is retried next time */
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
async sweepRun() {
|
|
445
|
+
const fresh = this.cache && this.now().getTime() - this.cache.at < this.cacheTtlMs;
|
|
446
|
+
if (fresh && this.cache)
|
|
447
|
+
return this.cache.run;
|
|
448
|
+
// One sweep at a time: the round path and the TUI poll can both land here.
|
|
449
|
+
if (this.inFlight && this.inFlight.gen === this.generation)
|
|
450
|
+
return this.inFlight.run;
|
|
451
|
+
const gen = this.generation;
|
|
452
|
+
const run = this.runSweep(gen).finally(() => {
|
|
453
|
+
if (this.inFlight?.run === run)
|
|
454
|
+
this.inFlight = null;
|
|
455
|
+
});
|
|
456
|
+
this.inFlight = { gen, run };
|
|
457
|
+
return run;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* One sweep, incremental: every category is listed, and a
|
|
461
|
+
* file is read only when its listed size or modification time differs from
|
|
462
|
+
* the read this store already holds (or it has none, or the listing carries
|
|
463
|
+
* no modTime) — except on a periodic FULL refresh (`fullRefreshMs`), which
|
|
464
|
+
* reads everything. A file that fails to read is unread exactly as before
|
|
465
|
+
* and forgotten, so the next sweep tries it again; a category that fails to
|
|
466
|
+
* LIST keeps its files' cached reads for when it lists again, but none of
|
|
467
|
+
* them is served while it does not.
|
|
468
|
+
*/
|
|
469
|
+
async runSweep(gen) {
|
|
470
|
+
this.takeRetiredSeed();
|
|
471
|
+
const seq = (this.sweepSeq += 1);
|
|
472
|
+
const startedAt = this.now().getTime();
|
|
473
|
+
const full = this.fullRefreshMs === 0 ||
|
|
474
|
+
this.lastFullAt === null ||
|
|
475
|
+
startedAt - this.lastFullAt >= this.fullRefreshMs;
|
|
476
|
+
const unread = [];
|
|
477
|
+
let retired = [];
|
|
478
|
+
const listing = await this.listFiles(unread);
|
|
479
|
+
const { files } = listing;
|
|
480
|
+
const prior = this.known;
|
|
481
|
+
const next = new Map();
|
|
482
|
+
let vanished = 0;
|
|
483
|
+
let reread = 0;
|
|
484
|
+
let reused = 0;
|
|
485
|
+
await pool(files, this.concurrency, async (file) => {
|
|
486
|
+
const sig = signatureOf(file);
|
|
487
|
+
const had = prior.get(file.id);
|
|
488
|
+
if (!full && sig !== null && had !== undefined && had.sig === sig) {
|
|
489
|
+
next.set(file.id, had);
|
|
490
|
+
reused += 1;
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
reread += 1;
|
|
494
|
+
const uri = this.uriOf(file.id);
|
|
495
|
+
const res = await this.client.readContent(uri);
|
|
496
|
+
if (!res.ok) {
|
|
497
|
+
if (res.code === "NOT_FOUND")
|
|
498
|
+
vanished += 1;
|
|
499
|
+
else
|
|
500
|
+
unread.push(`read ${uri}: ${res.message}`);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
const entry = entryFrom(file.id, res.result, file.modTime);
|
|
504
|
+
if (entry.text === "") {
|
|
505
|
+
next.set(file.id, { sig, entry: null, retired: false });
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
// Header-only, like `pinned`: a [feedback] entry quoting "#retired"
|
|
509
|
+
// must not vanish from every read. `retire` writes the tag at the head.
|
|
510
|
+
next.set(file.id, {
|
|
511
|
+
sig,
|
|
512
|
+
entry,
|
|
513
|
+
retired: isRetired(entry.text),
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
const entries = [];
|
|
517
|
+
const retiredEntries = [];
|
|
518
|
+
for (const file of files) {
|
|
519
|
+
const known = next.get(file.id);
|
|
520
|
+
if (!known || known.entry === null)
|
|
521
|
+
continue;
|
|
522
|
+
if (known.retired) {
|
|
523
|
+
retired.push(this.uriOf(file.id));
|
|
524
|
+
retiredEntries.push(known.entry);
|
|
525
|
+
}
|
|
526
|
+
else
|
|
527
|
+
entries.push(known.entry);
|
|
528
|
+
}
|
|
529
|
+
// One file gone between the listing and the read is a race (the companion
|
|
530
|
+
// can forget an entry; this store never deletes). EVERY listed file gone
|
|
531
|
+
// is not a race — it is a server whose listing and content disagree — and
|
|
532
|
+
// a head built on it could be missing every standing instruction, so it
|
|
533
|
+
// is unread rather than an exact, empty answer. (Only a sweep that READ
|
|
534
|
+
// every listed file can say that; the hourly full refresh is one.)
|
|
535
|
+
const contradiction = listingContradiction(this.root, files.length, reread, vanished);
|
|
536
|
+
if (contradiction !== null)
|
|
537
|
+
unread.push(contradiction);
|
|
538
|
+
entries.sort(byTsThenId);
|
|
539
|
+
retiredEntries.sort(byTsThenId);
|
|
540
|
+
// A sweep that could not read everything never SHRINKS the retired list:
|
|
541
|
+
// the published list is what every session's recall excludes, and a root
|
|
542
|
+
// or category that failed to list, or a file that failed to read, says
|
|
543
|
+
// nothing about whether its entries are still retired. It keeps what was
|
|
544
|
+
// known (the last list, the cached reads it could not re-check) and adds
|
|
545
|
+
// what it saw; only a complete sweep may drop a URI.
|
|
546
|
+
if (unread.length > 0) {
|
|
547
|
+
const keep = new Set(retired);
|
|
548
|
+
for (const uri of this.knownRetired ?? [])
|
|
549
|
+
keep.add(uri);
|
|
550
|
+
for (const [id, known] of prior)
|
|
551
|
+
if (known.retired && !next.has(id))
|
|
552
|
+
keep.add(this.uriOf(id));
|
|
553
|
+
retired = [...keep];
|
|
554
|
+
}
|
|
555
|
+
retired.sort();
|
|
556
|
+
// The file cache: what this sweep read or kept, plus the cached reads of
|
|
557
|
+
// any category it could not list (kept for when it lists again). A root
|
|
558
|
+
// that could not be listed changes nothing.
|
|
559
|
+
if (listing.rootListed && seq > this.adoptedSeq) {
|
|
560
|
+
for (const [id, known] of prior) {
|
|
561
|
+
if (underFailedDir(id, listing.failedDirs) && !next.has(id))
|
|
562
|
+
next.set(id, known);
|
|
563
|
+
}
|
|
564
|
+
for (const [id, at] of this.forgotten) {
|
|
565
|
+
if (at >= seq)
|
|
566
|
+
next.delete(id);
|
|
567
|
+
else
|
|
568
|
+
this.forgotten.delete(id);
|
|
569
|
+
}
|
|
570
|
+
this.known = next;
|
|
571
|
+
this.adoptedSeq = seq;
|
|
572
|
+
if (full)
|
|
573
|
+
this.lastFullAt = startedAt;
|
|
574
|
+
// Each listed directory counts the files directly in it
|
|
575
|
+
// (`experiences/x.md` is `experiences`'; `preferences/pinned/x.md` is
|
|
576
|
+
// `preferences/pinned`'s, never `preferences`'). A directory whose
|
|
577
|
+
// listing failed keeps its last count, or is unknown without one.
|
|
578
|
+
const counts = new Map();
|
|
579
|
+
for (const file of files) {
|
|
580
|
+
const cut = file.id.lastIndexOf("/");
|
|
581
|
+
if (cut < 0)
|
|
582
|
+
continue;
|
|
583
|
+
const dir = file.id.slice(0, cut);
|
|
584
|
+
counts.set(dir, (counts.get(dir) ?? 0) + 1);
|
|
585
|
+
}
|
|
586
|
+
// A failed listing hides its subdirectories too: they keep their last
|
|
587
|
+
// counts like it, and with none they read unknown (`areaSize` matches
|
|
588
|
+
// an unknown directory by prefix).
|
|
589
|
+
const unknown = new Set();
|
|
590
|
+
const prev = this.listedByArea;
|
|
591
|
+
for (const dir of listing.failedDirs) {
|
|
592
|
+
const under = (d) => d === dir || d.startsWith(`${dir}/`);
|
|
593
|
+
for (const d of [...counts.keys()])
|
|
594
|
+
if (under(d))
|
|
595
|
+
counts.delete(d);
|
|
596
|
+
if (prev)
|
|
597
|
+
for (const [d, n] of prev.counts)
|
|
598
|
+
if (under(d))
|
|
599
|
+
counts.set(d, n);
|
|
600
|
+
if (!counts.has(dir))
|
|
601
|
+
unknown.add(dir);
|
|
602
|
+
if (prev)
|
|
603
|
+
for (const d of prev.unknown)
|
|
604
|
+
if (under(d))
|
|
605
|
+
unknown.add(d);
|
|
606
|
+
}
|
|
607
|
+
this.listedByArea = { counts, unknown };
|
|
608
|
+
}
|
|
609
|
+
const run = { sweep: { entries, unread, retired: retiredEntries }, retired };
|
|
610
|
+
this.stats = { full, listed: files.length, reread, reused };
|
|
611
|
+
if (gen === this.generation) {
|
|
612
|
+
this.cache = { at: this.now().getTime(), run };
|
|
613
|
+
this.setKnownRetired([...retired]);
|
|
614
|
+
}
|
|
615
|
+
return run;
|
|
616
|
+
}
|
|
617
|
+
/** Record the retired list, telling `onRetired` when it differs from the
|
|
618
|
+
* last list it published (and the first time it is known at all). */
|
|
619
|
+
setKnownRetired(next) {
|
|
620
|
+
this.knownRetired = next;
|
|
621
|
+
if (this.onRetired === undefined)
|
|
622
|
+
return;
|
|
623
|
+
const prev = this.publishedRetired;
|
|
624
|
+
if (prev !== null && prev.length === next.length && prev.every((u, i) => u === next[i]))
|
|
625
|
+
return;
|
|
626
|
+
try {
|
|
627
|
+
if (this.onRetired([...next]) !== false)
|
|
628
|
+
this.publishedRetired = next;
|
|
629
|
+
}
|
|
630
|
+
catch {
|
|
631
|
+
/* the publication is best-effort; the store never throws */
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* The memory tree, three levels deep: `<root>/<area>/<file>`,
|
|
636
|
+
* `<root>/<area>/<sub>/<file>` (`preferences/pinned/`) plus any file
|
|
637
|
+
* sitting directly in the root — every area but the ones
|
|
638
|
+
* `SWEEP_EXCLUDED_AREAS` names (`finding-cases/`, the daemon's derived
|
|
639
|
+
* documents, written by `putDocuments` and read through the server's search
|
|
640
|
+
* alone). A bounded depth is the whole convention — a
|
|
641
|
+
* recursive walk against a store this system fully owns would only ever be
|
|
642
|
+
* finding files it did not write.
|
|
643
|
+
*
|
|
644
|
+
* A listing that fails is recorded in `unread` — except NOT_FOUND, which is
|
|
645
|
+
* an absent directory and therefore a known-empty one (see `MemorySweep`).
|
|
646
|
+
*/
|
|
647
|
+
async listFiles(unread) {
|
|
648
|
+
const failedDirs = new Set();
|
|
649
|
+
const top = await this.client.ls(this.root);
|
|
650
|
+
if (!top.ok) {
|
|
651
|
+
if (top.code !== "NOT_FOUND") {
|
|
652
|
+
unread.push(`ls ${this.root}: ${top.message}`);
|
|
653
|
+
return { files: [], rootListed: false, failedDirs };
|
|
654
|
+
}
|
|
655
|
+
return { files: [], rootListed: true, failedDirs };
|
|
656
|
+
}
|
|
657
|
+
const out = [];
|
|
658
|
+
const dirs = [];
|
|
659
|
+
for (const row of top.result) {
|
|
660
|
+
if (row.isDir) {
|
|
661
|
+
// `finding-cases/` is the daemon's derived documents, not curated memory
|
|
662
|
+
// (`SWEEP_EXCLUDED_AREAS`): never listed here.
|
|
663
|
+
if (!row.name.startsWith(".") && !SWEEP_EXCLUDED_AREAS.includes(row.name))
|
|
664
|
+
dirs.push(row.name);
|
|
665
|
+
}
|
|
666
|
+
else if (isTextFile(row.name)) {
|
|
667
|
+
out.push({ id: row.name, size: row.size, modTime: row.modTime });
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
const listDir = async (dir, deeper) => {
|
|
671
|
+
const uri = `${this.root}/${dir}`;
|
|
672
|
+
const sub = await this.client.ls(uri);
|
|
673
|
+
if (!sub.ok) {
|
|
674
|
+
if (sub.code !== "NOT_FOUND") {
|
|
675
|
+
unread.push(`ls ${uri}: ${sub.message}`);
|
|
676
|
+
failedDirs.add(dir);
|
|
677
|
+
}
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
const subdirs = [];
|
|
681
|
+
for (const row of sub.result) {
|
|
682
|
+
if (row.isDir) {
|
|
683
|
+
if (deeper && !row.name.startsWith("."))
|
|
684
|
+
subdirs.push(`${dir}/${row.name}`);
|
|
685
|
+
}
|
|
686
|
+
else if (isTextFile(row.name)) {
|
|
687
|
+
out.push({ id: `${dir}/${row.name}`, size: row.size, modTime: row.modTime });
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
await pool(subdirs, this.concurrency, (d) => listDir(d, false));
|
|
691
|
+
};
|
|
692
|
+
await pool(dirs, this.concurrency, (dir) => listDir(dir, true));
|
|
693
|
+
return { files: out, rootListed: true, failedDirs };
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* The pinned area and nothing else: one listing of
|
|
697
|
+
* `preferences/pinned/` and a read of every file in it — never the rest of
|
|
698
|
+
* the store, never a file from the incremental cache (the head is exact:
|
|
699
|
+
* every byte read now). Cached for `cacheTtlMs` like a sweep, and dropped by
|
|
700
|
+
* `invalidate()`. Total: a dead server is `unread`, never a throw; an absent
|
|
701
|
+
* directory is known-empty (`entries: []`, nothing unread) — a fresh
|
|
702
|
+
* install's healthy answer. Retired entries are left out.
|
|
703
|
+
*/
|
|
704
|
+
async pinnedSweep() {
|
|
705
|
+
const fresh = this.pinnedCache &&
|
|
706
|
+
this.pinnedCache.gen === this.generation &&
|
|
707
|
+
this.now().getTime() - this.pinnedCache.at < this.cacheTtlMs;
|
|
708
|
+
if (fresh && this.pinnedCache)
|
|
709
|
+
return this.pinnedCache.sweep;
|
|
710
|
+
if (this.pinnedInFlight && this.pinnedInFlight.gen === this.generation)
|
|
711
|
+
return this.pinnedInFlight.run;
|
|
712
|
+
const gen = this.generation;
|
|
713
|
+
const run = this.readPinnedArea().then((sweep) => {
|
|
714
|
+
if (gen === this.generation)
|
|
715
|
+
this.pinnedCache = { at: this.now().getTime(), gen, sweep };
|
|
716
|
+
return sweep;
|
|
717
|
+
});
|
|
718
|
+
const tracked = run.finally(() => {
|
|
719
|
+
if (this.pinnedInFlight?.run === tracked)
|
|
720
|
+
this.pinnedInFlight = null;
|
|
721
|
+
});
|
|
722
|
+
this.pinnedInFlight = { gen, run: tracked };
|
|
723
|
+
return tracked;
|
|
724
|
+
}
|
|
725
|
+
async readPinnedArea() {
|
|
726
|
+
const unread = [];
|
|
727
|
+
const dir = `${this.root}/${PINNED_AREA}`;
|
|
728
|
+
try {
|
|
729
|
+
const listed = await this.client.ls(dir);
|
|
730
|
+
if (!listed.ok) {
|
|
731
|
+
if (listed.code === "NOT_FOUND")
|
|
732
|
+
return { entries: [], unread };
|
|
733
|
+
return { entries: [], unread: [`ls ${dir}: ${listed.message}`] };
|
|
734
|
+
}
|
|
735
|
+
const files = listed.result.filter((r) => !r.isDir && isTextFile(r.name));
|
|
736
|
+
const entries = [];
|
|
737
|
+
let vanished = 0;
|
|
738
|
+
await pool(files, this.concurrency, async (file) => {
|
|
739
|
+
const id = `${PINNED_AREA}/${file.name}`;
|
|
740
|
+
const res = await this.client.readContent(this.uriOf(id));
|
|
741
|
+
if (!res.ok) {
|
|
742
|
+
if (res.code === "NOT_FOUND")
|
|
743
|
+
vanished += 1;
|
|
744
|
+
else
|
|
745
|
+
unread.push(`read ${this.uriOf(id)}: ${res.message}`);
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
const entry = entryFrom(id, res.result, file.modTime);
|
|
749
|
+
if (entry.text === "" || isRetired(entry.text))
|
|
750
|
+
return;
|
|
751
|
+
entries.push(entry);
|
|
752
|
+
});
|
|
753
|
+
// Every listed file gone: the listing and the content disagree, and an
|
|
754
|
+
// empty head built on that could be missing every standing instruction.
|
|
755
|
+
const contradiction = listingContradiction(dir, files.length, files.length, vanished);
|
|
756
|
+
if (contradiction !== null)
|
|
757
|
+
unread.push(contradiction);
|
|
758
|
+
entries.sort(byTsThenId);
|
|
759
|
+
return { entries, unread };
|
|
760
|
+
}
|
|
761
|
+
catch (err) {
|
|
762
|
+
return {
|
|
763
|
+
entries: [],
|
|
764
|
+
unread: [`ls ${dir}: ${err instanceof Error ? err.message : String(err)}`],
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Free-text / tag matching over the entries, NOT a vector search.
|
|
770
|
+
*
|
|
771
|
+
* Deliberate: `search()` returns ADDRESSABLE entries — every hit carries the
|
|
772
|
+
* hash a management verb writes back with — and OpenViking's context search
|
|
773
|
+
* returns budgeted, tier-degraded prose (an abstract, an overview) whose
|
|
774
|
+
* bytes are not the stored bytes. Handing those back would mint hashes that
|
|
775
|
+
* 409 on first use. The semantic surface is `OpenVikingClient.searchContext`
|
|
776
|
+
* and it is consumed by the round's context block, where nothing is
|
|
777
|
+
* addressed.
|
|
778
|
+
*/
|
|
779
|
+
async search(query, opts) {
|
|
780
|
+
return searchEntries(await this.read(), query, opts);
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* One mutation, through `content/batch-write` and nothing else.
|
|
784
|
+
*
|
|
785
|
+
* Resolves for every outcome, per the seam's contract: a stale hash is
|
|
786
|
+
* `{ok:false}` with a sentence an operator can act on, and so is a dead
|
|
787
|
+
* server. A caller must never have to tell a refusal from an outage to know
|
|
788
|
+
* whether memory changed — in both cases it did not.
|
|
789
|
+
*/
|
|
790
|
+
async apply(op) {
|
|
791
|
+
try {
|
|
792
|
+
// The owner's own words are standing instructions: `preferences/`.
|
|
793
|
+
if (op.op === "add")
|
|
794
|
+
return await this.create(renderEntryBody(op.text, [MANUAL_TAG]), PREFERENCES_AREA);
|
|
795
|
+
if (op.op === "note") {
|
|
796
|
+
const body = renderEntryBody(op.text);
|
|
797
|
+
return await this.create(body, op.category ?? areaForNewEntry(body, this.category), op.name, op.identity);
|
|
798
|
+
}
|
|
799
|
+
if (!validEntryId(op.id))
|
|
800
|
+
return {
|
|
801
|
+
ok: false,
|
|
802
|
+
message: `refused: '${op.id}' is not a memory address this store can spell`,
|
|
803
|
+
};
|
|
804
|
+
if (!isHashToken(op.expect)) {
|
|
805
|
+
return {
|
|
806
|
+
ok: false,
|
|
807
|
+
message: "refused: this entry was listed without a usable concurrency token — re-read it and retry",
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
const current = await this.client.readContent(this.uriOf(op.id));
|
|
811
|
+
if (!current.ok) {
|
|
812
|
+
return {
|
|
813
|
+
ok: false,
|
|
814
|
+
message: `memory write failed: could not read ${op.id} (${current.code})`,
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
const entry = entryFrom(op.id, current.result, "");
|
|
818
|
+
if (entry.hash !== op.expect) {
|
|
819
|
+
// Caught before the round trip, but the server's 409 is the real
|
|
820
|
+
// guard — this is only a cheaper, clearer version of the same answer.
|
|
821
|
+
// The caller's copy may have come from this store's own file cache,
|
|
822
|
+
// which a same-size rewrite inside one modTime second cannot move:
|
|
823
|
+
// forget the file, so the re-read the refusal asks for is a real one.
|
|
824
|
+
this.forget(op.id);
|
|
825
|
+
return {
|
|
826
|
+
ok: false,
|
|
827
|
+
message: "refused: the entry changed since you read it — re-read it and retry",
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
// Pin and unpin move the entry: into the pinned area, and
|
|
831
|
+
// back to where it came from. On an entry already where the verb would
|
|
832
|
+
// put it, they only fix its tag in place.
|
|
833
|
+
if (op.op === "pin" && !isPinnedId(op.id)) {
|
|
834
|
+
const area = areaOfId(op.id);
|
|
835
|
+
const tagged = area === "" ? entry.text : withHeaderTags(entry.text, { [FROM_TAG]: area });
|
|
836
|
+
return await this.move(op.id, op.expect, pinnedIdFor(op.id), withTag(tagged, PINNED_TAG));
|
|
837
|
+
}
|
|
838
|
+
if (op.op === "unpin" && isPinnedId(op.id)) {
|
|
839
|
+
const to = `${unpinTargetArea(entry.text)}/${nameOfId(op.id)}`;
|
|
840
|
+
const body = renderEntryBody(withoutHeaderTag(withoutTag(entry.text, PINNED_TAG), FROM_TAG));
|
|
841
|
+
return await this.move(op.id, op.expect, to, body);
|
|
842
|
+
}
|
|
843
|
+
const next = this.rewrite(op, entry.text);
|
|
844
|
+
if (next === null)
|
|
845
|
+
return { ok: false, message: `refused: unknown memory op '${op.op}'` };
|
|
846
|
+
if (next.trim() === "")
|
|
847
|
+
return {
|
|
848
|
+
ok: false,
|
|
849
|
+
message: "refused: an edit may not empty an entry — retire it instead",
|
|
850
|
+
};
|
|
851
|
+
const res = await this.replace(op.id, next, op.expect);
|
|
852
|
+
// A retire, or an edit whose new header says `#retired`: the content
|
|
853
|
+
// decides, as it does for every other writer of retired bytes.
|
|
854
|
+
if (res.ok && isRetired(next))
|
|
855
|
+
this.noteRetired(this.uriOf(op.id));
|
|
856
|
+
return res;
|
|
857
|
+
}
|
|
858
|
+
catch (err) {
|
|
859
|
+
// The client is total, but a caller-supplied seam is not, and a write
|
|
860
|
+
// that throws would break the one promise the whole phase rests on.
|
|
861
|
+
return {
|
|
862
|
+
ok: false,
|
|
863
|
+
message: `memory write failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
/** The new body for an addressed op. Null for an op this store cannot spell
|
|
868
|
+
* (the union is closed, so this is belt-and-braces against a newer caller). */
|
|
869
|
+
rewrite(op, text) {
|
|
870
|
+
switch (op.op) {
|
|
871
|
+
case "edit":
|
|
872
|
+
// Verbatim: the caller supplies the
|
|
873
|
+
// whole block, tags included, and gets what it wrote.
|
|
874
|
+
return renderEntryBody(op.text);
|
|
875
|
+
case "pin":
|
|
876
|
+
return withTag(text, PINNED_TAG);
|
|
877
|
+
case "unpin":
|
|
878
|
+
return withoutTag(text, PINNED_TAG);
|
|
879
|
+
case "retire":
|
|
880
|
+
return withTag(text, RETIRED_TAG);
|
|
881
|
+
default:
|
|
882
|
+
return null;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Create a new entry. `create_if_absent` makes a name collision a 409 rather
|
|
887
|
+
* than a silent overwrite of somebody else's memory, so a collision is
|
|
888
|
+
* retried under a fresh name — two writers in the same second is ordinary,
|
|
889
|
+
* losing one of their entries is not.
|
|
890
|
+
*
|
|
891
|
+
* A named create (`note` with `name`) is never renamed: the
|
|
892
|
+
* address IS the writer's idempotency key. The server answers a replay of
|
|
893
|
+
* the same bytes `unchanged` (landed — a plain success), and a 409 there
|
|
894
|
+
* means the address already holds other bytes, which is KEPT and reported
|
|
895
|
+
* `existed`. Only the precondition 409 means that: a BUSY 409 never
|
|
896
|
+
* reaches here as a conflict (the client retries it, then fails it).
|
|
897
|
+
*/
|
|
898
|
+
async create(body, category = this.category, name, identity) {
|
|
899
|
+
if (body.trim() === "")
|
|
900
|
+
return { ok: false, message: "refused: an empty memory entry" };
|
|
901
|
+
if (!validArea(category))
|
|
902
|
+
return {
|
|
903
|
+
ok: false,
|
|
904
|
+
message: `refused: '${category}' is not a memory area this store can spell`,
|
|
905
|
+
};
|
|
906
|
+
if (name !== undefined && !validEntryId(`${category}/${name}.md`))
|
|
907
|
+
return { ok: false, message: `refused: '${name}' is not an entry name this store can spell` };
|
|
908
|
+
if (identity !== undefined)
|
|
909
|
+
return this.createIdentified(body, category, name, identity);
|
|
910
|
+
const dir = `${this.root}/${category}`;
|
|
911
|
+
const ts = tsFromModTime(this.now().toISOString());
|
|
912
|
+
const stamp = stampOf(ts);
|
|
913
|
+
let mkdirTried = false;
|
|
914
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
915
|
+
const suffix = createHash("sha256").update(`${body}#${attempt}`).digest("hex").slice(0, 8);
|
|
916
|
+
const id = name !== undefined ? `${category}/${name}.md` : `${category}/${stamp}-${suffix}.md`;
|
|
917
|
+
const uri = this.uriOf(id);
|
|
918
|
+
const res = await this.client.batchWrite({
|
|
919
|
+
root_uri: dir,
|
|
920
|
+
operations: [{ uri, content: body, precondition: { kind: "create_if_absent" } }],
|
|
921
|
+
// The bytes are durable without waiting; only searchability lags, and
|
|
922
|
+
// ~1.6 core-seconds of index refresh must never sit on a caller.
|
|
923
|
+
wait: false,
|
|
924
|
+
});
|
|
925
|
+
if (res.ok && batchWriteLanded(res.result, uri)) {
|
|
926
|
+
this.forget(id);
|
|
927
|
+
if (isRetired(body))
|
|
928
|
+
this.noteRetired(uri);
|
|
929
|
+
await this.mirrorTags(uri, body);
|
|
930
|
+
const same = res.result.unchanged.includes(uri);
|
|
931
|
+
return {
|
|
932
|
+
ok: true,
|
|
933
|
+
message: same ? `already present: ${id}` : `wrote ${id}`,
|
|
934
|
+
uri,
|
|
935
|
+
...(same ? { unchanged: true } : {}),
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
if (res.ok)
|
|
939
|
+
return {
|
|
940
|
+
ok: false,
|
|
941
|
+
message: `memory write failed: the server accepted the batch without writing ${id}`,
|
|
942
|
+
};
|
|
943
|
+
// A missing area is made here, once, and the loop retries — the
|
|
944
|
+
// alternative is a bootstrap step that a restart or a new category
|
|
945
|
+
// silently outlives. One attempt per call: a failed mkdir falls through
|
|
946
|
+
// to the refusal below.
|
|
947
|
+
if (!mkdirTried && res.code === "NOT_FOUND") {
|
|
948
|
+
mkdirTried = true;
|
|
949
|
+
if (await this.madeMissingArea(res.code, dir))
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
if (!res.conflict)
|
|
953
|
+
return { ok: false, message: `memory write failed: ${res.message}` };
|
|
954
|
+
if (name !== undefined) {
|
|
955
|
+
this.forget(id); // An external writer won; the cached destination may predate it.
|
|
956
|
+
return {
|
|
957
|
+
ok: true,
|
|
958
|
+
existed: true,
|
|
959
|
+
uri,
|
|
960
|
+
message: `kept ${id}: an entry is already there with other bytes (never overwritten)`,
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
this.log(`openviking: memory name ${id} taken — retrying under a fresh name`);
|
|
964
|
+
}
|
|
965
|
+
return { ok: false, message: "refused: could not find a free name for this entry" };
|
|
966
|
+
}
|
|
967
|
+
/** The immutable claim is the global identity, independent of placement.
|
|
968
|
+
* Its target is durable before the note write. Any contender can
|
|
969
|
+
* finish an interrupted claim, always at the same address; moved/retired
|
|
970
|
+
* target bytes are protected by create_if_absent. A hidden bookkeeping area
|
|
971
|
+
* is omitted from curated-memory sweeps. No cross-area batch is required. */
|
|
972
|
+
async createIdentified(body, category, name, identity) {
|
|
973
|
+
if (!name || identity.length === 0 || identity.length > 256)
|
|
974
|
+
return {
|
|
975
|
+
ok: false,
|
|
976
|
+
message: "refused: identified notes require a bounded identity and name",
|
|
977
|
+
};
|
|
978
|
+
const dir = `${this.root}/${EXPERIENCES_AREA}/.import-identities`;
|
|
979
|
+
const uri = `${dir}/${createHash("sha256").update(identity).digest("hex")}.json`;
|
|
980
|
+
const content = JSON.stringify({ identity, category, name });
|
|
981
|
+
let result = await this.client.batchWrite({
|
|
982
|
+
root_uri: dir,
|
|
983
|
+
operations: [{ uri, content, precondition: { kind: "create_if_absent" } }],
|
|
984
|
+
wait: false,
|
|
985
|
+
});
|
|
986
|
+
if (!result.ok && result.code === "NOT_FOUND" && (await this.madeMissingArea(result.code, dir)))
|
|
987
|
+
result = await this.client.batchWrite({
|
|
988
|
+
root_uri: dir,
|
|
989
|
+
operations: [{ uri, content, precondition: { kind: "create_if_absent" } }],
|
|
990
|
+
wait: false,
|
|
991
|
+
});
|
|
992
|
+
if (result.ok && !batchWriteLanded(result.result, uri))
|
|
993
|
+
return { ok: false, message: "memory identity claim was not confirmed" };
|
|
994
|
+
if (!result.ok && !result.conflict)
|
|
995
|
+
return { ok: false, message: `memory identity claim failed: ${result.message}` };
|
|
996
|
+
if (!result.ok) {
|
|
997
|
+
const stored = await this.client.readContent(uri);
|
|
998
|
+
if (!stored.ok)
|
|
999
|
+
return { ok: false, message: `memory identity unread: ${stored.message}` };
|
|
1000
|
+
try {
|
|
1001
|
+
const claim = JSON.parse(stored.result);
|
|
1002
|
+
if (claim["identity"] !== identity ||
|
|
1003
|
+
typeof claim["category"] !== "string" ||
|
|
1004
|
+
typeof claim["name"] !== "string")
|
|
1005
|
+
throw new Error("invalid claim");
|
|
1006
|
+
// create validates the recovered area/name before any subsequent write.
|
|
1007
|
+
return this.create(body, claim["category"], claim["name"]);
|
|
1008
|
+
}
|
|
1009
|
+
catch {
|
|
1010
|
+
return { ok: false, message: "memory identity claim is malformed — kept unchanged" };
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
return this.create(body, category, name);
|
|
1014
|
+
}
|
|
1015
|
+
async replace(id, body, expect) {
|
|
1016
|
+
const uri = this.uriOf(id);
|
|
1017
|
+
const res = await this.client.batchWrite({
|
|
1018
|
+
root_uri: parentOf(uri),
|
|
1019
|
+
operations: [
|
|
1020
|
+
{ uri, content: body, precondition: { kind: "replace_if_hash", base_hash: expect } },
|
|
1021
|
+
],
|
|
1022
|
+
wait: false,
|
|
1023
|
+
});
|
|
1024
|
+
if (res.ok && batchWriteLanded(res.result, uri)) {
|
|
1025
|
+
this.forget(id);
|
|
1026
|
+
await this.mirrorTags(uri, body);
|
|
1027
|
+
return { ok: true, message: `updated ${id}` };
|
|
1028
|
+
}
|
|
1029
|
+
if (res.ok)
|
|
1030
|
+
return {
|
|
1031
|
+
ok: false,
|
|
1032
|
+
message: `memory write failed: the server accepted the batch without writing ${id}`,
|
|
1033
|
+
};
|
|
1034
|
+
if (res.conflict) {
|
|
1035
|
+
// Somebody else wrote it: the cached read is stale too (see apply).
|
|
1036
|
+
this.forget(id);
|
|
1037
|
+
return {
|
|
1038
|
+
ok: false,
|
|
1039
|
+
message: "refused: the entry changed since you read it — re-read it and retry",
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
return { ok: false, message: `memory write failed: ${res.message}` };
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* Move one entry: create `to` holding `body`, then replace
|
|
1046
|
+
* `from` — still the bytes hashed `expect`, or nothing happens — with the
|
|
1047
|
+
* `#moved` stub. Two batch-writes, each under its own directory: the server
|
|
1048
|
+
* locks the root it writes under, and that root must be a directory inside
|
|
1049
|
+
* a memory area, so no one root covers two areas. The new copy goes FIRST,
|
|
1050
|
+
* so the bytes are never only in a stub.
|
|
1051
|
+
*
|
|
1052
|
+
* If the stub PROVABLY did not land (a precondition refusal, or a re-read
|
|
1053
|
+
* that finds the source still holding the bytes we hashed), the new copy is
|
|
1054
|
+
* turned back into a pointer at the source (`movedStub(from)` — which is
|
|
1055
|
+
* exactly the stub it replaced, when an entry was moving back), so a failed
|
|
1056
|
+
* move leaves one live copy and an address a retry may write over again. If
|
|
1057
|
+
* the outcome cannot be proven either way (a timeout, a lost answer, a
|
|
1058
|
+
* source holding other bytes, a re-read that fails), NOTHING is undone: two
|
|
1059
|
+
* live copies are recoverable by hand, and retiring the only live one is
|
|
1060
|
+
* not. (A copy write whose answer was lost is read back the same way —
|
|
1061
|
+
* `createExact`.)
|
|
1062
|
+
*/
|
|
1063
|
+
async move(from, expect, to, body) {
|
|
1064
|
+
if (!validEntryId(to))
|
|
1065
|
+
return {
|
|
1066
|
+
ok: false,
|
|
1067
|
+
message: `refused: '${to}' is not a memory address this store can spell`,
|
|
1068
|
+
};
|
|
1069
|
+
const content = renderEntryBody(body);
|
|
1070
|
+
if (content === "")
|
|
1071
|
+
return { ok: false, message: "refused: a move may not empty an entry — retire it instead" };
|
|
1072
|
+
const toUri = this.uriOf(to);
|
|
1073
|
+
const placed = await this.createExact(toUri, content);
|
|
1074
|
+
if (!placed.ok)
|
|
1075
|
+
return { ok: false, message: placed.message };
|
|
1076
|
+
const fromUri = this.uriOf(from);
|
|
1077
|
+
const stub = movedStub(to);
|
|
1078
|
+
const res = await this.client.batchWrite({
|
|
1079
|
+
root_uri: parentOf(fromUri),
|
|
1080
|
+
operations: [
|
|
1081
|
+
{
|
|
1082
|
+
uri: fromUri,
|
|
1083
|
+
content: stub,
|
|
1084
|
+
precondition: { kind: "replace_if_hash", base_hash: expect },
|
|
1085
|
+
},
|
|
1086
|
+
],
|
|
1087
|
+
wait: false,
|
|
1088
|
+
});
|
|
1089
|
+
// A 200 naming the stub: it landed. A refusal (`provenUnwritten` — a
|
|
1090
|
+
// precondition is checked before the first write): it did not. Anything
|
|
1091
|
+
// else — a timeout, a lost connection, a 5xx, a 200 naming nothing —
|
|
1092
|
+
// proves nothing: the batch may have committed after its answer was lost,
|
|
1093
|
+
// so the source is read back before anything is undone.
|
|
1094
|
+
const outcome = res.ok && batchWriteLanded(res.result, fromUri)
|
|
1095
|
+
? "landed"
|
|
1096
|
+
: !res.ok && provenUnwritten(res)
|
|
1097
|
+
? "absent"
|
|
1098
|
+
: await this.writeOutcome(fromUri, stub, expect);
|
|
1099
|
+
this.forget(from);
|
|
1100
|
+
this.forget(to);
|
|
1101
|
+
const why = res.ok ? `the server accepted the batch without writing ${from}` : res.message;
|
|
1102
|
+
if (outcome === "landed") {
|
|
1103
|
+
this.noteRetired(fromUri);
|
|
1104
|
+
this.noteLive(toUri);
|
|
1105
|
+
await this.mirrorTags(toUri, content);
|
|
1106
|
+
const confirmed = res.ok && batchWriteLanded(res.result, fromUri);
|
|
1107
|
+
return {
|
|
1108
|
+
ok: true,
|
|
1109
|
+
message: confirmed
|
|
1110
|
+
? `moved ${from} → ${to}`
|
|
1111
|
+
: `moved ${from} → ${to} (its answer was lost — ${why}; a re-read confirmed it)`,
|
|
1112
|
+
uri: toUri,
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
if (outcome === "unknown") {
|
|
1116
|
+
this.log(`openviking: a move of ${from} to ${to} has an unknown outcome — both left as found`);
|
|
1117
|
+
return {
|
|
1118
|
+
ok: false,
|
|
1119
|
+
message: `memory write failed: ${why} — the move's outcome is unknown: ${to} holds the new copy ` +
|
|
1120
|
+
`and ${from} could not be confirmed either way, so both are left as they are; read memory ` +
|
|
1121
|
+
`again and retire the copy that should not stay`,
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
const undone = await this.unplaceCopy(toUri, content, from);
|
|
1125
|
+
const left = undone === "landed"
|
|
1126
|
+
? ""
|
|
1127
|
+
: undone === "absent"
|
|
1128
|
+
? ` — and the new copy at ${to} could not be turned back into a pointer: retire it`
|
|
1129
|
+
: ` — the rollback outcome at ${to} is unknown; re-read it before deciding whether to retire a copy`;
|
|
1130
|
+
if (!res.ok && res.conflict)
|
|
1131
|
+
return {
|
|
1132
|
+
ok: false,
|
|
1133
|
+
message: `refused: the entry changed since you read it — nothing moved; re-read it and retry${left}`,
|
|
1134
|
+
};
|
|
1135
|
+
return {
|
|
1136
|
+
ok: false,
|
|
1137
|
+
message: `memory write failed: ${why} — nothing moved${left}`,
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* After a write whose answer proves nothing (a timeout, a lost connection,
|
|
1142
|
+
* a 5xx, a 200 that names nothing): did it land? `landed` when `uri` now
|
|
1143
|
+
* holds exactly `wrote`; `absent` when it still holds the bytes hashed
|
|
1144
|
+
* `before` (`null` = it did not exist, and still does not); `unknown`
|
|
1145
|
+
* otherwise — other bytes, or a read that failed. Never guessed.
|
|
1146
|
+
*/
|
|
1147
|
+
async writeOutcome(uri, wrote, before) {
|
|
1148
|
+
const now = await this.client.readContent(uri);
|
|
1149
|
+
if (!now.ok)
|
|
1150
|
+
return before === null && now.code === "NOT_FOUND" ? "absent" : "unknown";
|
|
1151
|
+
if (now.result === wrote)
|
|
1152
|
+
return "landed";
|
|
1153
|
+
if (before !== null && sha256Prefixed(now.result) === before)
|
|
1154
|
+
return "absent";
|
|
1155
|
+
return "unknown";
|
|
1156
|
+
}
|
|
1157
|
+
/** Create `uri` holding exactly `content` — landed when it was created or
|
|
1158
|
+
* was already there byte for byte (a half-finished earlier move); refused
|
|
1159
|
+
* when it holds other bytes, unless those bytes are a `#moved` stub (an
|
|
1160
|
+
* entry moving back to where it once was, or the pointer a failed move left
|
|
1161
|
+
* behind: the stub is only a pointer, and it is replaced under its own
|
|
1162
|
+
* hash). A write whose answer was lost is read back: landed if the bytes
|
|
1163
|
+
* are there. Makes the area directory once if it is missing. */
|
|
1164
|
+
async createExact(uri, content) {
|
|
1165
|
+
const dir = parentOf(uri);
|
|
1166
|
+
let precondition = { kind: "create_if_absent" };
|
|
1167
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
1168
|
+
const res = await this.client.batchWrite({
|
|
1169
|
+
root_uri: dir,
|
|
1170
|
+
operations: [{ uri, content, precondition }],
|
|
1171
|
+
wait: false,
|
|
1172
|
+
});
|
|
1173
|
+
if (res.ok && batchWriteLanded(res.result, uri))
|
|
1174
|
+
return { ok: true };
|
|
1175
|
+
if (!res.ok &&
|
|
1176
|
+
precondition.kind === "create_if_absent" &&
|
|
1177
|
+
(await this.madeMissingArea(res.code, dir)))
|
|
1178
|
+
continue;
|
|
1179
|
+
if (!res.ok && res.conflict && precondition.kind === "create_if_absent") {
|
|
1180
|
+
const there = await this.client.readContent(uri);
|
|
1181
|
+
if (there.ok && isMovedStub(there.result)) {
|
|
1182
|
+
precondition = { kind: "replace_if_hash", base_hash: sha256Prefixed(there.result) };
|
|
1183
|
+
continue;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
if (!res.ok && res.conflict)
|
|
1187
|
+
return {
|
|
1188
|
+
ok: false,
|
|
1189
|
+
message: `refused: ${uri} already holds another entry — nothing moved`,
|
|
1190
|
+
};
|
|
1191
|
+
if (!res.ok && provenUnwritten(res))
|
|
1192
|
+
return { ok: false, message: `memory write failed: ${res.message}` };
|
|
1193
|
+
// An answer that proves nothing: the copy may have landed anyway.
|
|
1194
|
+
const there = await this.client.readContent(uri);
|
|
1195
|
+
if (there.ok && there.result === content)
|
|
1196
|
+
return { ok: true };
|
|
1197
|
+
return {
|
|
1198
|
+
ok: false,
|
|
1199
|
+
message: `memory write failed: ${res.ok ? `the server accepted the batch without writing ${uri}` : res.message}`,
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
return { ok: false, message: `memory write failed: could not create the area of ${uri}` };
|
|
1203
|
+
}
|
|
1204
|
+
/** Undo a copy this store just wrote at `uri` (`content` is its exact
|
|
1205
|
+
* bytes) for an entry that provably still lives at `from`: the copy becomes
|
|
1206
|
+
* a `#moved` stub pointing back there — retired, so no reader shows it,
|
|
1207
|
+
* and a pointer, so a later move may write over it (a retired COPY would
|
|
1208
|
+
* block every later move into that address for good). An uncertain answer
|
|
1209
|
+
* is read back before its outcome is reported. */
|
|
1210
|
+
async unplaceCopy(uri, content, from) {
|
|
1211
|
+
const res = await this.client.batchWrite({
|
|
1212
|
+
root_uri: parentOf(uri),
|
|
1213
|
+
operations: [
|
|
1214
|
+
{
|
|
1215
|
+
uri,
|
|
1216
|
+
content: movedStub(from),
|
|
1217
|
+
precondition: { kind: "replace_if_hash", base_hash: sha256Prefixed(content) },
|
|
1218
|
+
},
|
|
1219
|
+
],
|
|
1220
|
+
wait: false,
|
|
1221
|
+
});
|
|
1222
|
+
const outcome = res.ok && batchWriteLanded(res.result, uri)
|
|
1223
|
+
? "landed"
|
|
1224
|
+
: !res.ok && provenUnwritten(res)
|
|
1225
|
+
? "absent"
|
|
1226
|
+
: await this.writeOutcome(uri, movedStub(from), sha256Prefixed(content));
|
|
1227
|
+
if (outcome === "landed") {
|
|
1228
|
+
this.noteRetired(uri);
|
|
1229
|
+
return "landed";
|
|
1230
|
+
}
|
|
1231
|
+
this.log(`openviking: rollback at ${uri} is ${outcome}`);
|
|
1232
|
+
return outcome;
|
|
1233
|
+
}
|
|
1234
|
+
/** A confirmed revival removes its former stub from recall exclusions. */
|
|
1235
|
+
noteLive(uri) {
|
|
1236
|
+
this.takeRetiredSeed();
|
|
1237
|
+
if (this.knownRetired?.includes(uri))
|
|
1238
|
+
this.setKnownRetired(this.knownRetired.filter((retired) => retired !== uri));
|
|
1239
|
+
}
|
|
1240
|
+
/** A retirement this store wrote joins the published list at once. */
|
|
1241
|
+
noteRetired(uri) {
|
|
1242
|
+
this.takeRetiredSeed();
|
|
1243
|
+
if (this.knownRetired !== null && !this.knownRetired.includes(uri))
|
|
1244
|
+
this.setKnownRetired([...this.knownRetired, uri].sort());
|
|
1245
|
+
}
|
|
1246
|
+
/**
|
|
1247
|
+
* Many moves and in-place rewrites at once — the relayout's executor
|
|
1248
|
+
* (`qcr memory relayout --apply`), and nothing else's.
|
|
1249
|
+
*
|
|
1250
|
+
* Every write is a batch-write with a precondition, ONE per directory root
|
|
1251
|
+
* (chunked at the server's 256 operations): first every new copy
|
|
1252
|
+
* (`create_if_absent`, grouped by destination), then every source — a
|
|
1253
|
+
* `#moved` stub for a moved entry, the new bytes for one rewritten in place
|
|
1254
|
+
* — under `replace_if_hash` of the bytes the plan read (grouped by where it
|
|
1255
|
+
* sits). A source is touched only when its new copy landed. A batch the
|
|
1256
|
+
* server refuses writes nothing (its preconditions are checked before the
|
|
1257
|
+
* first write); the new copies of moves whose source batch was refused are
|
|
1258
|
+
* turned back into pointers at their sources (`movedStub(source)`), so a
|
|
1259
|
+
* refused batch leaves one live copy each and a re-run may move them again.
|
|
1260
|
+
* A batch whose answer proves nothing (a timeout, a lost connection, a
|
|
1261
|
+
* 5xx) may have committed anyway, so each of its items is READ BACK: a
|
|
1262
|
+
* landed write is a success, a provably absent one is handled as refused,
|
|
1263
|
+
* and one that cannot be proven either way is left exactly as found and
|
|
1264
|
+
* reported — the only live copy of an entry is never undone on a guess.
|
|
1265
|
+
* Nothing is ever deleted. Total: every outcome is reported per item, never
|
|
1266
|
+
* thrown.
|
|
1267
|
+
*/
|
|
1268
|
+
async relocate(items) {
|
|
1269
|
+
const results = new Map();
|
|
1270
|
+
const batches = [];
|
|
1271
|
+
const ok = (it, message) => {
|
|
1272
|
+
results.set(it.id, { id: it.id, ...(it.to ? { to: it.to } : {}), ok: true, message });
|
|
1273
|
+
};
|
|
1274
|
+
const fail = (it, message) => {
|
|
1275
|
+
results.set(it.id, { id: it.id, ...(it.to ? { to: it.to } : {}), ok: false, message });
|
|
1276
|
+
};
|
|
1277
|
+
const valid = [];
|
|
1278
|
+
const seen = new Set();
|
|
1279
|
+
for (const it of items) {
|
|
1280
|
+
if (!validEntryId(it.id) || (it.to !== undefined && !validEntryId(it.to)))
|
|
1281
|
+
fail(it, "refused: not a memory address this store can spell");
|
|
1282
|
+
// The server takes no batch rooted at the memories root itself, so an
|
|
1283
|
+
// entry sitting there can be neither stubbed nor rewritten: refused
|
|
1284
|
+
// before its copy is written, never written and then undone.
|
|
1285
|
+
else if (areaOfId(it.id) === "" || (it.to !== undefined && areaOfId(it.to) === ""))
|
|
1286
|
+
fail(it, "refused: the entry sits directly in the memories root, where the server writes nothing — move it by hand");
|
|
1287
|
+
else if (!isHashToken(it.expect))
|
|
1288
|
+
fail(it, "refused: no usable concurrency token — re-plan");
|
|
1289
|
+
else if (renderEntryBody(it.text) === "")
|
|
1290
|
+
fail(it, "refused: an entry may not be emptied");
|
|
1291
|
+
else if (seen.has(it.id) || (it.to !== undefined && seen.has(it.to)))
|
|
1292
|
+
fail(it, "refused: the plan names this address twice");
|
|
1293
|
+
else {
|
|
1294
|
+
seen.add(it.id);
|
|
1295
|
+
if (it.to !== undefined)
|
|
1296
|
+
seen.add(it.to);
|
|
1297
|
+
valid.push(it);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
const content = (it) => renderEntryBody(it.text);
|
|
1301
|
+
// Phase 1 — every new copy, one batch per destination directory.
|
|
1302
|
+
const moves = valid.filter((it) => it.to !== undefined);
|
|
1303
|
+
const placed = new Set();
|
|
1304
|
+
for (const [root, group] of groupBy(moves, (it) => parentOf(this.uriOf(it.to)))) {
|
|
1305
|
+
for (const chunk of chunks(group, BATCH_MAX_OPERATIONS)) {
|
|
1306
|
+
const out = await this.batchWithArea(root, chunk.map((it) => ({
|
|
1307
|
+
uri: this.uriOf(it.to),
|
|
1308
|
+
content: content(it),
|
|
1309
|
+
precondition: it.toExpect !== undefined
|
|
1310
|
+
? { kind: "replace_if_hash", base_hash: it.toExpect }
|
|
1311
|
+
: { kind: "create_if_absent" },
|
|
1312
|
+
})));
|
|
1313
|
+
batches.push({ root, ops: chunk.length, ok: out.ok, message: out.message });
|
|
1314
|
+
const unsure = [];
|
|
1315
|
+
for (const it of chunk) {
|
|
1316
|
+
if (out.landed.has(this.uriOf(it.to)))
|
|
1317
|
+
placed.add(it.id);
|
|
1318
|
+
else if (out.refused)
|
|
1319
|
+
fail(it, out.message);
|
|
1320
|
+
else
|
|
1321
|
+
unsure.push(it);
|
|
1322
|
+
}
|
|
1323
|
+
// An answer that proves nothing: a copy that is there byte for byte
|
|
1324
|
+
// landed; any other copy is not placed, and its source is not touched.
|
|
1325
|
+
await pool(unsure, this.concurrency, async (it) => {
|
|
1326
|
+
const there = await this.client.readContent(this.uriOf(it.to));
|
|
1327
|
+
if (there.ok && there.result === content(it))
|
|
1328
|
+
placed.add(it.id);
|
|
1329
|
+
else
|
|
1330
|
+
fail(it, out.ok ? `the server did not write ${it.to}` : out.message);
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
// Phase 2 — every source: a stub after a landed move, or its rewrite.
|
|
1335
|
+
// ONLY a move whose new copy landed: a stub over a source whose copy is
|
|
1336
|
+
// not there would leave the entry's bytes nowhere live.
|
|
1337
|
+
const sources = valid.filter((it) => it.to === undefined || placed.has(it.id));
|
|
1338
|
+
const unplace = [];
|
|
1339
|
+
for (const [root, group] of groupBy(sources, (it) => parentOf(this.uriOf(it.id)))) {
|
|
1340
|
+
for (const chunk of chunks(group, BATCH_MAX_OPERATIONS)) {
|
|
1341
|
+
const wrote = (it) => it.to !== undefined ? movedStub(it.to) : content(it);
|
|
1342
|
+
const out = await this.batchWithArea(root, chunk.map((it) => ({
|
|
1343
|
+
uri: this.uriOf(it.id),
|
|
1344
|
+
content: wrote(it),
|
|
1345
|
+
precondition: { kind: "replace_if_hash", base_hash: it.expect },
|
|
1346
|
+
})));
|
|
1347
|
+
batches.push({ root, ops: chunk.length, ok: out.ok, message: out.message });
|
|
1348
|
+
const done = (it, how = "") => ok(it, `${it.to !== undefined ? `moved → ${it.to}` : "rewritten in place"}${how}`);
|
|
1349
|
+
const unsure = [];
|
|
1350
|
+
for (const it of chunk) {
|
|
1351
|
+
if (out.landed.has(this.uriOf(it.id)))
|
|
1352
|
+
done(it);
|
|
1353
|
+
else if (out.refused) {
|
|
1354
|
+
fail(it, out.message);
|
|
1355
|
+
if (it.to !== undefined)
|
|
1356
|
+
unplace.push(it);
|
|
1357
|
+
}
|
|
1358
|
+
else
|
|
1359
|
+
unsure.push(it);
|
|
1360
|
+
}
|
|
1361
|
+
// The batch may have committed after its answer was lost: read each
|
|
1362
|
+
// source back. Undo a copy only when its source PROVABLY still holds
|
|
1363
|
+
// the bytes the plan read; never on a guess.
|
|
1364
|
+
await pool(unsure, this.concurrency, async (it) => {
|
|
1365
|
+
const why = out.ok ? `the server did not write ${it.id}` : out.message;
|
|
1366
|
+
const outcome = await this.writeOutcome(this.uriOf(it.id), wrote(it), it.expect);
|
|
1367
|
+
if (outcome === "landed")
|
|
1368
|
+
done(it, ` (its answer was lost — ${why}; a re-read confirmed it)`);
|
|
1369
|
+
else if (outcome === "absent") {
|
|
1370
|
+
fail(it, `${why} — nothing written`);
|
|
1371
|
+
if (it.to !== undefined)
|
|
1372
|
+
unplace.push(it);
|
|
1373
|
+
}
|
|
1374
|
+
else
|
|
1375
|
+
fail(it, it.to !== undefined
|
|
1376
|
+
? `${why} — the outcome is unknown: ${it.to} holds the new copy and ${it.id} could not be confirmed either way, so both are left as they are; read memory again and retire the copy that should not stay`
|
|
1377
|
+
: `${why} — the outcome is unknown: ${it.id} could not be confirmed either way; plan again`);
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
// Phase 3 — a move whose source write provably did not land: its new
|
|
1382
|
+
// copy becomes a `#moved` pointer back at the source (retired, so no
|
|
1383
|
+
// reader shows it; a pointer, so a re-run may write over it), and the
|
|
1384
|
+
// source stays the one live copy.
|
|
1385
|
+
for (const [root, group] of groupBy(unplace, (it) => parentOf(this.uriOf(it.to)))) {
|
|
1386
|
+
for (const chunk of chunks(group, BATCH_MAX_OPERATIONS)) {
|
|
1387
|
+
const out = await this.batchWithArea(root, chunk.map((it) => ({
|
|
1388
|
+
uri: this.uriOf(it.to),
|
|
1389
|
+
content: movedStub(it.id),
|
|
1390
|
+
precondition: {
|
|
1391
|
+
kind: "replace_if_hash",
|
|
1392
|
+
base_hash: sha256Prefixed(content(it)),
|
|
1393
|
+
},
|
|
1394
|
+
})));
|
|
1395
|
+
batches.push({ root, ops: chunk.length, ok: out.ok, message: `undo: ${out.message}` });
|
|
1396
|
+
for (const uri of out.landed)
|
|
1397
|
+
this.noteRetired(uri);
|
|
1398
|
+
await pool(chunk, this.concurrency, async (it) => {
|
|
1399
|
+
if (out.landed.has(this.uriOf(it.to)))
|
|
1400
|
+
return;
|
|
1401
|
+
const uri = this.uriOf(it.to);
|
|
1402
|
+
const outcome = out.refused
|
|
1403
|
+
? "absent"
|
|
1404
|
+
: await this.writeOutcome(uri, movedStub(it.id), sha256Prefixed(content(it)));
|
|
1405
|
+
if (outcome === "landed") {
|
|
1406
|
+
this.noteRetired(uri);
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
const prior = results.get(it.id);
|
|
1410
|
+
fail(it, outcome === "absent"
|
|
1411
|
+
? `${prior?.message ?? "refused"} — and the new copy at ${it.to} could not be turned back into a pointer: retire it`
|
|
1412
|
+
: `${prior?.message ?? "refused"} — the rollback outcome at ${it.to} is unknown; re-read it before deciding whether to retire a copy`);
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
// Bookkeeping: what this store wrote is re-read, and what it retired is
|
|
1417
|
+
// published at once.
|
|
1418
|
+
this.invalidate();
|
|
1419
|
+
for (const it of valid) {
|
|
1420
|
+
this.forget(it.id);
|
|
1421
|
+
if (it.to !== undefined)
|
|
1422
|
+
this.forget(it.to);
|
|
1423
|
+
}
|
|
1424
|
+
for (const it of valid) {
|
|
1425
|
+
const r = results.get(it.id);
|
|
1426
|
+
if (!r?.ok)
|
|
1427
|
+
continue;
|
|
1428
|
+
if (it.to !== undefined) {
|
|
1429
|
+
this.noteRetired(this.uriOf(it.id));
|
|
1430
|
+
this.noteLive(this.uriOf(it.to));
|
|
1431
|
+
await this.mirrorTags(this.uriOf(it.to), content(it));
|
|
1432
|
+
}
|
|
1433
|
+
else if (isRetired(content(it))) {
|
|
1434
|
+
this.noteRetired(this.uriOf(it.id));
|
|
1435
|
+
}
|
|
1436
|
+
else {
|
|
1437
|
+
await this.mirrorTags(this.uriOf(it.id), content(it));
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
return {
|
|
1441
|
+
results: items.map((it) => results.get(it.id) ?? {
|
|
1442
|
+
id: it.id,
|
|
1443
|
+
...(it.to ? { to: it.to } : {}),
|
|
1444
|
+
ok: false,
|
|
1445
|
+
message: "not attempted",
|
|
1446
|
+
}),
|
|
1447
|
+
batches,
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* Many named DOCUMENTS, each created or replaced under its own
|
|
1452
|
+
* precondition (the daemon's finding cases; the seam's `putDocuments`). ONE batch-write per area directory (chunked at the
|
|
1453
|
+
* server's 256), so writing twenty cases is one write, never twenty.
|
|
1454
|
+
*
|
|
1455
|
+
* A batch is all-or-nothing at its preconditions (the server checks every
|
|
1456
|
+
* one before the first write), so one document somebody else changed would
|
|
1457
|
+
* hold back the rest. When a batch is refused on a precondition, each of
|
|
1458
|
+
* its documents is read back: one already holding its bytes is `written`;
|
|
1459
|
+
* one still holding what its writer expected (absent, for a create) goes
|
|
1460
|
+
* into ONE more batch; anything else is a `conflict` carrying the bytes
|
|
1461
|
+
* found — never overwritten. A batch whose answer proves nothing (a
|
|
1462
|
+
* timeout, a 5xx) is read back too: a document holding its bytes is
|
|
1463
|
+
* `written`, any other `failed`, for the writer's next attempt. Never a
|
|
1464
|
+
* delete; total — every outcome is a result, never a throw.
|
|
1465
|
+
*/
|
|
1466
|
+
async putDocuments(docs) {
|
|
1467
|
+
const results = new Map();
|
|
1468
|
+
const items = [];
|
|
1469
|
+
const seen = new Set();
|
|
1470
|
+
docs.forEach((doc, i) => {
|
|
1471
|
+
const id = `${doc.area}/${doc.name}.md`;
|
|
1472
|
+
const content = renderEntryBody(doc.text);
|
|
1473
|
+
const refuse = (message) => {
|
|
1474
|
+
results.set(i, { area: doc.area, name: doc.name, state: "failed", message });
|
|
1475
|
+
};
|
|
1476
|
+
if (!validArea(doc.area) || !validEntryId(id))
|
|
1477
|
+
refuse(`refused: '${id}' is not a memory address this store can spell`);
|
|
1478
|
+
else if (doc.expect !== null && !isHashToken(doc.expect))
|
|
1479
|
+
refuse("refused: no usable concurrency token for the replace");
|
|
1480
|
+
else if (content === "")
|
|
1481
|
+
refuse("refused: an empty document");
|
|
1482
|
+
else if (seen.has(id))
|
|
1483
|
+
refuse("refused: one call names this document twice");
|
|
1484
|
+
else {
|
|
1485
|
+
seen.add(id);
|
|
1486
|
+
items.push({
|
|
1487
|
+
i,
|
|
1488
|
+
area: doc.area,
|
|
1489
|
+
name: doc.name,
|
|
1490
|
+
id,
|
|
1491
|
+
uri: this.uriOf(id),
|
|
1492
|
+
content,
|
|
1493
|
+
expect: doc.expect,
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
});
|
|
1497
|
+
const settle = (it, r) => {
|
|
1498
|
+
results.set(it.i, { area: it.area, name: it.name, ...r });
|
|
1499
|
+
};
|
|
1500
|
+
const written = (it, how) => settle(it, {
|
|
1501
|
+
state: "written",
|
|
1502
|
+
hash: sha256Prefixed(it.content),
|
|
1503
|
+
uri: it.uri,
|
|
1504
|
+
message: `${how} ${it.id}`,
|
|
1505
|
+
});
|
|
1506
|
+
/** One batch; returns the documents a precondition refusal held back
|
|
1507
|
+
* that still hold what their writer expected (retried once). */
|
|
1508
|
+
const send = async (root, group, retried) => {
|
|
1509
|
+
const out = await this.batchWithArea(root, group.map((it) => ({
|
|
1510
|
+
uri: it.uri,
|
|
1511
|
+
content: it.content,
|
|
1512
|
+
precondition: it.expect === null
|
|
1513
|
+
? { kind: "create_if_absent" }
|
|
1514
|
+
: { kind: "replace_if_hash", base_hash: it.expect },
|
|
1515
|
+
})));
|
|
1516
|
+
if (out.ok) {
|
|
1517
|
+
for (const it of group) {
|
|
1518
|
+
if (out.landed.has(it.uri))
|
|
1519
|
+
written(it, "wrote");
|
|
1520
|
+
else
|
|
1521
|
+
settle(it, {
|
|
1522
|
+
state: "failed",
|
|
1523
|
+
message: `memory write failed: the server accepted the batch without writing ${it.id}`,
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
return [];
|
|
1527
|
+
}
|
|
1528
|
+
if (out.refused && !out.conflict) {
|
|
1529
|
+
// Busy, auth, a bad request: provably nothing written, and nothing
|
|
1530
|
+
// a read-back would change.
|
|
1531
|
+
for (const it of group)
|
|
1532
|
+
settle(it, { state: "failed", message: out.message });
|
|
1533
|
+
return [];
|
|
1534
|
+
}
|
|
1535
|
+
// A precondition refusal, or an answer that proves nothing: read back.
|
|
1536
|
+
const again = [];
|
|
1537
|
+
await pool(group, this.concurrency, async (it) => {
|
|
1538
|
+
const now = await this.client.readContent(it.uri);
|
|
1539
|
+
if (now.ok && now.result === it.content) {
|
|
1540
|
+
written(it, "already holding its bytes:");
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
if (!now.ok && now.code !== "NOT_FOUND") {
|
|
1544
|
+
settle(it, {
|
|
1545
|
+
state: "failed",
|
|
1546
|
+
message: `${out.message} — and ${it.id} could not be read back (${now.message}); the next attempt retries`,
|
|
1547
|
+
});
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
if (!out.conflict) {
|
|
1551
|
+
settle(it, { state: "failed", message: `${out.message} — ${it.id} was not written` });
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1554
|
+
const held = now.ok ? now.result : null;
|
|
1555
|
+
const asExpected = it.expect === null ? held === null : held !== null && sha256Prefixed(held) === it.expect;
|
|
1556
|
+
if (asExpected && !retried)
|
|
1557
|
+
again.push(it);
|
|
1558
|
+
else if (asExpected)
|
|
1559
|
+
settle(it, {
|
|
1560
|
+
state: "failed",
|
|
1561
|
+
message: `refused: ${it.id} changed while it was being written — the next attempt retries`,
|
|
1562
|
+
});
|
|
1563
|
+
else
|
|
1564
|
+
settle(it, {
|
|
1565
|
+
state: "conflict",
|
|
1566
|
+
found: held,
|
|
1567
|
+
...(held === null ? {} : { hash: sha256Prefixed(held) }),
|
|
1568
|
+
uri: it.uri,
|
|
1569
|
+
message: held === null
|
|
1570
|
+
? `refused: ${it.id} is not there any more — nothing written`
|
|
1571
|
+
: `refused: ${it.id} holds other bytes than its writer last wrote — never overwritten`,
|
|
1572
|
+
});
|
|
1573
|
+
});
|
|
1574
|
+
return again;
|
|
1575
|
+
};
|
|
1576
|
+
for (const [root, group] of groupBy(items, (it) => parentOf(it.uri))) {
|
|
1577
|
+
for (const chunk of chunks(group, BATCH_MAX_OPERATIONS)) {
|
|
1578
|
+
const again = await send(root, chunk, false);
|
|
1579
|
+
if (again.length > 0)
|
|
1580
|
+
await send(root, again, true);
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
// Bookkeeping. An area the sweep never lists (`finding-cases/`) has nothing in
|
|
1584
|
+
// this store's caches, and its retirements are not on the sweep's list.
|
|
1585
|
+
for (const it of items) {
|
|
1586
|
+
if (results.get(it.i)?.state !== "written")
|
|
1587
|
+
continue;
|
|
1588
|
+
if (!SWEEP_EXCLUDED_AREAS.includes(it.area)) {
|
|
1589
|
+
this.forget(it.id);
|
|
1590
|
+
if (isRetired(it.content))
|
|
1591
|
+
this.noteRetired(it.uri);
|
|
1592
|
+
}
|
|
1593
|
+
await this.mirrorTags(it.uri, it.content);
|
|
1594
|
+
}
|
|
1595
|
+
return docs.map((doc, i) => results.get(i) ?? {
|
|
1596
|
+
area: doc.area,
|
|
1597
|
+
name: doc.name,
|
|
1598
|
+
state: "failed",
|
|
1599
|
+
message: "not attempted",
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
/** One batch-write under `root`, making the directory once if it is
|
|
1603
|
+
* missing. `landed` = the URIs the server wrote or already held byte for
|
|
1604
|
+
* byte. `refused` = PROVEN that nothing in the batch was written: a
|
|
1605
|
+
* precondition refusal (checked before the first write) or a root that does
|
|
1606
|
+
* not exist. Any other failure proves nothing — the batch may have
|
|
1607
|
+
* committed after its answer was lost — and the caller reads back. */
|
|
1608
|
+
async batchWithArea(root, operations) {
|
|
1609
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1610
|
+
const res = await this.client.batchWrite({ root_uri: root, operations, wait: false });
|
|
1611
|
+
if (res.ok) {
|
|
1612
|
+
const landed = new Set(operations.map((o) => o.uri).filter((uri) => batchWriteLanded(res.result, uri)));
|
|
1613
|
+
return {
|
|
1614
|
+
ok: true,
|
|
1615
|
+
refused: false,
|
|
1616
|
+
conflict: false,
|
|
1617
|
+
message: `${landed.size}/${operations.length} written under ${root}`,
|
|
1618
|
+
landed,
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
if (attempt === 0 && (await this.madeMissingArea(res.code, root)))
|
|
1622
|
+
continue;
|
|
1623
|
+
return {
|
|
1624
|
+
ok: false,
|
|
1625
|
+
refused: provenUnwritten(res),
|
|
1626
|
+
conflict: res.conflict,
|
|
1627
|
+
message: res.conflict
|
|
1628
|
+
? `refused under ${root}: an entry changed since the plan read it, or a destination already holds other bytes — nothing in this batch was written; plan again and re-run`
|
|
1629
|
+
: `memory write failed under ${root}: ${res.message}`,
|
|
1630
|
+
landed: new Set(),
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
return {
|
|
1634
|
+
ok: false,
|
|
1635
|
+
refused: true,
|
|
1636
|
+
conflict: false,
|
|
1637
|
+
message: `memory write failed: could not create ${root}`,
|
|
1638
|
+
landed: new Set(),
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
/**
|
|
1642
|
+
* Drop the sweep cache AND this file's cached read: the next sweep reads it
|
|
1643
|
+
* whatever its listing says — and a sweep already in flight, which may have
|
|
1644
|
+
* read the old bytes, cannot put them back. After a write through this store,
|
|
1645
|
+
* and after a refusal that proved the cached bytes stale.
|
|
1646
|
+
*/
|
|
1647
|
+
forget(id) {
|
|
1648
|
+
this.invalidate();
|
|
1649
|
+
this.known.delete(id);
|
|
1650
|
+
this.forgotten.set(id, this.sweepSeq);
|
|
1651
|
+
}
|
|
1652
|
+
/**
|
|
1653
|
+
* Mirror the entry's EVENT TAGS onto OpenViking's native retrieval tag list,
|
|
1654
|
+
* so a `tags`-filtered search finds it server-side.
|
|
1655
|
+
*
|
|
1656
|
+
* The vocabulary and the extraction are `src/memory/eventtags.ts`'s
|
|
1657
|
+
* (`parseEventTags` — header-only, so quoted human text cannot mint a tag);
|
|
1658
|
+
* this store only carries them across. BEST-EFFORT by contract: the tags are
|
|
1659
|
+
* already inside the bytes just committed and every reader re-parses them
|
|
1660
|
+
* from there, so a failure here narrows retrieval — it never loses the
|
|
1661
|
+
* memory or its tags.
|
|
1662
|
+
*/
|
|
1663
|
+
async mirrorTags(uri, body) {
|
|
1664
|
+
const tags = formatEventTags(parseEventTags(body))
|
|
1665
|
+
.split(" ")
|
|
1666
|
+
.filter((t) => t !== "");
|
|
1667
|
+
const ok = await this.client.setTags(uri, tags);
|
|
1668
|
+
if (!ok)
|
|
1669
|
+
this.log(`openviking: tags not mirrored for ${uri} — the entry is stored, retrieval by tag may miss it`);
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
// ---- helpers ----------------------------------------------------------------
|
|
1673
|
+
/** Does a failed batch-write PROVE that nothing was written? Yes when the
|
|
1674
|
+
* server answered and refused — a 4xx other than 408: a precondition
|
|
1675
|
+
* (checked before the first write), a busy lock, a missing root, a bad
|
|
1676
|
+
* request, auth. No answer at all (status 0: a timeout, a lost connection), a
|
|
1677
|
+
* 408 (a timeout the server reports) and a 5xx prove nothing: the batch may
|
|
1678
|
+
* have committed after its answer was lost. */
|
|
1679
|
+
export function provenUnwritten(res) {
|
|
1680
|
+
return res.conflict || res.busy || (res.status >= 400 && res.status < 500 && res.status !== 408);
|
|
1681
|
+
}
|
|
1682
|
+
/** The `unread` line for a listing whose every file answered NOT_FOUND when
|
|
1683
|
+
* read — the listing and the content disagree, which is no race — or null.
|
|
1684
|
+
* Only a read of EVERY listed file can say so: `read` is how many were read
|
|
1685
|
+
* now (an incremental sweep reuses the unchanged ones). */
|
|
1686
|
+
function listingContradiction(where, listed, read, vanished) {
|
|
1687
|
+
if (listed === 0 || read !== listed || vanished !== listed)
|
|
1688
|
+
return null;
|
|
1689
|
+
return `read ${where}: all ${listed} listed files answered NOT_FOUND — the listing and the content disagree`;
|
|
1690
|
+
}
|
|
1691
|
+
/** Oldest first; the id breaks a tie so the order never depends on listing
|
|
1692
|
+
* order. */
|
|
1693
|
+
function byTsThenId(a, b) {
|
|
1694
|
+
return a.ts !== b.ts ? (a.ts < b.ts ? -1 : 1) : a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
1695
|
+
}
|
|
1696
|
+
/** Group in first-seen order. */
|
|
1697
|
+
function groupBy(items, key) {
|
|
1698
|
+
const out = new Map();
|
|
1699
|
+
for (const it of items) {
|
|
1700
|
+
const k = key(it);
|
|
1701
|
+
const group = out.get(k);
|
|
1702
|
+
if (group)
|
|
1703
|
+
group.push(it);
|
|
1704
|
+
else
|
|
1705
|
+
out.set(k, [it]);
|
|
1706
|
+
}
|
|
1707
|
+
return out;
|
|
1708
|
+
}
|
|
1709
|
+
function chunks(items, size) {
|
|
1710
|
+
const out = [];
|
|
1711
|
+
for (let i = 0; i < items.length; i += size)
|
|
1712
|
+
out.push(items.slice(i, i + size));
|
|
1713
|
+
return out;
|
|
1714
|
+
}
|
|
1715
|
+
/** Is `id` inside a directory whose listing failed (the area itself or one
|
|
1716
|
+
* above it)? */
|
|
1717
|
+
function underFailedDir(id, failed) {
|
|
1718
|
+
const area = areaOfId(id);
|
|
1719
|
+
for (const dir of failed)
|
|
1720
|
+
if (area === dir || area.startsWith(`${dir}/`))
|
|
1721
|
+
return true;
|
|
1722
|
+
return false;
|
|
1723
|
+
}
|
|
1724
|
+
function isTextFile(name) {
|
|
1725
|
+
return (name.endsWith(".md") || name.endsWith(".txt")) && !name.startsWith(".");
|
|
1726
|
+
}
|
|
1727
|
+
function parentOf(uri) {
|
|
1728
|
+
const cleaned = uri.replace(/\/+$/, "");
|
|
1729
|
+
const cut = cleaned.lastIndexOf("/");
|
|
1730
|
+
return cut > 0 ? cleaned.slice(0, cut) : cleaned;
|
|
1731
|
+
}
|
|
1732
|
+
/** Run `fn` over `items` with at most `limit` in flight. Rejections are
|
|
1733
|
+
* swallowed by the callers above (each `fn` is already total); the pool exists
|
|
1734
|
+
* to bound the burst against a 2-core server, not to collect results. */
|
|
1735
|
+
async function pool(items, limit, fn) {
|
|
1736
|
+
let next = 0;
|
|
1737
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
1738
|
+
for (;;) {
|
|
1739
|
+
const index = next;
|
|
1740
|
+
next += 1;
|
|
1741
|
+
const item = items[index];
|
|
1742
|
+
if (item === undefined)
|
|
1743
|
+
return;
|
|
1744
|
+
try {
|
|
1745
|
+
await fn(item);
|
|
1746
|
+
}
|
|
1747
|
+
catch {
|
|
1748
|
+
/* one bad file never loses the sweep */
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
});
|
|
1752
|
+
await Promise.all(workers);
|
|
1753
|
+
}
|
|
1754
|
+
/** Convenience for wiring: the store the daemon and the TUI both use. */
|
|
1755
|
+
export function openVikingStore(client, opts = {}) {
|
|
1756
|
+
return new OpenVikingStore({ client, ...opts });
|
|
1757
|
+
}
|
|
1758
|
+
//# sourceMappingURL=store.js.map
|