@quickchatai/quickreview 1.2.0 → 1.3.2
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/README.md +116 -53
- package/bin/qcr.js +1 -1
- package/config.example.toml +25 -7
- package/deploy/ansible/README.md +20 -12
- package/deploy/ansible/inventory.example.ini +1 -1
- package/deploy/collect-secrets.sh +1 -1
- package/deploy/qcr-update.sh +1 -1
- package/dist/extensions/cli/home.js +14 -0
- package/dist/extensions/cli/home.js.map +1 -0
- package/dist/extensions/cli/selfimprove.js +18 -0
- package/dist/extensions/cli/selfimprove.js.map +1 -0
- package/dist/extensions/home.js +350 -0
- package/dist/extensions/home.js.map +1 -0
- package/dist/extensions/login-route.js +1 -1
- package/dist/extensions/login-route.js.map +1 -1
- package/dist/extensions/onboarding.js +8 -7
- package/dist/extensions/onboarding.js.map +1 -1
- package/dist/extensions/selfimprove.js +49 -0
- package/dist/extensions/selfimprove.js.map +1 -0
- package/dist/extensions/setup-scan.js +23 -592
- package/dist/extensions/setup-scan.js.map +1 -1
- package/dist/extensions/setup.js +2 -47
- package/dist/extensions/setup.js.map +1 -1
- package/dist/extensions/vendor/openviking-pi/index.js +4 -0
- package/dist/extensions/vendor/openviking-pi/index.js.map +1 -1
- package/dist/extensions/vendor/openviking-pi/qcr-hooks.js +3 -1
- package/dist/extensions/vendor/openviking-pi/qcr-hooks.js.map +1 -1
- package/dist/extensions/vendor/openviking-pi/sync.js +81 -0
- package/dist/extensions/vendor/openviking-pi/sync.js.map +1 -1
- package/dist/src/archive/restore.js +18 -15
- package/dist/src/archive/restore.js.map +1 -1
- package/dist/src/botworker.js +13 -2
- package/dist/src/botworker.js.map +1 -1
- package/dist/src/channelfs.js +3 -26
- package/dist/src/channelfs.js.map +1 -1
- package/dist/src/cli.js +14 -50
- package/dist/src/cli.js.map +1 -1
- package/dist/src/config.js +16 -0
- package/dist/src/config.js.map +1 -1
- package/dist/src/daemon.js +33 -4
- package/dist/src/daemon.js.map +1 -1
- package/dist/src/debuground.js +1 -0
- package/dist/src/debuground.js.map +1 -1
- package/dist/src/debugroundpi.js +1 -0
- package/dist/src/debugroundpi.js.map +1 -1
- package/dist/src/engine/caserunner-sdk.js +15 -251
- package/dist/src/engine/caserunner-sdk.js.map +1 -1
- package/dist/src/home/adapters.js +349 -0
- package/dist/src/home/adapters.js.map +1 -0
- package/dist/src/home/advanced.js +318 -0
- package/dist/src/home/advanced.js.map +1 -0
- package/dist/src/home/advancedtools.js +57 -0
- package/dist/src/home/advancedtools.js.map +1 -0
- package/dist/src/home/autoreview.js +325 -0
- package/dist/src/home/autoreview.js.map +1 -0
- package/dist/src/home/daemonstatus.js +32 -0
- package/dist/src/home/daemonstatus.js.map +1 -0
- package/dist/src/home/devicelogin.js +61 -0
- package/dist/src/home/devicelogin.js.map +1 -0
- package/dist/src/home/launch.js +202 -0
- package/dist/src/home/launch.js.map +1 -0
- package/dist/src/home/loginowner.js +205 -0
- package/dist/src/home/loginowner.js.map +1 -0
- package/dist/src/home/memory.js +122 -0
- package/dist/src/home/memory.js.map +1 -0
- package/dist/src/home/model.js +811 -0
- package/dist/src/home/model.js.map +1 -0
- package/dist/src/home/panel.js +520 -0
- package/dist/src/home/panel.js.map +1 -0
- package/dist/src/home/ports.js +19 -0
- package/dist/src/home/ports.js.map +1 -0
- package/dist/src/home/review.js +163 -0
- package/dist/src/home/review.js.map +1 -0
- package/dist/src/home/reviews.js +478 -0
- package/dist/src/home/reviews.js.map +1 -0
- package/dist/src/home/setup.js +507 -0
- package/dist/src/home/setup.js.map +1 -0
- package/dist/src/home/setupadapters.js +214 -0
- package/dist/src/home/setupadapters.js.map +1 -0
- package/dist/src/home/setupcard.js +199 -0
- package/dist/src/home/setupcard.js.map +1 -0
- package/dist/src/home/setuptools.js +45 -0
- package/dist/src/home/setuptools.js.map +1 -0
- package/dist/src/home/spend.js +160 -0
- package/dist/src/home/spend.js.map +1 -0
- package/dist/src/home/switches.js +209 -0
- package/dist/src/home/switches.js.map +1 -0
- package/dist/src/home/switchon.js +439 -0
- package/dist/src/home/switchon.js.map +1 -0
- package/dist/src/home/toolnames.js +41 -0
- package/dist/src/home/toolnames.js.map +1 -0
- package/dist/src/home/tune.js +341 -0
- package/dist/src/home/tune.js.map +1 -0
- package/dist/src/home/words.js +7 -0
- package/dist/src/home/words.js.map +1 -0
- package/dist/src/homes.js +14 -0
- package/dist/src/homes.js.map +1 -1
- package/dist/src/install.js +1 -1
- package/dist/src/install.js.map +1 -1
- package/dist/src/ipc/workerspec.js +8 -0
- package/dist/src/ipc/workerspec.js.map +1 -1
- package/dist/src/ledgercheck.js +39 -10
- package/dist/src/ledgercheck.js.map +1 -1
- package/dist/src/live/publish.js +1 -1
- package/dist/src/live/publish.js.map +1 -1
- package/dist/src/livecli.js +1 -1
- package/dist/src/livecli.js.map +1 -1
- package/dist/src/loginrefresh/handover.js +121 -0
- package/dist/src/loginrefresh/handover.js.map +1 -0
- package/dist/src/loginrefresh/lock.js +54 -3
- package/dist/src/loginrefresh/lock.js.map +1 -1
- package/dist/src/loginrefresh/refresher.js +33 -5
- package/dist/src/loginrefresh/refresher.js.map +1 -1
- package/dist/src/loginrefresh/supervisor.js +18 -1
- package/dist/src/loginrefresh/supervisor.js.map +1 -1
- package/dist/src/npmverbs.js +42 -12
- package/dist/src/npmverbs.js.map +1 -1
- package/dist/src/onboard/choices.js +6 -3
- package/dist/src/onboard/choices.js.map +1 -1
- package/dist/src/onboard/engine.js +33 -292
- package/dist/src/onboard/engine.js.map +1 -1
- package/dist/src/onboard/steps/bringup.js +3 -2
- package/dist/src/onboard/steps/bringup.js.map +1 -1
- package/dist/src/onboard/steps/configfill.js +18 -7
- package/dist/src/onboard/steps/configfill.js.map +1 -1
- package/dist/src/onboard/steps/loginshare.js +213 -0
- package/dist/src/onboard/steps/loginshare.js.map +1 -0
- package/dist/src/onboard/steps/memory.js +14 -2
- package/dist/src/onboard/steps/memory.js.map +1 -1
- package/dist/src/onboard/steps/repoconfig.js +72 -0
- package/dist/src/onboard/steps/repoconfig.js.map +1 -0
- package/dist/src/onboard/toolnames.js +2 -2
- package/dist/src/onboard/toolnames.js.map +1 -1
- package/dist/src/piagentdir.js +13 -0
- package/dist/src/piagentdir.js.map +1 -0
- package/dist/src/pilogin.js +31 -16
- package/dist/src/pilogin.js.map +1 -1
- package/dist/src/qcrbin.js +16 -11
- package/dist/src/qcrbin.js.map +1 -1
- package/dist/src/reviewargs.js +122 -0
- package/dist/src/reviewargs.js.map +1 -0
- package/dist/src/reviewcli.js +7 -161
- package/dist/src/reviewcli.js.map +1 -1
- package/dist/src/reviewdetach.js +76 -11
- package/dist/src/reviewdetach.js.map +1 -1
- package/dist/src/scancli.js +39 -691
- package/dist/src/scancli.js.map +1 -1
- package/dist/src/selfimprove.js +66 -0
- package/dist/src/selfimprove.js.map +1 -0
- package/dist/src/session-command-supervisor.js +38 -9
- package/dist/src/session-command-supervisor.js.map +1 -1
- package/dist/src/setup/adapters.js +5 -223
- package/dist/src/setup/adapters.js.map +1 -1
- package/dist/src/setup/caserunner.js +15 -249
- package/dist/src/setup/caserunner.js.map +1 -1
- package/dist/src/setup/engine.js +32 -341
- package/dist/src/setup/engine.js.map +1 -1
- package/dist/src/setup/facts.js +3 -592
- package/dist/src/setup/facts.js.map +1 -1
- package/dist/src/setup/findings.js +3 -947
- package/dist/src/setup/findings.js.map +1 -1
- package/dist/src/setup/presentation.js +3 -8
- package/dist/src/setup/presentation.js.map +1 -1
- package/dist/src/setup/scan.js +3 -724
- package/dist/src/setup/scan.js.map +1 -1
- package/dist/src/setup/scanbuild.js +9 -92
- package/dist/src/setup/scanbuild.js.map +1 -1
- package/dist/src/setup/scanlease.js +3 -250
- package/dist/src/setup/scanlease.js.map +1 -1
- package/dist/src/setup/scanreport.js +3 -288
- package/dist/src/setup/scanreport.js.map +1 -1
- package/dist/src/setup/scanrun.js +3 -1732
- package/dist/src/setup/scanrun.js.map +1 -1
- package/dist/src/setup/scantree.js +3 -256
- package/dist/src/setup/scantree.js.map +1 -1
- package/dist/src/setup/stage2request.js +3 -258
- package/dist/src/setup/stage2request.js.map +1 -1
- package/dist/src/setup/unitcut.js +3 -436
- package/dist/src/setup/unitcut.js.map +1 -1
- package/dist/src/state.js +75 -9
- package/dist/src/state.js.map +1 -1
- package/dist/src/takeover.js +1 -0
- package/dist/src/takeover.js.map +1 -1
- package/dist/src/takeoverpi.js +2 -1
- package/dist/src/takeoverpi.js.map +1 -1
- package/dist/src/tui/alerts.js +4 -1
- package/dist/src/tui/alerts.js.map +1 -1
- package/dist/src/tui/board.js +2 -2
- package/dist/src/tui/board.js.map +1 -1
- package/dist/src/tui/companion.js +2 -1
- package/dist/src/tui/companion.js.map +1 -1
- package/dist/src/tui/deploytab.js +2 -2
- package/dist/src/tui/deploytab.js.map +1 -1
- package/dist/src/tui/index.js +9 -6
- package/dist/src/tui/index.js.map +1 -1
- package/dist/src/tui/memory.js +3 -16
- package/dist/src/tui/memory.js.map +1 -1
- package/dist/src/tui/memoryurl.js +22 -0
- package/dist/src/tui/memoryurl.js.map +1 -0
- package/dist/src/tui/remote.js +1 -1
- package/dist/src/tui/remote.js.map +1 -1
- package/dist/src/tui/route.js +2 -2
- package/dist/src/tui/route.js.map +1 -1
- package/dist/src/tui/setupstate.js +8 -5
- package/dist/src/tui/setupstate.js.map +1 -1
- package/dist/src/tui/sshtriage.js +1 -1
- package/dist/src/tui/sshtriage.js.map +1 -1
- package/dist/src/verdicts.js +12 -0
- package/dist/src/verdicts.js.map +1 -1
- package/dist/src/wire.js +9 -4
- package/dist/src/wire.js.map +1 -1
- package/dist/src/workerbreaker.js +32 -5
- package/dist/src/workerbreaker.js.map +1 -1
- package/dist/src/workerworld.js +4 -0
- package/dist/src/workerworld.js.map +1 -1
- package/docker/README.md +1 -1
- package/docker/entrypoint.sh +40 -6
- package/docker-compose.yml +16 -1
- package/extensions/cli/home.ts +14 -0
- package/extensions/cli/selfimprove.ts +18 -0
- package/extensions/home.ts +484 -0
- package/extensions/login-route.ts +1 -1
- package/extensions/onboarding.ts +8 -7
- package/extensions/selfimprove.ts +65 -0
- package/extensions/setup-scan.ts +79 -766
- package/extensions/setup.ts +14 -70
- package/extensions/vendor/openviking-pi/NOTICE +5 -0
- package/extensions/vendor/openviking-pi/README.md +39 -5
- package/extensions/vendor/openviking-pi/index.ts +4 -0
- package/extensions/vendor/openviking-pi/patches/0014-one-turn-sync-in-flight.patch +239 -0
- package/extensions/vendor/openviking-pi/qcr-hooks.ts +3 -1
- package/extensions/vendor/openviking-pi/sync.ts +80 -0
- package/extensions/vendor/openviking-pi/upstream.json +2 -1
- package/package.json +6 -4
- package/prompts/onboarding.md +8 -7
- package/schemas/review-result.v1.json +60 -0
- package/scripts/deploy.sh +8 -6
- package/scripts/qcr-host.sh +28 -5
- package/scripts/vm-ghcr-login.sh +7 -4
- package/src/archive/restore.ts +18 -21
- package/src/botworker.ts +13 -2
- package/src/channelfs.ts +3 -26
- package/src/cli.ts +13 -54
- package/src/config.ts +38 -0
- package/src/daemon.ts +34 -4
- package/src/debuground.ts +3 -0
- package/src/debugroundpi.ts +1 -0
- package/src/engine/caserunner-sdk.ts +29 -314
- package/src/home/adapters.ts +418 -0
- package/src/home/advanced.ts +421 -0
- package/src/home/advancedtools.ts +96 -0
- package/src/home/autoreview.ts +385 -0
- package/src/home/daemonstatus.ts +55 -0
- package/src/home/devicelogin.ts +107 -0
- package/src/home/launch.ts +257 -0
- package/src/home/loginowner.ts +248 -0
- package/src/home/memory.ts +176 -0
- package/src/home/model.ts +965 -0
- package/src/home/panel.ts +666 -0
- package/src/home/ports.ts +338 -0
- package/src/home/review.ts +206 -0
- package/src/home/reviews.ts +565 -0
- package/src/home/setup.ts +689 -0
- package/src/home/setupadapters.ts +217 -0
- package/src/home/setupcard.ts +288 -0
- package/src/home/setuptools.ts +57 -0
- package/src/home/spend.ts +194 -0
- package/src/home/switches.ts +293 -0
- package/src/home/switchon.ts +554 -0
- package/src/home/toolnames.ts +46 -0
- package/src/home/tune.ts +405 -0
- package/src/home/words.ts +8 -0
- package/src/homes.ts +17 -0
- package/src/install.ts +1 -1
- package/src/ipc/workerspec.ts +15 -0
- package/src/ledgercheck.ts +45 -9
- package/src/live/publish.ts +4 -1
- package/src/livecli.ts +6 -1
- package/src/loginrefresh/handover.ts +138 -0
- package/src/loginrefresh/lock.ts +72 -3
- package/src/loginrefresh/refresher.ts +34 -5
- package/src/loginrefresh/supervisor.ts +25 -2
- package/src/npmverbs.ts +51 -11
- package/src/onboard/choices.ts +6 -3
- package/src/onboard/engine.ts +64 -434
- package/src/onboard/steps/bringup.ts +3 -2
- package/src/onboard/steps/configfill.ts +23 -7
- package/src/onboard/steps/loginshare.ts +258 -0
- package/src/onboard/steps/memory.ts +18 -2
- package/src/onboard/steps/repoconfig.ts +85 -0
- package/src/onboard/toolnames.ts +2 -2
- package/src/piagentdir.ts +12 -0
- package/src/pilogin.ts +32 -16
- package/src/qcrbin.ts +16 -11
- package/src/reviewargs.ts +119 -0
- package/src/reviewcli.ts +6 -167
- package/src/reviewdetach.ts +107 -13
- package/src/scancli.ts +75 -826
- package/src/selfimprove.ts +90 -0
- package/src/session-command-supervisor.ts +41 -8
- package/src/setup/adapters.ts +13 -221
- package/src/setup/caserunner.ts +34 -273
- package/src/setup/engine.ts +62 -496
- package/src/setup/facts.ts +3 -627
- package/src/setup/findings.ts +3 -1264
- package/src/setup/presentation.ts +3 -18
- package/src/setup/scan.ts +3 -1033
- package/src/setup/scanbuild.ts +20 -114
- package/src/setup/scanlease.ts +3 -297
- package/src/setup/scanreport.ts +3 -373
- package/src/setup/scanrun.ts +3 -2268
- package/src/setup/scantree.ts +3 -259
- package/src/setup/stage2request.ts +3 -347
- package/src/setup/unitcut.ts +3 -523
- package/src/state.ts +126 -11
- package/src/takeover.ts +5 -0
- package/src/takeoverpi.ts +3 -2
- package/src/tui/alerts.ts +9 -2
- package/src/tui/board.ts +2 -2
- package/src/tui/companion.ts +2 -1
- package/src/tui/deploytab.ts +2 -2
- package/src/tui/index.ts +9 -6
- package/src/tui/memory.ts +3 -15
- package/src/tui/memoryurl.ts +22 -0
- package/src/tui/remote.ts +1 -1
- package/src/tui/route.ts +2 -2
- package/src/tui/setupstate.ts +20 -16
- package/src/tui/sshtriage.ts +1 -1
- package/src/verdicts.ts +13 -0
- package/src/wire.ts +9 -4
- package/src/workerbreaker.ts +41 -5
- package/src/workerworld.ts +9 -1
- package/dist/src/live/projection.js +0 -450
- package/dist/src/live/projection.js.map +0 -1
- package/dist/src/live/read.js +0 -60
- package/dist/src/live/read.js.map +0 -1
- package/dist/src/onboard/launch.js +0 -75
- package/dist/src/onboard/launch.js.map +0 -1
- package/dist/src/setup/ledger.js +0 -207
- package/dist/src/setup/ledger.js.map +0 -1
- package/dist/src/setup/stage2.js +0 -617
- package/dist/src/setup/stage2.js.map +0 -1
- package/dist/src/setup/textsniff.js +0 -89
- package/dist/src/setup/textsniff.js.map +0 -1
- package/src/live/projection.ts +0 -491
- package/src/live/read.ts +0 -86
- package/src/onboard/launch.ts +0 -103
- package/src/setup/ledger.ts +0 -324
- package/src/setup/stage2.ts +0 -926
- package/src/setup/textsniff.ts +0 -95
package/src/state.ts
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
// Core tables: pr_state, rounds, bot_comments (registry), deliveries
|
|
3
3
|
// (webhook dedupe), events (append log).
|
|
4
4
|
import { findingRowFields } from "@quickchatai/pi-quickreview/src/finding-row.js";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
discoveryRowsOf,
|
|
7
|
+
roundStageDeadlines,
|
|
8
|
+
} from "@quickchatai/pi-quickreview/src/round-stage-deadlines.js";
|
|
6
9
|
import Database from "better-sqlite3";
|
|
7
10
|
import { chmodSync, mkdirSync, renameSync, writeFileSync } from "node:fs";
|
|
8
11
|
import { dirname, join } from "node:path";
|
|
@@ -550,8 +553,9 @@ export type DrainState =
|
|
|
550
553
|
/** When the hold lapses; null = until lifted (operator only). */
|
|
551
554
|
expires_at: string | null;
|
|
552
555
|
/** Why a SYSTEM engaged it (`worker_failures`, the crash-loop breaker); null
|
|
553
|
-
* for a hand-set drain. A drain with a reason is lifted
|
|
554
|
-
*
|
|
556
|
+
* for a hand-set drain. A drain with a reason is never lifted by a boot's
|
|
557
|
+
* first reconcile pass: the breaker's lapses at its `expires_at`, or an
|
|
558
|
+
* explicit `drain off` lifts it sooner. */
|
|
555
559
|
reason: string | null;
|
|
556
560
|
/** The operator's no-auto-lift HOLD (`qcr drain on --hold`): no
|
|
557
561
|
* boot's first reconcile lifts it, only an explicit operator `drain off`.
|
|
@@ -1050,7 +1054,8 @@ CREATE TABLE IF NOT EXISTS drain_state (
|
|
|
1050
1054
|
-- with parked_at set gets no worker until the operator's rerun-round or
|
|
1051
1055
|
-- restart-session clears the row. worker_breaker_global.epoch ticks at every
|
|
1052
1056
|
-- worker success (and at the lift of the breaker's drain); a failure counts
|
|
1053
|
-
-- toward the global drain only while it carries the current epoch.
|
|
1057
|
+
-- toward the global drain only while it carries the current epoch. trips
|
|
1058
|
+
-- counts the breaker's drains since then: each waits twice as long to lapse.
|
|
1054
1059
|
CREATE TABLE IF NOT EXISTS worker_breaker (
|
|
1055
1060
|
pr INTEGER PRIMARY KEY,
|
|
1056
1061
|
failures INTEGER NOT NULL DEFAULT 0,
|
|
@@ -1064,6 +1069,7 @@ CREATE TABLE IF NOT EXISTS worker_breaker (
|
|
|
1064
1069
|
CREATE TABLE IF NOT EXISTS worker_breaker_global (
|
|
1065
1070
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
1066
1071
|
epoch INTEGER NOT NULL DEFAULT 0,
|
|
1072
|
+
trips INTEGER NOT NULL DEFAULT 0,
|
|
1067
1073
|
updated_at TEXT NOT NULL
|
|
1068
1074
|
);
|
|
1069
1075
|
-- The host lease: who holds a PR's session while the operator works
|
|
@@ -1828,6 +1834,7 @@ export class StateStore {
|
|
|
1828
1834
|
this.db.exec("CREATE INDEX IF NOT EXISTS bot_comments_id ON bot_comments(comment_id)");
|
|
1829
1835
|
this.migrateRecheckReceipts();
|
|
1830
1836
|
this.migrateDrainHold();
|
|
1837
|
+
this.migrateBreakerHeal();
|
|
1831
1838
|
this.migrateSessionCommandCancelledStatus();
|
|
1832
1839
|
this.runDataMigrations();
|
|
1833
1840
|
}
|
|
@@ -1932,6 +1939,37 @@ export class StateStore {
|
|
|
1932
1939
|
})();
|
|
1933
1940
|
}
|
|
1934
1941
|
|
|
1942
|
+
/**
|
|
1943
|
+
* The breaker's drain lapses on its own (src/workerbreaker.ts). One an older
|
|
1944
|
+
* daemon set stands with no expiry, and only `drain off` would lift it: the
|
|
1945
|
+
* boot that adds the `trips` column gives it an expiry of now, so the first
|
|
1946
|
+
* sweep lifts it and the queue retries, with the failures that tripped it
|
|
1947
|
+
* spent. Only the breaker's drain is carried
|
|
1948
|
+
* (its reason, no hold); a hand-set drain stands as it was set.
|
|
1949
|
+
*/
|
|
1950
|
+
private migrateBreakerHeal(): void {
|
|
1951
|
+
this.db.transaction(() => {
|
|
1952
|
+
if (
|
|
1953
|
+
!this.addColumn(
|
|
1954
|
+
"ALTER TABLE worker_breaker_global ADD COLUMN trips INTEGER NOT NULL DEFAULT 0",
|
|
1955
|
+
)
|
|
1956
|
+
)
|
|
1957
|
+
return;
|
|
1958
|
+
const info = this.db
|
|
1959
|
+
.prepare(
|
|
1960
|
+
`UPDATE drain_state SET expires_at = ?
|
|
1961
|
+
WHERE id = 1 AND draining = 1 AND reason = ? AND expires_at IS NULL AND hold = 0`,
|
|
1962
|
+
)
|
|
1963
|
+
.run(this.ts(), WORKER_FAILURES_DRAIN_REASON);
|
|
1964
|
+
if (info.changes > 0) {
|
|
1965
|
+
// The failures that tripped it are spent, as at a new trip: a retry
|
|
1966
|
+
// failing once more must not re-drain with them.
|
|
1967
|
+
this.resetWorkerFailureWindow();
|
|
1968
|
+
this.logEvent("breaker_drain_heal_carried", null, {});
|
|
1969
|
+
}
|
|
1970
|
+
})();
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1935
1973
|
/** Existing databases carry a CHECK constraint that predates operator
|
|
1936
1974
|
* cancellation. Rebuild this one table in-place; every column and index is
|
|
1937
1975
|
* copied, and the transaction makes the old or new shape visible atomically. */
|
|
@@ -6696,18 +6734,31 @@ export class StateStore {
|
|
|
6696
6734
|
* One more consecutive worker failure for `pr`. The PR is parked when the count
|
|
6697
6735
|
* reaches `parkAfter`; `parked` says whether it stands parked now and
|
|
6698
6736
|
* `newlyParked` whether THIS failure parked it (the caller raises the anomaly
|
|
6699
|
-
* once).
|
|
6737
|
+
* once). With `drain: false` it does not count toward the global drain: its
|
|
6738
|
+
* epoch is one no window ever carries, so `failedWorkerPrsSince` never lists
|
|
6739
|
+
* it. With `park: false` it counts but does not park the PR by itself.
|
|
6700
6740
|
*/
|
|
6701
6741
|
recordWorkerFailure(
|
|
6702
6742
|
pr: number,
|
|
6703
|
-
o: {
|
|
6743
|
+
o: {
|
|
6744
|
+
commandId: string;
|
|
6745
|
+
error: string;
|
|
6746
|
+
at: string;
|
|
6747
|
+
parkAfter: number;
|
|
6748
|
+
drain?: boolean;
|
|
6749
|
+
park?: boolean;
|
|
6750
|
+
},
|
|
6704
6751
|
): { failures: number; parked: boolean; newlyParked: boolean } {
|
|
6705
6752
|
return this.db
|
|
6706
6753
|
.transaction(() => {
|
|
6707
6754
|
const row = this.workerBreaker(pr);
|
|
6708
6755
|
const failures = (row?.failures ?? 0) + 1;
|
|
6709
6756
|
const wasParked = row?.parked_at != null;
|
|
6710
|
-
const parkedAt = wasParked
|
|
6757
|
+
const parkedAt = wasParked
|
|
6758
|
+
? row.parked_at
|
|
6759
|
+
: o.park !== false && failures >= o.parkAfter
|
|
6760
|
+
? o.at
|
|
6761
|
+
: null;
|
|
6711
6762
|
this.db
|
|
6712
6763
|
.prepare(
|
|
6713
6764
|
`INSERT INTO worker_breaker (pr, failures, parked_at, last_error, last_failure_at, last_command_id, failure_epoch, updated_at)
|
|
@@ -6722,7 +6773,7 @@ export class StateStore {
|
|
|
6722
6773
|
error: o.error,
|
|
6723
6774
|
at: o.at,
|
|
6724
6775
|
command: o.commandId,
|
|
6725
|
-
epoch: this.workerEpoch(),
|
|
6776
|
+
epoch: o.drain === false ? -1 : this.workerEpoch(),
|
|
6726
6777
|
});
|
|
6727
6778
|
return {
|
|
6728
6779
|
failures,
|
|
@@ -6770,12 +6821,31 @@ export class StateStore {
|
|
|
6770
6821
|
return row?.epoch ?? 0;
|
|
6771
6822
|
}
|
|
6772
6823
|
|
|
6773
|
-
/** Start the global failure window afresh: earlier failures stop counting
|
|
6824
|
+
/** Start the global failure window afresh: earlier failures stop counting,
|
|
6825
|
+
* and the breaker's next drain waits the shortest time again. */
|
|
6774
6826
|
resetWorkerFailureWindow(): void {
|
|
6775
6827
|
this.db
|
|
6776
6828
|
.prepare(
|
|
6777
|
-
`INSERT INTO worker_breaker_global (id, epoch, updated_at) VALUES (1, 1, @ts)
|
|
6778
|
-
ON CONFLICT(id) DO UPDATE SET epoch = epoch + 1, updated_at = @ts`,
|
|
6829
|
+
`INSERT INTO worker_breaker_global (id, epoch, trips, updated_at) VALUES (1, 1, 0, @ts)
|
|
6830
|
+
ON CONFLICT(id) DO UPDATE SET epoch = epoch + 1, trips = 0, updated_at = @ts`,
|
|
6831
|
+
)
|
|
6832
|
+
.run({ ts: this.ts() });
|
|
6833
|
+
}
|
|
6834
|
+
|
|
6835
|
+
/** The breaker's drains since the last worker success or operator lift. */
|
|
6836
|
+
workerBreakerTrips(): number {
|
|
6837
|
+
const row = this.db.prepare("SELECT trips FROM worker_breaker_global WHERE id = 1").get() as
|
|
6838
|
+
{ trips: number } | undefined;
|
|
6839
|
+
return row?.trips ?? 0;
|
|
6840
|
+
}
|
|
6841
|
+
|
|
6842
|
+
/** The breaker drained once more: the failures that tripped it are spent (a
|
|
6843
|
+
* new epoch), and its next drain waits longer. */
|
|
6844
|
+
recordWorkerBreakerTrip(): void {
|
|
6845
|
+
this.db
|
|
6846
|
+
.prepare(
|
|
6847
|
+
`INSERT INTO worker_breaker_global (id, epoch, trips, updated_at) VALUES (1, 1, 1, @ts)
|
|
6848
|
+
ON CONFLICT(id) DO UPDATE SET epoch = epoch + 1, trips = trips + 1, updated_at = @ts`,
|
|
6779
6849
|
)
|
|
6780
6850
|
.run({ ts: this.ts() });
|
|
6781
6851
|
}
|
|
@@ -7005,6 +7075,25 @@ export class StateStore {
|
|
|
7005
7075
|
return someMarkHasKey(rows, key);
|
|
7006
7076
|
}
|
|
7007
7077
|
|
|
7078
|
+
/** Has a check after this (pr, key)'s spent nudge already been recorded? */
|
|
7079
|
+
ledgerRepeatSeen(pr: number, key: string): boolean {
|
|
7080
|
+
const rows = this.db
|
|
7081
|
+
.prepare("SELECT payload FROM events WHERE kind = 'ledger_stale_repeat' AND pr = ?")
|
|
7082
|
+
.all(pr) as Array<{ payload: string | null }>;
|
|
7083
|
+
return someMarkHasKey(rows, key);
|
|
7084
|
+
}
|
|
7085
|
+
|
|
7086
|
+
/** The heads a recheck upheld on `pr` at or after `since` (every one when null). */
|
|
7087
|
+
recheckUpheldHeads(pr: number, since: string | null): string[] {
|
|
7088
|
+
const rows = this.db
|
|
7089
|
+
.prepare("SELECT payload FROM events WHERE kind = 'recheck_upheld' AND pr = ? AND ts >= ?")
|
|
7090
|
+
.all(pr, since ?? "") as Array<{ payload: string | null }>;
|
|
7091
|
+
// An unreadable mark names no head.
|
|
7092
|
+
return rows
|
|
7093
|
+
.map((r) => readStringField(r.payload, "head_sha"))
|
|
7094
|
+
.filter((head): head is string => head !== undefined && head !== "");
|
|
7095
|
+
}
|
|
7096
|
+
|
|
7008
7097
|
/**
|
|
7009
7098
|
* What the event log says about the migration-collision COMMENT with this
|
|
7010
7099
|
* fingerprint on this PR (the collision comment's dedupe). Scans every such event for the
|
|
@@ -7781,9 +7870,27 @@ export interface OpenPrsSnapshot {
|
|
|
7781
7870
|
prs: OpenPrSummary[];
|
|
7782
7871
|
}
|
|
7783
7872
|
|
|
7873
|
+
/** The authoritative reconcile poll's last pass (status.json `poll`): what
|
|
7874
|
+
* "auto-review is working" is judged on, beside the heartbeat (the home's
|
|
7875
|
+
* label, src/home/autoreview.ts). */
|
|
7876
|
+
export interface PollStatus {
|
|
7877
|
+
/** When the last pass ran, ISO; null before the boot's first pass has ended. */
|
|
7878
|
+
last_at: string | null;
|
|
7879
|
+
/** That pass ran with no step error; null before the first pass. */
|
|
7880
|
+
ok: boolean | null;
|
|
7881
|
+
/** The first error, while not ok. */
|
|
7882
|
+
error?: string;
|
|
7883
|
+
/** The daemon's `[daemon] poll_interval_s`: freshness is judged against
|
|
7884
|
+
* the daemon's own cadence, never a reader's guess. */
|
|
7885
|
+
interval_s: number;
|
|
7886
|
+
}
|
|
7887
|
+
|
|
7784
7888
|
export interface StatusSnapshot {
|
|
7785
7889
|
written_at: string;
|
|
7786
7890
|
dry_run: boolean;
|
|
7891
|
+
/** The reconcile poll's last pass. Absent in snapshots from an older
|
|
7892
|
+
* daemon — readers say "unknown", never "working". */
|
|
7893
|
+
poll?: PollStatus;
|
|
7787
7894
|
/** Where the daemon runs (src/runhost.ts). Absent: not reported (an older
|
|
7788
7895
|
* daemon, or one assembled without its host facts). */
|
|
7789
7896
|
run_host?: import("./runhost.js").RunHostStatus;
|
|
@@ -7874,6 +7981,11 @@ export interface StatusSnapshot {
|
|
|
7874
7981
|
max_concurrent_rounds?: number;
|
|
7875
7982
|
total_spend_usd: number;
|
|
7876
7983
|
today_spend_usd?: number;
|
|
7984
|
+
/** The month's spend so far: every round started in the snapshot's UTC
|
|
7985
|
+
* month (the same rule as `today_spend_usd`, expired rounds included).
|
|
7986
|
+
* Absent in snapshots from an older daemon: the home's spend line then
|
|
7987
|
+
* reads unknown, never a zero. */
|
|
7988
|
+
month_spend_usd?: number;
|
|
7877
7989
|
digest_headline?: string;
|
|
7878
7990
|
/** Present while pi sessions are failing API calls with token-expired
|
|
7879
7991
|
* errors (packages/pi-plugin/src/authwatch.ts, fresh < 30 min). The TUI raises an alert. */
|
|
@@ -8155,6 +8267,7 @@ export function buildStatusSnapshot(
|
|
|
8155
8267
|
started,
|
|
8156
8268
|
roundStageBounds,
|
|
8157
8269
|
round.sweep_owed === 1,
|
|
8270
|
+
discoveryRowsOf(round),
|
|
8158
8271
|
);
|
|
8159
8272
|
return {
|
|
8160
8273
|
...identity,
|
|
@@ -8224,6 +8337,8 @@ export function buildStatusSnapshot(
|
|
|
8224
8337
|
...(maxConcurrentRounds !== undefined ? { max_concurrent_rounds: maxConcurrentRounds } : {}),
|
|
8225
8338
|
total_spend_usd: store.totalSpend(),
|
|
8226
8339
|
today_spend_usd: roundUsage(store.roundsOn(today)).spend_usd,
|
|
8340
|
+
// roundsOn matches by prefix: the month's `YYYY-MM` is every day of it
|
|
8341
|
+
month_spend_usd: roundUsage(store.roundsOn(today.slice(0, 7))).spend_usd,
|
|
8227
8342
|
...(digestHeadline ? { digest_headline: digestHeadline } : {}),
|
|
8228
8343
|
...(piAuth
|
|
8229
8344
|
? {
|
package/src/takeover.ts
CHANGED
|
@@ -82,6 +82,9 @@ export interface TakeoverPlan {
|
|
|
82
82
|
cwd: string;
|
|
83
83
|
/** The login's owner dir (`pihome/agent`): read-only, borrowed from. */
|
|
84
84
|
agent_dir: string;
|
|
85
|
+
/** The login's dir when it is not `agent_dir` (the single box's shared
|
|
86
|
+
* login): the login is borrowed from here, the rest from `agent_dir`. */
|
|
87
|
+
login_dir?: string;
|
|
85
88
|
/** The PR's JSONL, which the pi resumes and appends to. */
|
|
86
89
|
session_file: string;
|
|
87
90
|
model: string;
|
|
@@ -597,6 +600,7 @@ function rmQuiet(file: string): void {
|
|
|
597
600
|
export function takeoverPlanFor(o: {
|
|
598
601
|
root: string;
|
|
599
602
|
agentDir: string;
|
|
603
|
+
loginDir?: string;
|
|
600
604
|
sessionsRoot: string;
|
|
601
605
|
model: string;
|
|
602
606
|
pr: number;
|
|
@@ -612,6 +616,7 @@ export function takeoverPlanFor(o: {
|
|
|
612
616
|
root: o.root,
|
|
613
617
|
cwd: o.root,
|
|
614
618
|
agent_dir: o.agentDir,
|
|
619
|
+
...(o.loginDir !== undefined && o.loginDir !== o.agentDir ? { login_dir: o.loginDir } : {}),
|
|
615
620
|
session_file: file,
|
|
616
621
|
model: o.model,
|
|
617
622
|
...(o.systemPromptFile ? { system_prompt_file: o.systemPromptFile } : {}),
|
package/src/takeoverpi.ts
CHANGED
|
@@ -91,6 +91,7 @@ export function readTakeoverSpec(file: string): TakeoverSpec {
|
|
|
91
91
|
raw.session !== `takeover-pr-${raw.pr}` ||
|
|
92
92
|
typeof raw.session_file !== "string" ||
|
|
93
93
|
typeof raw.agent_dir !== "string" ||
|
|
94
|
+
(raw.login_dir !== undefined && typeof raw.login_dir !== "string") ||
|
|
94
95
|
typeof raw.borrow_dir !== "string" ||
|
|
95
96
|
typeof raw.model !== "string" ||
|
|
96
97
|
typeof raw.cwd !== "string" ||
|
|
@@ -119,11 +120,11 @@ export function readLogin(agentDir: string): string | null {
|
|
|
119
120
|
* the owner's dir.
|
|
120
121
|
*/
|
|
121
122
|
export function prepareBorrowedAgent(
|
|
122
|
-
spec: Pick<TakeoverSpec, "agent_dir" | "borrow_dir" | "idle_ms">,
|
|
123
|
+
spec: Pick<TakeoverSpec, "agent_dir" | "login_dir" | "borrow_dir" | "idle_ms">,
|
|
123
124
|
now?: () => number,
|
|
124
125
|
): { expiresAt: number | null } {
|
|
125
126
|
const { expiresAt } = copyPiAuth({
|
|
126
|
-
srcDir: spec.agent_dir,
|
|
127
|
+
srcDir: spec.login_dir ?? spec.agent_dir,
|
|
127
128
|
dstDir: spec.borrow_dir,
|
|
128
129
|
needMs: spec.idle_ms + PI_REFRESH_MARGIN_MS,
|
|
129
130
|
words: ENGINE_AUTH_WORDS,
|
package/src/tui/alerts.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { zoneDateString } from "../eod.js";
|
|
|
30
30
|
import { curationNeedsAttention } from "../learner/status.js";
|
|
31
31
|
import { memoryStatusLine, type MemoryStatus } from "../memory/health.js";
|
|
32
32
|
import type { StatusSnapshot } from "../state.js";
|
|
33
|
+
import { DEFAULT_BREAKER_LIMITS } from "../workerbreaker.js";
|
|
33
34
|
|
|
34
35
|
export type AlertTier = "notice" | "alert";
|
|
35
36
|
|
|
@@ -616,7 +617,11 @@ export interface WorkerCrashAlertMemory {
|
|
|
616
617
|
export function maybeWriteWorkerCrashAlerts(
|
|
617
618
|
alertsDir: string,
|
|
618
619
|
snapshot:
|
|
619
|
-
Pick<
|
|
620
|
+
| Pick<
|
|
621
|
+
StatusSnapshot,
|
|
622
|
+
"prs" | "draining" | "draining_reason" | "draining_since" | "draining_until"
|
|
623
|
+
>
|
|
624
|
+
| undefined,
|
|
620
625
|
fresh: boolean,
|
|
621
626
|
mem: WorkerCrashAlertMemory,
|
|
622
627
|
now: () => Date = () => new Date(),
|
|
@@ -660,7 +665,9 @@ export function maybeWriteWorkerCrashAlerts(
|
|
|
660
665
|
"worker-failures-drain",
|
|
661
666
|
null,
|
|
662
667
|
"workers failing on several PRs: the bot is draining",
|
|
663
|
-
|
|
668
|
+
snapshot.draining_until
|
|
669
|
+
? `Workers failed on three PRs within 15 minutes with no success, so the bot drains: no round, answer or debug worker starts. It lifts itself at ${snapshot.draining_until} and the queue retries; if workers fail again it drains again, each time waiting twice as long (up to ${DEFAULT_BREAKER_LIMITS.healMaxMs / 3_600_000} h). Run drain off to retry now.`
|
|
670
|
+
: "Workers failed on three PRs within 15 minutes with no success, so the bot drains: no round, answer or debug worker starts. Fix the cause, then run drain off.",
|
|
664
671
|
);
|
|
665
672
|
return names;
|
|
666
673
|
}
|
package/src/tui/board.ts
CHANGED
|
@@ -522,7 +522,7 @@ function barSegments(view: BoardView, alerts: boolean): string[] {
|
|
|
522
522
|
}
|
|
523
523
|
// Short on the bar, where every segment competes for the width; the list
|
|
524
524
|
// carries the full sentence with the next step.
|
|
525
|
-
const setupSeg = view.setup ? `${WARN}▲ setup not finished
|
|
525
|
+
const setupSeg = view.setup ? `${WARN}▲ setup not finished${DRESET}` : "";
|
|
526
526
|
const s = view.snapshot;
|
|
527
527
|
const rounds = s ? `rounds ${s.running_rounds}/${s.max_concurrent_rounds ?? "?"}` : "";
|
|
528
528
|
const spend = s?.today_spend_usd !== undefined ? `today $${s.today_spend_usd.toFixed(2)}` : "";
|
|
@@ -534,7 +534,7 @@ function barSegments(view: BoardView, alerts: boolean): string[] {
|
|
|
534
534
|
// guessed at. The operator's HOLD says that no boot lifts it; an
|
|
535
535
|
// operator drain from a daemon that reports no hold marker says "hold ?".
|
|
536
536
|
const drainSeg = s?.draining
|
|
537
|
-
? `${FG.anomaly}⛔ DRAINING — ${s.running_rounds} round${s.running_rounds === 1 ? "" : "s"} finishing · ${s.drain_failed_loud ?? 0} failed loud · ${s.drained_deliveries ?? 0} dropped · ${leverHolderLabel(s.draining_by, s.draining_until)}${drainHoldLabel(s)}${s.draining_reason === "worker_failures" ? " · workers keep failing, drain off lifts it" : ""}${RESET}`
|
|
537
|
+
? `${FG.anomaly}⛔ DRAINING — ${s.running_rounds} round${s.running_rounds === 1 ? "" : "s"} finishing · ${s.drain_failed_loud ?? 0} failed loud · ${s.drained_deliveries ?? 0} dropped · ${leverHolderLabel(s.draining_by, s.draining_until)}${drainHoldLabel(s)}${s.draining_reason === "worker_failures" ? (s.draining_until ? " · workers kept failing, it retries when it lapses; drain off lifts it now" : " · workers keep failing, drain off lifts it") : ""}${RESET}`
|
|
538
538
|
: "";
|
|
539
539
|
const td = s?.thread_decisions;
|
|
540
540
|
const breakerSeg =
|
package/src/tui/companion.ts
CHANGED
|
@@ -45,6 +45,7 @@ import { COMPANION_IMAGE, type CompanionConfig } from "../config.js";
|
|
|
45
45
|
import { writeAtomic } from "../channelfs.js";
|
|
46
46
|
import { appStamp } from "../appstamp.js";
|
|
47
47
|
import { TOKEN_FILE } from "./ghread.js";
|
|
48
|
+
import { MEMORY_PORT } from "./memoryurl.js";
|
|
48
49
|
|
|
49
50
|
export interface DockerResult {
|
|
50
51
|
ok: boolean;
|
|
@@ -274,7 +275,7 @@ export const MEMORY = {
|
|
|
274
275
|
subnet: "198.51.100.0/24",
|
|
275
276
|
gateway: "198.51.100.1",
|
|
276
277
|
/** Same port the server listens on in the VM container ([openviking] port). */
|
|
277
|
-
port:
|
|
278
|
+
port: MEMORY_PORT,
|
|
278
279
|
/** The OpenViking user whose memory the companion owns — becomes the
|
|
279
280
|
* `X-OpenViking-User` header on every request it makes. */
|
|
280
281
|
user: "companion",
|
package/src/tui/deploytab.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// `[5] deploy` — "what would a deploy ship?" plus the deploy button.
|
|
2
|
-
// Every deploy is a RELEASE: the tag
|
|
3
|
-
//
|
|
2
|
+
// Every deploy is a RELEASE: the tag and the two-arch build with its smoke
|
|
3
|
+
// already happened in CI, so the tab only chooses a version.
|
|
4
4
|
// It shows the version and revision the VM runs (`qcr-update status --json`,
|
|
5
5
|
// read over ssh) against the chosen version, the commits between the two
|
|
6
6
|
// revisions (from the local checkout), and deploy.sh's own
|
package/src/tui/index.ts
CHANGED
|
@@ -114,8 +114,8 @@ troubleshooting:
|
|
|
114
114
|
project): every read is docker exec into the daemon
|
|
115
115
|
container, no ssh. The board names an absent or stopped
|
|
116
116
|
container with the docker compose command that starts it
|
|
117
|
-
setup not finished the
|
|
118
|
-
|
|
117
|
+
setup not finished the setup checklist is not complete: run qcr (the board
|
|
118
|
+
names the next step)
|
|
119
119
|
▲ DATA STALE ssh works but status.json stopped updating — the
|
|
120
120
|
container is down or the daemon hung (qcr-side:
|
|
121
121
|
ssh <host> sudo docker ps / docker logs)
|
|
@@ -454,11 +454,14 @@ export async function runTui(argv: string | string[] = [], deps: RunTuiDeps = {}
|
|
|
454
454
|
|
|
455
455
|
const fileConfig = deps.config ?? loadConfig();
|
|
456
456
|
// The route — which daemon the board reads, and how — from [tui] plus the
|
|
457
|
-
// facts
|
|
458
|
-
// onboarding checklist, the Docker socket's mode).
|
|
459
|
-
//
|
|
457
|
+
// facts the stack holds locally (the compose project's .env, the
|
|
458
|
+
// onboarding checklist, the Docker socket's mode). The stack is the one the
|
|
459
|
+
// home sets up (src/homes.ts `stackRoot`), so an app installed by a package
|
|
460
|
+
// manager reads it from its state home. Two small file reads and one
|
|
461
|
+
// access(2): no docker, network or walk, so it may precede the frame.
|
|
460
462
|
const { readRouteFacts, resolveTuiRoute, pinnedVersion } = await import("./route.js");
|
|
461
|
-
const
|
|
463
|
+
const { stackRoot } = await import("../homes.js");
|
|
464
|
+
const routeFacts = deps.routeFacts ?? readRouteFacts(stackRoot(process.env, sandboxRoot()));
|
|
462
465
|
const route = resolveTuiRoute(fileConfig.tui, routeFacts);
|
|
463
466
|
const loaded: Config = { ...fileConfig, tui: route.cfg };
|
|
464
467
|
// In the containerised TUI the companion is a container the host wrapper
|
package/src/tui/memory.ts
CHANGED
|
@@ -52,7 +52,7 @@ import {
|
|
|
52
52
|
import { countOutcome, emptyRequestCounter, type RequestCounter } from "../memory/health.js";
|
|
53
53
|
import { OpenVikingStore } from "../openviking/store.js";
|
|
54
54
|
import { readRetiredUrisFile, writeRetiredUris } from "../memory/ovsession.js";
|
|
55
|
-
import { keysFilePath
|
|
55
|
+
import { keysFilePath } from "./companion.js";
|
|
56
56
|
|
|
57
57
|
export { matchesQuery, normalizeTag, refOf } from "../memory/store.js";
|
|
58
58
|
export type {
|
|
@@ -227,20 +227,8 @@ export function keysFileReader(
|
|
|
227
227
|
};
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
*
|
|
233
|
-
* `QCR_MEMORY_URL` overrides it wholesale; `QCR_MEMORY_PORT` matches the flag
|
|
234
|
-
* the forward itself takes, so a non-default port only has to be spelled once.
|
|
235
|
-
* `MEMORY.port` is the shared constant the forward and the companion already
|
|
236
|
-
* agree on.
|
|
237
|
-
*/
|
|
238
|
-
export function tuiMemoryBaseUrl(env: NodeJS.ProcessEnv = process.env): string {
|
|
239
|
-
const explicit = (env["QCR_MEMORY_URL"] ?? env["QCR_MEMORY_ENDPOINT"] ?? "").trim();
|
|
240
|
-
if (explicit !== "") return explicit.replace(/\/+$/, "");
|
|
241
|
-
const port = Number(env["QCR_MEMORY_PORT"] ?? MEMORY.port) || MEMORY.port;
|
|
242
|
-
return `http://127.0.0.1:${port}`;
|
|
243
|
-
}
|
|
230
|
+
import { tuiMemoryBaseUrl } from "./memoryurl.js";
|
|
231
|
+
export { tuiMemoryBaseUrl };
|
|
244
232
|
|
|
245
233
|
// ---- the store ---------------------------------------------------------------
|
|
246
234
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Where this host reaches OpenViking: the memory forward's loopback
|
|
2
|
+
// listener. A leaf module, so a reader of the URL (the wizard's memory step,
|
|
3
|
+
// the home) never imports the TUI's memory store or the companion.
|
|
4
|
+
|
|
5
|
+
/** The port the memory forward and the companion agree on, the server's own
|
|
6
|
+
* in the VM container (`[openviking] port`). */
|
|
7
|
+
export const MEMORY_PORT = 1933;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Where the TUI reaches OpenViking: the memory forward's loopback listener.
|
|
11
|
+
*
|
|
12
|
+
* `QCR_MEMORY_URL` overrides it wholesale; `QCR_MEMORY_PORT` matches the flag
|
|
13
|
+
* the forward itself takes, so a non-default port only has to be spelled once.
|
|
14
|
+
* `MEMORY_PORT` is the shared constant the forward and the companion already
|
|
15
|
+
* agree on.
|
|
16
|
+
*/
|
|
17
|
+
export function tuiMemoryBaseUrl(env: NodeJS.ProcessEnv = process.env): string {
|
|
18
|
+
const explicit = (env["QCR_MEMORY_URL"] ?? env["QCR_MEMORY_ENDPOINT"] ?? "").trim();
|
|
19
|
+
if (explicit !== "") return explicit.replace(/\/+$/, "");
|
|
20
|
+
const port = Number(env["QCR_MEMORY_PORT"] ?? MEMORY_PORT) || MEMORY_PORT;
|
|
21
|
+
return `http://127.0.0.1:${port}`;
|
|
22
|
+
}
|
package/src/tui/remote.ts
CHANGED
|
@@ -341,7 +341,7 @@ export function killInFlightSsh(): number {
|
|
|
341
341
|
* die on "Could not resolve hostname" and every surface would print an ssh
|
|
342
342
|
* remedy with no host in it. */
|
|
343
343
|
export const NO_SSH_HOST_STDERR =
|
|
344
|
-
'[tui] ssh_host is empty —
|
|
344
|
+
'[tui] ssh_host is empty — set it to the VM\'s ssh alias, or set [tui] target = "local"';
|
|
345
345
|
|
|
346
346
|
export const realSshRunner: SshRunner = (host, argv, opts) =>
|
|
347
347
|
host.trim() === ""
|
package/src/tui/route.ts
CHANGED
|
@@ -109,7 +109,7 @@ export function resolveTuiRoute(file: TuiConfig, facts: RouteFacts): TuiRoute {
|
|
|
109
109
|
};
|
|
110
110
|
notes.push(
|
|
111
111
|
`[tui] target is "ssh" with no ssh_host — reading the local compose stack in ${facts.projectDir} instead ` +
|
|
112
|
-
'(
|
|
112
|
+
'(set [tui] target = "local" to make it explicit)',
|
|
113
113
|
);
|
|
114
114
|
} else {
|
|
115
115
|
return {
|
|
@@ -117,7 +117,7 @@ export function resolveTuiRoute(file: TuiConfig, facts: RouteFacts): TuiRoute {
|
|
|
117
117
|
notes,
|
|
118
118
|
unconfigured:
|
|
119
119
|
topology === "vm"
|
|
120
|
-
? "[tui] ssh_host is empty — the VM
|
|
120
|
+
? "[tui] ssh_host is empty — set it to the VM's ssh alias in config.toml"
|
|
121
121
|
: EMPTY_SSH_HOST,
|
|
122
122
|
};
|
|
123
123
|
}
|
package/src/tui/setupstate.ts
CHANGED
|
@@ -1,25 +1,27 @@
|
|
|
1
|
-
// Is
|
|
2
|
-
// (src/onboard/engine.ts), so a TUI opened halfway through
|
|
3
|
-
//
|
|
4
|
-
// yet.
|
|
1
|
+
// Is setup finished? Read from the checklist file switch-on and Advanced keep
|
|
2
|
+
// (src/onboard/engine.ts), so a TUI opened halfway through says so on its
|
|
3
|
+
// first frame instead of troubleshooting a daemon nobody has started yet.
|
|
5
4
|
//
|
|
6
5
|
// Only a checklist that EXISTS and is not complete counts: an install that
|
|
7
|
-
// never
|
|
8
|
-
// has no checklist and gets no banner.
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
6
|
+
// never switched auto-review on (a hand-written config, the VM operator's own
|
|
7
|
+
// setup) has no checklist and gets no banner. Setup is finished in the home
|
|
8
|
+
// (a bare `qcr`), so the steps judged are the home's: switch-on's and
|
|
9
|
+
// Advanced's registry (src/home/advanced.ts), with the engine's own
|
|
10
|
+
// predicates. The next step is the first enabled, required one the file does
|
|
11
|
+
// not record as done; when there is none, nothing is left that the home could
|
|
12
|
+
// finish, and there is no banner (the home never runs the wizard's finish, so
|
|
13
|
+
// its checklist need not carry the wizard's completion marker).
|
|
12
14
|
import { type ChecklistFile } from "../onboard/engine.js";
|
|
13
15
|
|
|
14
16
|
export interface SetupState {
|
|
15
|
-
/** The title of the first pending step
|
|
16
|
-
*
|
|
17
|
+
/** The title of the first pending step, or undefined when it could not be
|
|
18
|
+
* computed. */
|
|
17
19
|
next: string | undefined;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
/** The one sentence every surface uses. */
|
|
21
23
|
export function setupLine(s: SetupState): string {
|
|
22
|
-
return `setup not finished — run \`qcr
|
|
24
|
+
return `setup not finished — run \`qcr\` to finish setting up${s.next ? ` (next: ${s.next})` : ""}`;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
/** Complete now: finished once and not reopened since (engine.ts `completeNow`). */
|
|
@@ -27,22 +29,24 @@ function completeNow(file: ChecklistFile): boolean {
|
|
|
27
29
|
return typeof file.completed_at === "string" && !file.reopened_at;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
/** undefined = nothing to say (no checklist,
|
|
32
|
+
/** undefined = nothing to say (no checklist, setup is complete, or the home
|
|
33
|
+
* has no step left to finish). */
|
|
31
34
|
export async function setupStateOf(
|
|
32
35
|
checklist: ChecklistFile | null,
|
|
33
36
|
): Promise<SetupState | undefined> {
|
|
34
37
|
if (checklist === null || completeNow(checklist)) return undefined;
|
|
35
38
|
let next: string | undefined;
|
|
36
39
|
try {
|
|
37
|
-
const {
|
|
38
|
-
const steps = [...
|
|
40
|
+
const { advancedRegistry } = await import("../home/advanced.js");
|
|
41
|
+
const steps = [...advancedRegistry().steps()].sort((a, b) => a.order - b.order);
|
|
39
42
|
const pending = steps.find(
|
|
40
43
|
(s) =>
|
|
41
44
|
!s.optional &&
|
|
42
45
|
(s.enabled ? s.enabled(checklist.choices) : true) &&
|
|
43
46
|
checklist.steps[s.id]?.status !== "done",
|
|
44
47
|
);
|
|
45
|
-
|
|
48
|
+
if (pending === undefined) return undefined;
|
|
49
|
+
next = pending.title;
|
|
46
50
|
} catch {
|
|
47
51
|
// The step registry could not be loaded: the banner still says setup is
|
|
48
52
|
// unfinished, without naming a step it could not compute.
|
package/src/tui/sshtriage.ts
CHANGED
|
@@ -584,7 +584,7 @@ export async function triageConnection(
|
|
|
584
584
|
return {
|
|
585
585
|
kind: "unconfigured",
|
|
586
586
|
cause: deps.unconfigured ?? NO_SSH_HOST_STDERR,
|
|
587
|
-
remedy: "
|
|
587
|
+
remedy: "set [tui] ssh_host in config.toml",
|
|
588
588
|
};
|
|
589
589
|
}
|
|
590
590
|
const exists = deps.exists ?? existsSync;
|
package/src/verdicts.ts
CHANGED
|
@@ -80,6 +80,7 @@ import {
|
|
|
80
80
|
import { estimateUsd, type ModelPricing } from "./usage.js";
|
|
81
81
|
import { TranscriptIndex } from "./transcriptindex.js";
|
|
82
82
|
import { withinBudget } from "@quickchatai/pi-quickreview/src/budget.js";
|
|
83
|
+
import { VALIDATE_PASS } from "@quickchatai/pi-quickreview/src/include.js";
|
|
83
84
|
import type { GateResult } from "@quickchatai/pi-quickreview/src/gate.js";
|
|
84
85
|
import { applyEvent, restoreStandingLifecycle } from "./statemachine.js";
|
|
85
86
|
import type { VerdictAnnouncer } from "./slack/announce.js";
|
|
@@ -505,6 +506,12 @@ function plannedTierMismatches(passes: VerdictPassEntry[], rows: PlanRow[]): str
|
|
|
505
506
|
return wrong;
|
|
506
507
|
}
|
|
507
508
|
|
|
509
|
+
/** Every lens of the stage reported its candidate count, and every count is zero. */
|
|
510
|
+
function nothingToValidate(passes: VerdictPassEntry[]): boolean {
|
|
511
|
+
const lenses = passes.filter((p) => p.name !== VALIDATE_PASS);
|
|
512
|
+
return lenses.length > 0 && lenses.every((p) => p.findings === 0);
|
|
513
|
+
}
|
|
514
|
+
|
|
508
515
|
function comparePlan(
|
|
509
516
|
passes: VerdictPassEntry[],
|
|
510
517
|
plan: PlanRow[] | null,
|
|
@@ -3003,6 +3010,12 @@ export class VerdictWatcher {
|
|
|
3003
3010
|
const deltaPasses = followUp ? v.passes.filter((p) => p.phase !== "sweep") : v.passes;
|
|
3004
3011
|
const sweepPasses = followUp ? v.passes.filter((p) => p.phase === "sweep") : [];
|
|
3005
3012
|
const main = comparePlan(deltaPasses, plan, addable, legacyPlanned, true);
|
|
3013
|
+
// A follow-up's delta stage dispatches no validator when its lenses
|
|
3014
|
+
// found nothing (prompts/round-follow-up.md, "Nothing to validate ⇒ no
|
|
3015
|
+
// validator"): that absence keeps the rule. Only a count every lens
|
|
3016
|
+
// reported as zero says so; an unreported one still owes the validator.
|
|
3017
|
+
if (followUp && nothingToValidate(deltaPasses))
|
|
3018
|
+
main.missing = main.missing.filter((name) => name !== VALIDATE_PASS);
|
|
3006
3019
|
let sweep: PlanComparison | null = null;
|
|
3007
3020
|
if (sweepPasses.length > 0 || sweepStatus === "ran") {
|
|
3008
3021
|
const sweepPlan = round.sweep_plan ? (JSON.parse(round.sweep_plan) as PlanRow[]) : [];
|