@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/extensions/setup.ts
CHANGED
|
@@ -57,7 +57,12 @@ import {
|
|
|
57
57
|
} from "@quickchatai/pi-quickreview/extensions/pi-tool.js";
|
|
58
58
|
import { fileURLToPath } from "node:url";
|
|
59
59
|
import { configHome, stateHome } from "../src/install.js";
|
|
60
|
-
import { SetupEngine
|
|
60
|
+
import { SetupEngine } from "../src/setup/engine.js";
|
|
61
|
+
import {
|
|
62
|
+
renderCut,
|
|
63
|
+
renderDraft,
|
|
64
|
+
renderStatus,
|
|
65
|
+
} from "@quickchatai/pi-quickreview/src/scan/cutrender.js";
|
|
61
66
|
import type { HistoryTheme, HistoryThemesFile } from "../src/setup/history.js";
|
|
62
67
|
import {
|
|
63
68
|
HISTORY_SECTIONS,
|
|
@@ -154,75 +159,14 @@ export function renderFacts(facts: RepoFacts, head: string | null): string {
|
|
|
154
159
|
].join("\n\n");
|
|
155
160
|
}
|
|
156
161
|
|
|
157
|
-
export
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const over = u.cases?.some((c) => c.over_budget)
|
|
166
|
-
? " [a file alone exceeds one case — a scan reads it in line ranges]"
|
|
167
|
-
: "";
|
|
168
|
-
return ` - ${u.name} [${u.kind}] ${u.roots.map(displayRoot).join(", ")}: ${u.files ?? "UNKNOWN"} files, ${u.reviewable ?? "UNKNOWN"} reviewable, ${u.lines ?? "UNKNOWN"} lines${split}${over}${u.note ? ` — ${u.note}` : ""}`;
|
|
169
|
-
});
|
|
170
|
-
const lines = [`${title}:`, ...rows];
|
|
171
|
-
if (v.ignored.length > 0)
|
|
172
|
-
lines.push(` ignored: ${v.ignored.join(", ")} (${v.ignored_files ?? "UNKNOWN"} files)`);
|
|
173
|
-
for (const e of v.errors) lines.push(` ✗ ${e}`);
|
|
174
|
-
for (const b of v.blocking_save) lines.push(` ! ${b}`);
|
|
175
|
-
return lines.join("\n");
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/** How a rendering names the save that makes a draft the cut — the tool, or
|
|
179
|
-
* the `qcr scan units` verb. */
|
|
180
|
-
export interface CutVerbs {
|
|
181
|
-
save: string;
|
|
182
|
-
}
|
|
183
|
-
export const CUT_TOOL_VERBS: CutVerbs = { save: "setup_unit_cut_save" };
|
|
184
|
-
export const CUT_CLI_VERBS: CutVerbs = { save: "qcr scan units save" };
|
|
185
|
-
|
|
186
|
-
/** A stored or adjusted draft, and that it is not in use until saved. */
|
|
187
|
-
export function renderDraft(
|
|
188
|
-
title: string,
|
|
189
|
-
draft: CutView,
|
|
190
|
-
verbs: CutVerbs = CUT_TOOL_VERBS,
|
|
191
|
-
): string {
|
|
192
|
-
return `${renderCut(title, draft)}\n\nNot in use until ${verbs.save}.`;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
export function renderStatus(s: CutStatus): string {
|
|
196
|
-
const out: string[] = [];
|
|
197
|
-
for (const p of s.problems) out.push(`✗ ${p}`);
|
|
198
|
-
if (s.saved) {
|
|
199
|
-
const stale =
|
|
200
|
-
s.saved.blocking_save.length > 0 ? " — STALE against the repo now, see the ! lines" : "";
|
|
201
|
-
out.push(
|
|
202
|
-
renderCut(
|
|
203
|
-
`Saved unit cut (saved ${s.saved.saved_at}${s.saved.head ? ` at ${s.saved.head.slice(0, 12)}` : ""})${stale}`,
|
|
204
|
-
s.saved,
|
|
205
|
-
),
|
|
206
|
-
);
|
|
207
|
-
} else if (s.cut_file_unreadable) {
|
|
208
|
-
out.push(
|
|
209
|
-
"Saved unit cut: UNKNOWN — the cut file could not be read (see above); nothing will overwrite it.",
|
|
210
|
-
);
|
|
211
|
-
} else {
|
|
212
|
-
out.push("No saved unit cut yet.");
|
|
213
|
-
}
|
|
214
|
-
if (s.draft)
|
|
215
|
-
out.push(
|
|
216
|
-
renderCut(
|
|
217
|
-
`Draft (${s.draft.source}, updated ${s.draft.updated_at}) — not in use until saved`,
|
|
218
|
-
s.draft,
|
|
219
|
-
),
|
|
220
|
-
);
|
|
221
|
-
if (s.has_previous)
|
|
222
|
-
out.push("The cut before the saved one is kept as `previous` in the state home.");
|
|
223
|
-
return out.join("\n\n");
|
|
224
|
-
}
|
|
225
|
-
|
|
162
|
+
export {
|
|
163
|
+
CUT_CLI_VERBS,
|
|
164
|
+
CUT_TOOL_VERBS,
|
|
165
|
+
renderCut,
|
|
166
|
+
renderDraft,
|
|
167
|
+
renderStatus,
|
|
168
|
+
type CutVerbs,
|
|
169
|
+
} from "@quickchatai/pi-quickreview/src/scan/cutrender.js";
|
|
226
170
|
export function renderThemes(themes: readonly HistoryTheme[]): string {
|
|
227
171
|
return themes.length === 0
|
|
228
172
|
? " (none — an empty history has no themes)"
|
|
@@ -60,6 +60,11 @@ Changed by the quickreviewd maintainers, 2026-08-20:
|
|
|
60
60
|
12. The extension is a factory over a configuration value; the process's
|
|
61
61
|
environment, argv, working directory and home directory are read once, by
|
|
62
62
|
a shim, and every hook runs under a deadline and a catch (2026-10-19).
|
|
63
|
+
13. One turn sync runs at a time per session: a turn that ends while one
|
|
64
|
+
runs queues its branch, the running sync sends the queued branches in
|
|
65
|
+
order, each entry sent once by its pi entry id, and agent_end waits for
|
|
66
|
+
them under turn_end's deadline, so a hook deadline can no longer start a
|
|
67
|
+
second sync that re-sends the same messages (2026-10-29).
|
|
63
68
|
|
|
64
69
|
Plus vendoring scaffolding: build-conformance fixes for this repo's stricter
|
|
65
70
|
tsconfig (ambient type declarations while pi was not installed, retired once
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# openviking-pi — vendored, patched fork
|
|
2
2
|
|
|
3
3
|
The OpenViking pi extension our sessions load. Vendored, not depended on:
|
|
4
|
-
upstream ships it as an `examples/` directory, not a package, and we need
|
|
4
|
+
upstream ships it as an `examples/` directory, not a package, and we need thirteen
|
|
5
5
|
behaviour changes that upstream has no reason to make.
|
|
6
6
|
|
|
7
7
|
| | |
|
|
@@ -20,7 +20,7 @@ fails on drift between the two.
|
|
|
20
20
|
|
|
21
21
|
## Why a fork rather than configuration
|
|
22
22
|
|
|
23
|
-
Every one of the
|
|
23
|
+
Every one of the thirteen patches was tried as configuration first and does not
|
|
24
24
|
exist there. The evidence is a spike against a **running v0.4.15 server plus
|
|
25
25
|
the pinned pi 0.84.1**, with transcripts, kept with the project's design
|
|
26
26
|
records. The short version: several of its findings contradict the OpenViking
|
|
@@ -29,12 +29,13 @@ anything here.
|
|
|
29
29
|
|
|
30
30
|
## The patch series
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
Thirteen numbered behaviour patches, applied in order over the vendored upstream
|
|
33
33
|
files, plus two unnumbered build patches (0000 scaffolding, 0012 typing). They live in `patches/` as
|
|
34
34
|
`git apply`-able diffs — verified by applying the first thirteen, in order, to a
|
|
35
35
|
pristine `v0.4.15` extraction and diffing the result against this directory:
|
|
36
|
-
byte-identical. (0013
|
|
37
|
-
produce; the full pristine-extraction replay has not been re-run
|
|
36
|
+
byte-identical. (0013 and 0014 are checked to apply on the tree the patches
|
|
37
|
+
before them produce; the full pristine-extraction replay has not been re-run
|
|
38
|
+
with them.) (`patches/` and `upstream.json` are meta and are not themselves
|
|
38
39
|
part of the reproduced tree.) Every hunk they add carries a greppable marker:
|
|
39
40
|
|
|
40
41
|
```sh
|
|
@@ -488,6 +489,39 @@ Touches: `index.ts`, `config.ts`, `sync.ts`, `qcr-leaf.ts`, `qcr-shim.ts`
|
|
|
488
489
|
(new), `qcr-hooks.ts` (new), `shared/pending-queue.mjs` and `.d.mts`, `NOTICE`,
|
|
489
490
|
this README.
|
|
490
491
|
|
|
492
|
+
### 0014 — one turn sync in flight per session (`[QCR PATCH 13]`)
|
|
493
|
+
|
|
494
|
+
The `turn_end` hook runs under a deadline (`qcr-hooks.ts`), and a deadline
|
|
495
|
+
that passes returns without cancelling the hook's work. Upstream's
|
|
496
|
+
`syncBranch` then ran on in the background, and under a slow server the next
|
|
497
|
+
turn started a second sync from the same watermark — which only advances once
|
|
498
|
+
a whole sync has finished. One process sent the same messages to one OV
|
|
499
|
+
session several times at once; the server takes a tree lock per session
|
|
500
|
+
append, so they queued on it, timed out client-side (and were queued for
|
|
501
|
+
replay), and the health probes queued behind them. On the bot it was measured
|
|
502
|
+
at up to five appends in flight to one session from its one worker.
|
|
503
|
+
|
|
504
|
+
`SyncManager.syncBranch` now runs one sync at a time. A turn that ends while
|
|
505
|
+
one runs queues its branch, in memory and in order; the running sync then
|
|
506
|
+
sends each queued branch in turn, so the process sends nothing twice and
|
|
507
|
+
nothing at once, and a later turn (a branch switch included) never overwrites
|
|
508
|
+
an earlier one. Upstream's watermark is a count of entries, so a queued branch
|
|
509
|
+
as long as the one before it would read as sent; what was sent is now
|
|
510
|
+
remembered by pi's entry ids, and only entries without ids fall back to the
|
|
511
|
+
count. The offline queue on disk is
|
|
512
|
+
not used for this: it is shared by every worker, it collapses identical
|
|
513
|
+
payloads and replays by write time, all of which would reorder or drop turns.
|
|
514
|
+
|
|
515
|
+
A bot worker disposes its session without `session_shutdown`, so the queued
|
|
516
|
+
turns must go out before the prompt resolves: `agent_end` awaits
|
|
517
|
+
`SyncManager.drain()`, and its deadline in `qcr-hooks.ts` is raised from 1 s
|
|
518
|
+
to turn_end's 30 s. With nothing queued, `drain()` settles at once; with a
|
|
519
|
+
slow server the prompt can resolve up to 30 s later than before. Turns still
|
|
520
|
+
queued when that deadline passes are lost with the worker, as a running sync
|
|
521
|
+
was before this patch; the pi transcript keeps them.
|
|
522
|
+
|
|
523
|
+
Touches: `sync.ts`, `index.ts`, `qcr-hooks.ts`, `NOTICE`, this README.
|
|
524
|
+
|
|
491
525
|
## Rebasing onto a newer upstream
|
|
492
526
|
|
|
493
527
|
```sh
|
|
@@ -405,6 +405,10 @@ async function register(pi: ExtensionAPI, cfg: OpenVikingConfig): Promise<void>
|
|
|
405
405
|
// --- agent_end ---
|
|
406
406
|
pi.on("agent_end", guard("agent_end", async (_event, _ctx) => {
|
|
407
407
|
recall.invalidate();
|
|
408
|
+
// [QCR PATCH 13] The turns queued behind a slow sync go out before the
|
|
409
|
+
// prompt resolves, under this hook's deadline: a worker disposes its
|
|
410
|
+
// session without session_shutdown, so this is the last chance.
|
|
411
|
+
await sync.drain();
|
|
408
412
|
}));
|
|
409
413
|
|
|
410
414
|
// ================================================================
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
diff --git a/extensions/vendor/openviking-pi/NOTICE b/extensions/vendor/openviking-pi/NOTICE
|
|
2
|
+
index 15992e4a..1afc0e01 100644
|
|
3
|
+
--- a/extensions/vendor/openviking-pi/NOTICE
|
|
4
|
+
+++ b/extensions/vendor/openviking-pi/NOTICE
|
|
5
|
+
@@ -60,6 +60,11 @@ Changed by the quickreviewd maintainers, 2026-08-20:
|
|
6
|
+
12. The extension is a factory over a configuration value; the process's
|
|
7
|
+
environment, argv, working directory and home directory are read once, by
|
|
8
|
+
a shim, and every hook runs under a deadline and a catch (2026-10-19).
|
|
9
|
+
+ 13. One turn sync runs at a time per session: a turn that ends while one
|
|
10
|
+
+ runs queues its branch, the running sync sends the queued branches in
|
|
11
|
+
+ order, each entry sent once by its pi entry id, and agent_end waits for
|
|
12
|
+
+ them under turn_end's deadline, so a hook deadline can no longer start a
|
|
13
|
+
+ second sync that re-sends the same messages (2026-10-29).
|
|
14
|
+
|
|
15
|
+
Plus vendoring scaffolding: build-conformance fixes for this repo's stricter
|
|
16
|
+
tsconfig (ambient type declarations while pi was not installed, retired once
|
|
17
|
+
diff --git a/extensions/vendor/openviking-pi/README.md b/extensions/vendor/openviking-pi/README.md
|
|
18
|
+
index 670e38b2..ca289c2d 100644
|
|
19
|
+
--- a/extensions/vendor/openviking-pi/README.md
|
|
20
|
+
+++ b/extensions/vendor/openviking-pi/README.md
|
|
21
|
+
@@ -1,7 +1,7 @@
|
|
22
|
+
# openviking-pi — vendored, patched fork
|
|
23
|
+
|
|
24
|
+
The OpenViking pi extension our sessions load. Vendored, not depended on:
|
|
25
|
+
-upstream ships it as an `examples/` directory, not a package, and we need twelve
|
|
26
|
+
+upstream ships it as an `examples/` directory, not a package, and we need thirteen
|
|
27
|
+
behaviour changes that upstream has no reason to make.
|
|
28
|
+
|
|
29
|
+
| | |
|
|
30
|
+
@@ -20,7 +20,7 @@ fails on drift between the two.
|
|
31
|
+
|
|
32
|
+
## Why a fork rather than configuration
|
|
33
|
+
|
|
34
|
+
-Every one of the twelve patches was tried as configuration first and does not
|
|
35
|
+
+Every one of the thirteen patches was tried as configuration first and does not
|
|
36
|
+
exist there. The evidence is a spike against a **running v0.4.15 server plus
|
|
37
|
+
the pinned pi 0.84.1**, with transcripts, kept with the project's design
|
|
38
|
+
records. The short version: several of its findings contradict the OpenViking
|
|
39
|
+
@@ -29,12 +29,13 @@ anything here.
|
|
40
|
+
|
|
41
|
+
## The patch series
|
|
42
|
+
|
|
43
|
+
-Twelve numbered behaviour patches, applied in order over the vendored upstream
|
|
44
|
+
+Thirteen numbered behaviour patches, applied in order over the vendored upstream
|
|
45
|
+
files, plus two unnumbered build patches (0000 scaffolding, 0012 typing). They live in `patches/` as
|
|
46
|
+
`git apply`-able diffs — verified by applying the first thirteen, in order, to a
|
|
47
|
+
pristine `v0.4.15` extraction and diffing the result against this directory:
|
|
48
|
+
-byte-identical. (0013 is checked to apply on the tree the first thirteen
|
|
49
|
+
-produce; the full pristine-extraction replay has not been re-run with it.) (`patches/` and `upstream.json` are meta and are not themselves
|
|
50
|
+
+byte-identical. (0013 and 0014 are checked to apply on the tree the patches
|
|
51
|
+
+before them produce; the full pristine-extraction replay has not been re-run
|
|
52
|
+
+with them.) (`patches/` and `upstream.json` are meta and are not themselves
|
|
53
|
+
part of the reproduced tree.) Every hunk they add carries a greppable marker:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
@@ -488,6 +489,39 @@ Touches: `index.ts`, `config.ts`, `sync.ts`, `qcr-leaf.ts`, `qcr-shim.ts`
|
|
57
|
+
(new), `qcr-hooks.ts` (new), `shared/pending-queue.mjs` and `.d.mts`, `NOTICE`,
|
|
58
|
+
this README.
|
|
59
|
+
|
|
60
|
+
+### 0014 — one turn sync in flight per session (`[QCR PATCH 13]`)
|
|
61
|
+
+
|
|
62
|
+
+The `turn_end` hook runs under a deadline (`qcr-hooks.ts`), and a deadline
|
|
63
|
+
+that passes returns without cancelling the hook's work. Upstream's
|
|
64
|
+
+`syncBranch` then ran on in the background, and under a slow server the next
|
|
65
|
+
+turn started a second sync from the same watermark — which only advances once
|
|
66
|
+
+a whole sync has finished. One process sent the same messages to one OV
|
|
67
|
+
+session several times at once; the server takes a tree lock per session
|
|
68
|
+
+append, so they queued on it, timed out client-side (and were queued for
|
|
69
|
+
+replay), and the health probes queued behind them. On the bot it was measured
|
|
70
|
+
+at up to five appends in flight to one session from its one worker.
|
|
71
|
+
+
|
|
72
|
+
+`SyncManager.syncBranch` now runs one sync at a time. A turn that ends while
|
|
73
|
+
+one runs queues its branch, in memory and in order; the running sync then
|
|
74
|
+
+sends each queued branch in turn, so the process sends nothing twice and
|
|
75
|
+
+nothing at once, and a later turn (a branch switch included) never overwrites
|
|
76
|
+
+an earlier one. Upstream's watermark is a count of entries, so a queued branch
|
|
77
|
+
+as long as the one before it would read as sent; what was sent is now
|
|
78
|
+
+remembered by pi's entry ids, and only entries without ids fall back to the
|
|
79
|
+
+count. The offline queue on disk is
|
|
80
|
+
+not used for this: it is shared by every worker, it collapses identical
|
|
81
|
+
+payloads and replays by write time, all of which would reorder or drop turns.
|
|
82
|
+
+
|
|
83
|
+
+A bot worker disposes its session without `session_shutdown`, so the queued
|
|
84
|
+
+turns must go out before the prompt resolves: `agent_end` awaits
|
|
85
|
+
+`SyncManager.drain()`, and its deadline in `qcr-hooks.ts` is raised from 1 s
|
|
86
|
+
+to turn_end's 30 s. With nothing queued, `drain()` settles at once; with a
|
|
87
|
+
+slow server the prompt can resolve up to 30 s later than before. Turns still
|
|
88
|
+
+queued when that deadline passes are lost with the worker, as a running sync
|
|
89
|
+
+was before this patch; the pi transcript keeps them.
|
|
90
|
+
+
|
|
91
|
+
+Touches: `sync.ts`, `index.ts`, `qcr-hooks.ts`, `NOTICE`, this README.
|
|
92
|
+
+
|
|
93
|
+
## Rebasing onto a newer upstream
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
diff --git a/extensions/vendor/openviking-pi/index.ts b/extensions/vendor/openviking-pi/index.ts
|
|
97
|
+
index 8bdc65d9..1b60eeee 100644
|
|
98
|
+
--- a/extensions/vendor/openviking-pi/index.ts
|
|
99
|
+
+++ b/extensions/vendor/openviking-pi/index.ts
|
|
100
|
+
@@ -405,6 +405,10 @@ async function register(pi: ExtensionAPI, cfg: OpenVikingConfig): Promise<void>
|
|
101
|
+
// --- agent_end ---
|
|
102
|
+
pi.on("agent_end", guard("agent_end", async (_event, _ctx) => {
|
|
103
|
+
recall.invalidate();
|
|
104
|
+
+ // [QCR PATCH 13] The turns queued behind a slow sync go out before the
|
|
105
|
+
+ // prompt resolves, under this hook's deadline: a worker disposes its
|
|
106
|
+
+ // session without session_shutdown, so this is the last chance.
|
|
107
|
+
+ await sync.drain();
|
|
108
|
+
}));
|
|
109
|
+
|
|
110
|
+
// ================================================================
|
|
111
|
+
diff --git a/extensions/vendor/openviking-pi/qcr-hooks.ts b/extensions/vendor/openviking-pi/qcr-hooks.ts
|
|
112
|
+
index 8979e729..89d8281c 100644
|
|
113
|
+
--- a/extensions/vendor/openviking-pi/qcr-hooks.ts
|
|
114
|
+
+++ b/extensions/vendor/openviking-pi/qcr-hooks.ts
|
|
115
|
+
@@ -32,7 +32,9 @@ export const HOOK_TIMEOUT_MS: Readonly<Record<HookName, number>> = {
|
|
116
|
+
tool_call: 2_000,
|
|
117
|
+
turn_end: 30_000,
|
|
118
|
+
session_shutdown: 5_000,
|
|
119
|
+
- agent_end: 1_000,
|
|
120
|
+
+ // [QCR PATCH 13] agent_end drains the turn syncs queued behind a slow one,
|
|
121
|
+
+ // so it gets turn_end's deadline; with nothing queued it returns at once.
|
|
122
|
+
+ agent_end: 30_000,
|
|
123
|
+
command: 15_000,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
diff --git a/extensions/vendor/openviking-pi/sync.ts b/extensions/vendor/openviking-pi/sync.ts
|
|
127
|
+
index 037d6306..ddfe651e 100644
|
|
128
|
+
--- a/extensions/vendor/openviking-pi/sync.ts
|
|
129
|
+
+++ b/extensions/vendor/openviking-pi/sync.ts
|
|
130
|
+
@@ -38,6 +38,14 @@ export class SyncManager {
|
|
131
|
+
private syncedEntryCount = 0;
|
|
132
|
+
private queue: PendingQueueConfig;
|
|
133
|
+
private debugLog: (message: string) => void;
|
|
134
|
+
+ // [QCR PATCH 13] One turn sync in flight per session. A turn that ends
|
|
135
|
+
+ // while one runs queues its branch here, in order; the running sync sends
|
|
136
|
+
+ // each in turn. `idle` settles when the queue is empty again.
|
|
137
|
+
+ private syncing = false;
|
|
138
|
+
+ private waiting: any[][] = [];
|
|
139
|
+
+ private idle: Promise<void> = Promise.resolve();
|
|
140
|
+
+ // [QCR PATCH 13] The ids of the entries already sent (see syncOnce).
|
|
141
|
+
+ private sentIds = new Set<string>();
|
|
142
|
+
|
|
143
|
+
constructor(client: OVClient, config: OVConfig, opts: SyncOptions = {}) {
|
|
144
|
+
this.client = client;
|
|
145
|
+
@@ -52,6 +60,7 @@ export class SyncManager {
|
|
146
|
+
restoreWatermark(n: number): void {
|
|
147
|
+
const next = Math.max(0, Math.floor(Number(n) || 0));
|
|
148
|
+
this.syncedEntryCount = next;
|
|
149
|
+
+ this.sentIds.clear(); // [QCR PATCH 13] the next sync re-seeds from `next`
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
@@ -110,8 +119,54 @@ export class SyncManager {
|
|
154
|
+
return countUndeliveredForSession(pending, this.ovSessionId) === 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
+ /**
|
|
158
|
+
+ * [QCR PATCH 13] The turn_end hook's deadline returns without cancelling
|
|
159
|
+
+ * this sync, so under a slow server the next turn used to start a second one
|
|
160
|
+
+ * from the same watermark: the same messages went to one session several
|
|
161
|
+
+ * times at once and queued on its lock. Now one sync runs at a time. A turn
|
|
162
|
+
+ * that ends while it runs queues its branch, and the running sync sends the
|
|
163
|
+
+ * queued branches one after another, each from the watermark the one before
|
|
164
|
+
+ * it left: nothing is sent twice or at once, and no turn is overwritten by a
|
|
165
|
+
+ * later one. `drain()` (awaited at agent_end) waits for the queue to empty.
|
|
166
|
+
+ */
|
|
167
|
+
async syncBranch(branch: any[]): Promise<SyncBranchResult> {
|
|
168
|
+
if (!this.ovSessionId) return { added: 0, tokens: 0, allDelivered: true };
|
|
169
|
+
+ if (this.syncing) {
|
|
170
|
+
+ this.waiting.push(branch);
|
|
171
|
+
+ return { added: 0, tokens: 0, allDelivered: false };
|
|
172
|
+
+ }
|
|
173
|
+
+ this.syncing = true;
|
|
174
|
+
+ let done!: () => void;
|
|
175
|
+
+ this.idle = new Promise<void>((resolve) => (done = resolve));
|
|
176
|
+
+ try {
|
|
177
|
+
+ const total = await this.syncOnce(branch);
|
|
178
|
+
+ for (let next = this.waiting.shift(); next; next = this.waiting.shift()) {
|
|
179
|
+
+ const r = await this.syncOnce(next);
|
|
180
|
+
+ total.added += r.added;
|
|
181
|
+
+ total.tokens += r.tokens;
|
|
182
|
+
+ total.allDelivered = total.allDelivered && r.allDelivered;
|
|
183
|
+
+ }
|
|
184
|
+
+ return total;
|
|
185
|
+
+ } finally {
|
|
186
|
+
+ this.syncing = false;
|
|
187
|
+
+ done();
|
|
188
|
+
+ }
|
|
189
|
+
+ }
|
|
190
|
+
+
|
|
191
|
+
+ /** [QCR PATCH 13] Settles once no sync runs and no turn waits for one. */
|
|
192
|
+
+ drain(): Promise<void> {
|
|
193
|
+
+ return this.idle;
|
|
194
|
+
+ }
|
|
195
|
+
+
|
|
196
|
+
+ private async syncOnce(branch: any[]): Promise<SyncBranchResult> {
|
|
197
|
+
+ // [QCR PATCH 13] A queued branch can be as long as the one sent before it
|
|
198
|
+
+ // and share none of its entries (a branch switch), so a length watermark
|
|
199
|
+
+ // would count its entries as sent. pi gives every entry an id: what was
|
|
200
|
+
+ // sent is remembered by id. The watermark below stays for entries without.
|
|
201
|
+
+ const ids = branch.map((entry) => entry?.id);
|
|
202
|
+
+ if (branch.length > 0 && ids.every((id) => typeof id === "string")) {
|
|
203
|
+
+ return this.syncById(branch, ids);
|
|
204
|
+
+ }
|
|
205
|
+
|
|
206
|
+
const extracted = extractBranchCapturePayloads(branch, this.syncedEntryCount, this.config);
|
|
207
|
+
if (extracted.resetWatermark) this.syncedEntryCount = 0;
|
|
208
|
+
@@ -135,6 +190,31 @@ export class SyncManager {
|
|
209
|
+
return { added, tokens, allDelivered };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
+ /** [QCR PATCH 13] Sends each entry of `branch` not sent before, in order. */
|
|
213
|
+
+ private async syncById(branch: any[], ids: string[]): Promise<SyncBranchResult> {
|
|
214
|
+
+ // A restored watermark counts the branch's first entries as sent.
|
|
215
|
+
+ if (this.sentIds.size === 0) {
|
|
216
|
+
+ for (const id of ids.slice(0, this.syncedEntryCount)) this.sentIds.add(id);
|
|
217
|
+
+ }
|
|
218
|
+
+ let added = 0;
|
|
219
|
+
+ let tokens = 0;
|
|
220
|
+
+ let allDelivered = true;
|
|
221
|
+
+ for (const [i, entry] of branch.entries()) {
|
|
222
|
+
+ const id = ids[i]!;
|
|
223
|
+
+ if (this.sentIds.has(id)) continue;
|
|
224
|
+
+ for (const payload of extractBranchCapturePayloads([entry], 0, this.config).payloads) {
|
|
225
|
+
+ const result = await this.addPayload(payload);
|
|
226
|
+
+ if (!result.accepted) return { added, tokens, allDelivered };
|
|
227
|
+
+ added++;
|
|
228
|
+
+ tokens += estimatePayloadTokens(payload);
|
|
229
|
+
+ allDelivered = allDelivered && result.delivered;
|
|
230
|
+
+ }
|
|
231
|
+
+ this.sentIds.add(id);
|
|
232
|
+
+ }
|
|
233
|
+
+ this.syncedEntryCount = branch.length;
|
|
234
|
+
+ return { added, tokens, allDelivered };
|
|
235
|
+
+ }
|
|
236
|
+
+
|
|
237
|
+
async addPayload(payload: any): Promise<AddPayloadResult> {
|
|
238
|
+
if (!this.ovSessionId) return { accepted: false, delivered: false };
|
|
239
|
+
const ok = await this.client.addMessagePayload(this.ovSessionId, payload);
|
|
@@ -32,7 +32,9 @@ export const HOOK_TIMEOUT_MS: Readonly<Record<HookName, number>> = {
|
|
|
32
32
|
tool_call: 2_000,
|
|
33
33
|
turn_end: 30_000,
|
|
34
34
|
session_shutdown: 5_000,
|
|
35
|
-
agent_end
|
|
35
|
+
// [QCR PATCH 13] agent_end drains the turn syncs queued behind a slow one,
|
|
36
|
+
// so it gets turn_end's deadline; with nothing queued it returns at once.
|
|
37
|
+
agent_end: 30_000,
|
|
36
38
|
command: 15_000,
|
|
37
39
|
};
|
|
38
40
|
|
|
@@ -38,6 +38,14 @@ export class SyncManager {
|
|
|
38
38
|
private syncedEntryCount = 0;
|
|
39
39
|
private queue: PendingQueueConfig;
|
|
40
40
|
private debugLog: (message: string) => void;
|
|
41
|
+
// [QCR PATCH 13] One turn sync in flight per session. A turn that ends
|
|
42
|
+
// while one runs queues its branch here, in order; the running sync sends
|
|
43
|
+
// each in turn. `idle` settles when the queue is empty again.
|
|
44
|
+
private syncing = false;
|
|
45
|
+
private waiting: any[][] = [];
|
|
46
|
+
private idle: Promise<void> = Promise.resolve();
|
|
47
|
+
// [QCR PATCH 13] The ids of the entries already sent (see syncOnce).
|
|
48
|
+
private sentIds = new Set<string>();
|
|
41
49
|
|
|
42
50
|
constructor(client: OVClient, config: OVConfig, opts: SyncOptions = {}) {
|
|
43
51
|
this.client = client;
|
|
@@ -52,6 +60,7 @@ export class SyncManager {
|
|
|
52
60
|
restoreWatermark(n: number): void {
|
|
53
61
|
const next = Math.max(0, Math.floor(Number(n) || 0));
|
|
54
62
|
this.syncedEntryCount = next;
|
|
63
|
+
this.sentIds.clear(); // [QCR PATCH 13] the next sync re-seeds from `next`
|
|
55
64
|
}
|
|
56
65
|
|
|
57
66
|
/**
|
|
@@ -110,8 +119,54 @@ export class SyncManager {
|
|
|
110
119
|
return countUndeliveredForSession(pending, this.ovSessionId) === 0;
|
|
111
120
|
}
|
|
112
121
|
|
|
122
|
+
/**
|
|
123
|
+
* [QCR PATCH 13] The turn_end hook's deadline returns without cancelling
|
|
124
|
+
* this sync, so under a slow server the next turn used to start a second one
|
|
125
|
+
* from the same watermark: the same messages went to one session several
|
|
126
|
+
* times at once and queued on its lock. Now one sync runs at a time. A turn
|
|
127
|
+
* that ends while it runs queues its branch, and the running sync sends the
|
|
128
|
+
* queued branches one after another, each from the watermark the one before
|
|
129
|
+
* it left: nothing is sent twice or at once, and no turn is overwritten by a
|
|
130
|
+
* later one. `drain()` (awaited at agent_end) waits for the queue to empty.
|
|
131
|
+
*/
|
|
113
132
|
async syncBranch(branch: any[]): Promise<SyncBranchResult> {
|
|
114
133
|
if (!this.ovSessionId) return { added: 0, tokens: 0, allDelivered: true };
|
|
134
|
+
if (this.syncing) {
|
|
135
|
+
this.waiting.push(branch);
|
|
136
|
+
return { added: 0, tokens: 0, allDelivered: false };
|
|
137
|
+
}
|
|
138
|
+
this.syncing = true;
|
|
139
|
+
let done!: () => void;
|
|
140
|
+
this.idle = new Promise<void>((resolve) => (done = resolve));
|
|
141
|
+
try {
|
|
142
|
+
const total = await this.syncOnce(branch);
|
|
143
|
+
for (let next = this.waiting.shift(); next; next = this.waiting.shift()) {
|
|
144
|
+
const r = await this.syncOnce(next);
|
|
145
|
+
total.added += r.added;
|
|
146
|
+
total.tokens += r.tokens;
|
|
147
|
+
total.allDelivered = total.allDelivered && r.allDelivered;
|
|
148
|
+
}
|
|
149
|
+
return total;
|
|
150
|
+
} finally {
|
|
151
|
+
this.syncing = false;
|
|
152
|
+
done();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** [QCR PATCH 13] Settles once no sync runs and no turn waits for one. */
|
|
157
|
+
drain(): Promise<void> {
|
|
158
|
+
return this.idle;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private async syncOnce(branch: any[]): Promise<SyncBranchResult> {
|
|
162
|
+
// [QCR PATCH 13] A queued branch can be as long as the one sent before it
|
|
163
|
+
// and share none of its entries (a branch switch), so a length watermark
|
|
164
|
+
// would count its entries as sent. pi gives every entry an id: what was
|
|
165
|
+
// sent is remembered by id. The watermark below stays for entries without.
|
|
166
|
+
const ids = branch.map((entry) => entry?.id);
|
|
167
|
+
if (branch.length > 0 && ids.every((id) => typeof id === "string")) {
|
|
168
|
+
return this.syncById(branch, ids);
|
|
169
|
+
}
|
|
115
170
|
|
|
116
171
|
const extracted = extractBranchCapturePayloads(branch, this.syncedEntryCount, this.config);
|
|
117
172
|
if (extracted.resetWatermark) this.syncedEntryCount = 0;
|
|
@@ -135,6 +190,31 @@ export class SyncManager {
|
|
|
135
190
|
return { added, tokens, allDelivered };
|
|
136
191
|
}
|
|
137
192
|
|
|
193
|
+
/** [QCR PATCH 13] Sends each entry of `branch` not sent before, in order. */
|
|
194
|
+
private async syncById(branch: any[], ids: string[]): Promise<SyncBranchResult> {
|
|
195
|
+
// A restored watermark counts the branch's first entries as sent.
|
|
196
|
+
if (this.sentIds.size === 0) {
|
|
197
|
+
for (const id of ids.slice(0, this.syncedEntryCount)) this.sentIds.add(id);
|
|
198
|
+
}
|
|
199
|
+
let added = 0;
|
|
200
|
+
let tokens = 0;
|
|
201
|
+
let allDelivered = true;
|
|
202
|
+
for (const [i, entry] of branch.entries()) {
|
|
203
|
+
const id = ids[i]!;
|
|
204
|
+
if (this.sentIds.has(id)) continue;
|
|
205
|
+
for (const payload of extractBranchCapturePayloads([entry], 0, this.config).payloads) {
|
|
206
|
+
const result = await this.addPayload(payload);
|
|
207
|
+
if (!result.accepted) return { added, tokens, allDelivered };
|
|
208
|
+
added++;
|
|
209
|
+
tokens += estimatePayloadTokens(payload);
|
|
210
|
+
allDelivered = allDelivered && result.delivered;
|
|
211
|
+
}
|
|
212
|
+
this.sentIds.add(id);
|
|
213
|
+
}
|
|
214
|
+
this.syncedEntryCount = branch.length;
|
|
215
|
+
return { added, tokens, allDelivered };
|
|
216
|
+
}
|
|
217
|
+
|
|
138
218
|
async addPayload(payload: any): Promise<AddPayloadResult> {
|
|
139
219
|
if (!this.ovSessionId) return { accepted: false, delivered: false };
|
|
140
220
|
const ok = await this.client.addMessagePayload(this.ovSessionId, payload);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quickchatai/quickreview",
|
|
3
|
-
"version": "1.2
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"//private": "The root is the published `qcr` CLI: the whole app (TUI, daemon, companion, every verb), installed with npm, pnpm or bun. It depends on the pi plugin (packages/pi-plugin) at its exact version and on pi at the exact pins below. Publishing is a release step of its own, never part of a build.",
|
|
6
6
|
"description": "A 24/7 AI code reviewer for GitHub: a deterministic daemon driving pi review sessions, with an operator TUI and a local qcr review",
|
|
@@ -94,11 +94,10 @@
|
|
|
94
94
|
"prepack": "npm run build",
|
|
95
95
|
"watch": "node scripts/tsc-watch.mjs -b --watch --preserveWatchOutput",
|
|
96
96
|
"dev": "bash scripts/dev-tui.sh",
|
|
97
|
-
"onboard": "bash scripts/qcr onboard",
|
|
98
97
|
"test": "vitest run",
|
|
99
98
|
"//canary:pi": "The offline pi canaries (tests/pi-canary/): the real pi SDK at the pinned version on pi-ai's faux provider, so no network and no cost. They also run inside `npm test`; this is the named entry point CI requires whenever the pi dependencies change (a bump is gated by it).",
|
|
100
99
|
"canary:pi": "vitest run tests/pi-canary",
|
|
101
|
-
"//canary:pi:range": "The same canaries on each pi version the plugin's peer range (packages/pi-plugin/package.json) should hold, refusing one outside it: no argument runs the installed pi; `-- --install 1.4.x` fetches another first (the registry
|
|
100
|
+
"//canary:pi:range": "The same canaries on each pi version the plugin's peer range (packages/pi-plugin/package.json) should hold, refusing one outside it: no argument runs the installed pi; `-- --install 1.4.x` fetches another first (the registry). Run it by hand before a version joins the peer range; CI runs only the installed pi's canaries. Needs the build.",
|
|
102
101
|
"canary:pi:range": "node scripts/pi-canary-range.mjs",
|
|
103
102
|
"//lint": "Deliberately NOT chained into `test` or `build`: scripts/dev-tui.sh blocks on the build before the TUI paints (ruling 5), and an ESLint pass there is seconds of blank terminal. CI runs it as its own step.",
|
|
104
103
|
"lint": "node --max-old-space-size=4096 node_modules/eslint/bin/eslint.js .",
|
|
@@ -116,7 +115,7 @@
|
|
|
116
115
|
},
|
|
117
116
|
"dependencies": {
|
|
118
117
|
"@earendil-works/pi-ai": "1.4.0",
|
|
119
|
-
"@quickchatai/pi-quickreview": "1.2
|
|
118
|
+
"@quickchatai/pi-quickreview": "1.3.2",
|
|
120
119
|
"@earendil-works/pi-coding-agent": "1.4.0",
|
|
121
120
|
"better-sqlite3": "13.0.3",
|
|
122
121
|
"smol-toml": "1.9.1"
|
|
@@ -132,6 +131,9 @@
|
|
|
132
131
|
"vitest": "5.0.5",
|
|
133
132
|
"yaml": "2.9.1"
|
|
134
133
|
},
|
|
134
|
+
"overrides": {
|
|
135
|
+
"undici": "8.12.2"
|
|
136
|
+
},
|
|
135
137
|
"qcrPi": {
|
|
136
138
|
"//": "Named qcrPi, never `pi`: pi reads a package.json `pi` key as a package manifest, and this root is not a pi package (the plugin is packages/pi-plugin). pi and pi-ai are exact dependencies above (types for the extensions, and the VM boot's plain `npm ci` installs them beside the global CLI the image carries). This block is the mirror a bare checkout without config.toml reads: package and version must equal the dependencies, the lockfile, config.toml [pi], ansible group_vars and every Dockerfile ARG (tests/fences.test.ts [T12], tests/docker-daemon.test.ts). Lens sessions run through our own lens tools (src/engine/lens/), not a pi subagent extension, so no extra extension pin exists.",
|
|
137
139
|
"package": "@earendil-works/pi-coding-agent",
|
package/prompts/onboarding.md
CHANGED
|
@@ -94,20 +94,21 @@ the own-account route: it is skipped there.
|
|
|
94
94
|
`onboard_go_live` are marked "after setup": setup finishes without them. Do
|
|
95
95
|
not run them here unless the person asks. Say plainly that the installation
|
|
96
96
|
stays in dry-run (every intended GitHub write is recorded, nothing is
|
|
97
|
-
posted), and that going live is their decision later
|
|
98
|
-
|
|
97
|
+
posted), and that going live is their decision later, from the home
|
|
98
|
+
(Advanced → "Dry-run or live"), once a review has shown them what it says.
|
|
99
99
|
|
|
100
100
|
## Finishing
|
|
101
101
|
|
|
102
102
|
When every setup step reports done (the "after setup" ones need not be), say
|
|
103
103
|
that `finish_and_exit` will re-verify the whole checklist one last time and,
|
|
104
|
-
only if all green, close this session
|
|
105
|
-
|
|
104
|
+
only if all green, close this session: the home then lists the repository's
|
|
105
|
+
pull requests, and they select one and press `alt+r` to review it now. If it
|
|
106
|
+
returns red checks, that is the answer: explain them, fix
|
|
106
107
|
what can be fixed through the step tools, and try again. Never present
|
|
107
108
|
onboarding as finished while anything is red, and never tell the operator to
|
|
108
109
|
quit around a red check.
|
|
109
110
|
|
|
110
|
-
Before `finish_and_exit`, pass on what comes next: the first review is `
|
|
111
|
-
a pull request
|
|
112
|
-
the
|
|
111
|
+
Before `finish_and_exit`, pass on what comes next: the first review is `alt+r`
|
|
112
|
+
on a pull request in the home; then tune the reviewer to the repository with
|
|
113
|
+
the home's "Tune to this repo" (`t` on the panel). Until then the reviewer knows nothing about this codebase. It is a
|
|
113
114
|
recommendation, once — never start it from here.
|