@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/wire.ts
CHANGED
|
@@ -30,7 +30,7 @@ import { Archiver } from "./archive/archiver.js";
|
|
|
30
30
|
import { piGlobalSessionsDir } from "./archive/piglobal.js";
|
|
31
31
|
import { runDirOf, toolUidSplit } from "./ipc/capability.js";
|
|
32
32
|
import { PermitBroker } from "./ipc/permits.js";
|
|
33
|
-
import { LoginRefreshSupervisor } from "./loginrefresh/supervisor.js";
|
|
33
|
+
import { LoginRefreshSupervisor, loginDirOf } from "./loginrefresh/supervisor.js";
|
|
34
34
|
import { pendingArchiveStatus } from "./archive/status.js";
|
|
35
35
|
import { Learner, learnerKnobsOf } from "./learner/learner.js";
|
|
36
36
|
import { renderNoPacketSection } from "./learner/packet.js";
|
|
@@ -770,6 +770,9 @@ export function createProductionDaemon(parts: ProductionParts = buildParts()): D
|
|
|
770
770
|
// src/workerworld.ts. No config key picks the tmux route, and nothing births a
|
|
771
771
|
// `pr-<n>` tmux session any more; `sessions` serves paths, worktrees and the replay harness.
|
|
772
772
|
const agentDir = process.env["PI_CODING_AGENT_DIR"]?.trim() || join(homedir(), ".pi", "agent");
|
|
773
|
+
// The login itself: the agent dir, or the host's shared login on a single
|
|
774
|
+
// box (one login per box): the refresher owns it and every worker borrows it.
|
|
775
|
+
const loginDir = loginDirOf(process.env, agentDir);
|
|
773
776
|
const workerWorld = buildWorkerWorld({
|
|
774
777
|
cfg,
|
|
775
778
|
root,
|
|
@@ -783,6 +786,7 @@ export function createProductionDaemon(parts: ProductionParts = buildParts()): D
|
|
|
783
786
|
...(parts.ovSessions ? { ovSessions: parts.ovSessions } : {}),
|
|
784
787
|
permits: lensPermits,
|
|
785
788
|
agentDir,
|
|
789
|
+
loginDir,
|
|
786
790
|
pump: () => daemon.pump(),
|
|
787
791
|
loginGate: {
|
|
788
792
|
paused: () => loginRefresh?.paused() ?? false,
|
|
@@ -1627,7 +1631,7 @@ export function createProductionDaemon(parts: ProductionParts = buildParts()): D
|
|
|
1627
1631
|
if (cfg.login_refresh.enabled) {
|
|
1628
1632
|
loginRefresh = new LoginRefreshSupervisor({
|
|
1629
1633
|
repoRoot: root,
|
|
1630
|
-
agentDir,
|
|
1634
|
+
agentDir: loginDir,
|
|
1631
1635
|
marginMin: cfg.login_refresh.margin_min,
|
|
1632
1636
|
log,
|
|
1633
1637
|
// One anomaly per failure episode, through the one durable funnel.
|
|
@@ -1832,8 +1836,9 @@ export function createProductionDaemon(parts: ProductionParts = buildParts()): D
|
|
|
1832
1836
|
// pump starts nothing, recheck/ping-pong/EOD hold — in-flight rounds
|
|
1833
1837
|
// finish naturally. Off is the manual lift; the automatic one is the
|
|
1834
1838
|
// next boot's first healthy reconcile pass (daemon.ts) — except for
|
|
1835
|
-
// the operator's HOLD, which only an explicit off lifts
|
|
1836
|
-
//
|
|
1839
|
+
// the operator's HOLD, which only an explicit off lifts, and the worker
|
|
1840
|
+
// breaker's drain, which lapses on its own (src/workerbreaker.ts).
|
|
1841
|
+
// Never touches GitHub, never goes near the gate.
|
|
1837
1842
|
drain(on, issuer, forMin, deliveryKey, hold) {
|
|
1838
1843
|
return drainLever(
|
|
1839
1844
|
{ ...leverDeps, deliveryKey },
|
package/src/workerbreaker.ts
CHANGED
|
@@ -4,14 +4,22 @@
|
|
|
4
4
|
//
|
|
5
5
|
// - PER PR. A worker's failure is a non-zero exit, a `failed` event over the IPC,
|
|
6
6
|
// or a deadline kill (wall clock, stage or silence). After `parkAfter`
|
|
7
|
-
// consecutive ones the PR is PARKED
|
|
7
|
+
// consecutive ones the PR is PARKED (a provider network drop counts but
|
|
8
|
+
// never parks by itself: it says nothing about the PR): no further spawn, a `worker_crash_loop`
|
|
8
9
|
// anomaly (to the companion inbox), the last error in status.json and a TUI
|
|
9
10
|
// alert. A worker that served its round resets the PR's count. The operator
|
|
10
11
|
// unparks with `rerun-round` or `restart-session` (src/operator-recovery.ts).
|
|
11
12
|
// - GLOBAL. `drainAfterPrs` PRs failing within `windowMs`, with no worker success
|
|
12
13
|
// in between, set the existing drain valve with the reason `worker_failures`.
|
|
13
|
-
//
|
|
14
|
-
//
|
|
14
|
+
// The drain heals itself: it lapses after `healAfterMs`, the queue retries,
|
|
15
|
+
// and if workers fail again it drains again, each time waiting twice as long
|
|
16
|
+
// up to `healMaxMs`. A served round (or the operator's `drain off`, which
|
|
17
|
+
// lifts it at once) starts the wait from `healAfterMs` again. A boot's first
|
|
18
|
+
// healthy reconcile pass does not lift it early, and an operator's own
|
|
19
|
+
// `drain on` over it stands until the operator lifts it. A PR's FIRST
|
|
20
|
+
// failure that is the provider's network dropping (`fetch failed`, a reset
|
|
21
|
+
// socket) does not feed the drain: one blip fails every concurrent worker at
|
|
22
|
+
// the same instant, and each of them is retried. A retry that fails again counts.
|
|
15
23
|
// - EVERY HOST. `gate` is the one question each host asks before it starts a
|
|
16
24
|
// round worker, an answer worker or a debug round: the bot
|
|
17
25
|
// is drained, or the PR is parked, and the answer is no. A takeover
|
|
@@ -29,12 +37,18 @@ export interface BreakerLimits {
|
|
|
29
37
|
drainAfterPrs: number;
|
|
30
38
|
/** The window those PRs' latest failures must fall in. */
|
|
31
39
|
windowMs: number;
|
|
40
|
+
/** How long the breaker's first drain stands before it lapses and the queue retries. */
|
|
41
|
+
healAfterMs: number;
|
|
42
|
+
/** The longest a drain waits, however often it has tripped. */
|
|
43
|
+
healMaxMs: number;
|
|
32
44
|
}
|
|
33
45
|
|
|
34
46
|
export const DEFAULT_BREAKER_LIMITS: BreakerLimits = {
|
|
35
47
|
parkAfter: 2,
|
|
36
48
|
drainAfterPrs: 3,
|
|
37
49
|
windowMs: 15 * 60_000,
|
|
50
|
+
healAfterMs: 15 * 60_000,
|
|
51
|
+
healMaxMs: 4 * 60 * 60_000,
|
|
38
52
|
};
|
|
39
53
|
|
|
40
54
|
export type SpawnVerdict = { ok: true } | { ok: false; why: "draining" | "parked"; reason: string };
|
|
@@ -48,6 +62,15 @@ export interface WorkerFailure {
|
|
|
48
62
|
|
|
49
63
|
const ERROR_CHARS = 300;
|
|
50
64
|
|
|
65
|
+
/** The provider's transport dropped the turn (src/botworker.ts words it), as
|
|
66
|
+
* opposed to the provider answering with an error or refusing the login. */
|
|
67
|
+
const PROVIDER_TRANSPORT =
|
|
68
|
+
/the provider failed the turn: .*\b(fetch failed|ECONNRESET|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|socket hang up)\b/i;
|
|
69
|
+
|
|
70
|
+
function isProviderTransportFailure(error: string): boolean {
|
|
71
|
+
return PROVIDER_TRANSPORT.test(error);
|
|
72
|
+
}
|
|
73
|
+
|
|
51
74
|
export class WorkerBreaker {
|
|
52
75
|
readonly limits: BreakerLimits;
|
|
53
76
|
private readonly now: () => number;
|
|
@@ -96,11 +119,15 @@ export class WorkerBreaker {
|
|
|
96
119
|
failure(pr: number, commandId: string, error: string): WorkerFailure {
|
|
97
120
|
const at = this.iso();
|
|
98
121
|
const last = error.replace(/\s+/g, " ").trim().slice(0, ERROR_CHARS) || "no reason given";
|
|
122
|
+
const firstFailure = (this.store.workerBreaker(pr)?.failures ?? 0) === 0;
|
|
123
|
+
const transport = isProviderTransportFailure(last);
|
|
99
124
|
const res = this.store.recordWorkerFailure(pr, {
|
|
100
125
|
commandId,
|
|
101
126
|
error: last,
|
|
102
127
|
at,
|
|
103
128
|
parkAfter: this.limits.parkAfter,
|
|
129
|
+
drain: !(firstFailure && transport),
|
|
130
|
+
park: !transport,
|
|
104
131
|
});
|
|
105
132
|
if (res.newlyParked) {
|
|
106
133
|
const detail = `PR #${pr} is parked after ${res.failures} consecutive worker failures; last: ${last}`;
|
|
@@ -126,12 +153,20 @@ export class WorkerBreaker {
|
|
|
126
153
|
const since = new Date(this.now() - this.limits.windowMs).toISOString();
|
|
127
154
|
const prs = this.store.failedWorkerPrsSince(since);
|
|
128
155
|
if (prs.length < this.limits.drainAfterPrs) return false;
|
|
156
|
+
// A standing drain, the breaker's or a hand-set one, is never extended here.
|
|
157
|
+
if (this.store.drainState().draining) return false;
|
|
158
|
+
const waitMs = Math.min(
|
|
159
|
+
this.limits.healAfterMs * 2 ** this.store.workerBreakerTrips(),
|
|
160
|
+
this.limits.healMaxMs,
|
|
161
|
+
);
|
|
162
|
+
const lapses = new Date(this.now() + waitMs).toISOString();
|
|
129
163
|
const res = this.store.engageDrain(
|
|
130
|
-
{ issuer: "operator", expires_at:
|
|
164
|
+
{ issuer: "operator", expires_at: lapses },
|
|
131
165
|
WORKER_FAILURES_DRAIN_REASON,
|
|
132
166
|
);
|
|
133
167
|
if (res.outcome === "unchanged") return false;
|
|
134
|
-
|
|
168
|
+
this.store.recordWorkerBreakerTrip();
|
|
169
|
+
const detail = `workers failed for ${prs.length} PRs (${prs.map((n) => `#${n}`).join(", ")}) within ${Math.round(this.limits.windowMs / 60_000)} min with no success; the bot is draining (${WORKER_FAILURES_DRAIN_REASON}) and retries at ${lapses} (in ${Math.round(waitMs / 60_000)} min); drain off lifts it now`;
|
|
135
170
|
this.log(`worker_failures drain — ${detail}`);
|
|
136
171
|
this.store.logEvent("drain_on", null, {
|
|
137
172
|
by: "system",
|
|
@@ -139,6 +174,7 @@ export class WorkerBreaker {
|
|
|
139
174
|
outcome: res.outcome,
|
|
140
175
|
prs,
|
|
141
176
|
at,
|
|
177
|
+
expires_at: lapses,
|
|
142
178
|
});
|
|
143
179
|
this.store.logAnomaly(
|
|
144
180
|
"worker_crash_loop",
|
package/src/workerworld.ts
CHANGED
|
@@ -27,6 +27,7 @@ import type { Config } from "./config.js";
|
|
|
27
27
|
import { DebugRoundManager } from "./debuground.js";
|
|
28
28
|
import type { FileOutcome } from "./improvements.js";
|
|
29
29
|
import { identityVars } from "@quickchatai/pi-quickreview/src/include.js";
|
|
30
|
+
import { reviewProviders } from "@quickchatai/pi-quickreview/src/modelroute.js";
|
|
30
31
|
import {
|
|
31
32
|
CapabilityStore,
|
|
32
33
|
LaunchFiles,
|
|
@@ -89,8 +90,12 @@ export interface WorkerWorldOptions {
|
|
|
89
90
|
sessions: PiSessionManager;
|
|
90
91
|
ovSessions?: OvSessionLifecycle;
|
|
91
92
|
permits: PermitBroker;
|
|
92
|
-
/** pi's agent dir:
|
|
93
|
+
/** pi's agent dir: models.json, settings and the built lens agents. */
|
|
93
94
|
agentDir: string;
|
|
95
|
+
/** Where the login lives (the refresher keeps it): the agent dir, unless
|
|
96
|
+
* the single box's shared login is mounted elsewhere (supervisor.ts
|
|
97
|
+
* `loginDirOf`). */
|
|
98
|
+
loginDir?: string;
|
|
94
99
|
/** Run the daemon's pump (a lease freed, a worker gone). Late-bound: the daemon is built
|
|
95
100
|
* after this. */
|
|
96
101
|
pump: () => Promise<void>;
|
|
@@ -206,6 +211,8 @@ export function buildWorkerWorld(o: WorkerWorldOptions): WorkerWorld {
|
|
|
206
211
|
root,
|
|
207
212
|
cwd: root,
|
|
208
213
|
agent_dir: o.agentDir,
|
|
214
|
+
...(o.loginDir !== undefined && o.loginDir !== o.agentDir ? { login_dir: o.loginDir } : {}),
|
|
215
|
+
providers: reviewProviders(cfg),
|
|
209
216
|
session_dir: sessions.sessionDir(row.session),
|
|
210
217
|
system_prompt_file: sessions.writeSystemPrompt(row.session, prompt),
|
|
211
218
|
model: cfg.models.conversation,
|
|
@@ -255,6 +262,7 @@ export function buildWorkerWorld(o: WorkerWorldOptions): WorkerWorld {
|
|
|
255
262
|
takeoverPlanFor({
|
|
256
263
|
root,
|
|
257
264
|
agentDir: o.agentDir,
|
|
265
|
+
...(o.loginDir !== undefined ? { loginDir: o.loginDir } : {}),
|
|
258
266
|
sessionsRoot: sessions.sessionsRoot(),
|
|
259
267
|
model: cfg.models.conversation,
|
|
260
268
|
pr,
|
|
@@ -1,450 +0,0 @@
|
|
|
1
|
-
// The round-progress v2 projection of a LiveEvent log.
|
|
2
|
-
//
|
|
3
|
-
// extensions/round-progress.ts publishes `state/round-progress/pr-<n>.json`
|
|
4
|
-
// (and the append-only transition log beside it) from inside the tmux pi, by
|
|
5
|
-
// listening to pi's events. This module reproduces both from the LiveEvent log
|
|
6
|
-
// alone, so the engine routes - which have no such extension - and a viewer
|
|
7
|
-
// reading a finished or running round all get the same snapshot, with the
|
|
8
|
-
// contract in packages/pi-plugin/src/round-progress.ts unchanged: board.ts, requiredpasses.ts and
|
|
9
|
-
// the supervisor read it as they always have.
|
|
10
|
-
//
|
|
11
|
-
// The extension is the REFERENCE. Every rule here mirrors the handler it
|
|
12
|
-
// stands for, line by line, because the golden test
|
|
13
|
-
// (tests/live-projection-golden.test.ts) holds the two byte-equal on the same
|
|
14
|
-
// events. The one place the log is coarser than pi's stream is time: a held
|
|
15
|
-
// update is stamped at its window's flush (~250 ms), so a heartbeat or a
|
|
16
|
-
// `started_at` taken from an update can differ from the extension's by that
|
|
17
|
-
// much. Nothing the supervisor decides on (the milestones' order, statuses,
|
|
18
|
-
// command identity) depends on it.
|
|
19
|
-
import { parseSessionCommandMarker } from "@quickchatai/pi-quickreview/src/session-command.js";
|
|
20
|
-
import { isLiveAgentStatus, } from "@quickchatai/pi-quickreview/src/round-progress.js";
|
|
21
|
-
function text(value) {
|
|
22
|
-
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
23
|
-
}
|
|
24
|
-
function number(value) {
|
|
25
|
-
return typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : 0;
|
|
26
|
-
}
|
|
27
|
-
function status(value, exitCode) {
|
|
28
|
-
if (isLiveAgentStatus(value))
|
|
29
|
-
return value;
|
|
30
|
-
return exitCode === 0 ? "completed" : exitCode === undefined ? "running" : "failed";
|
|
31
|
-
}
|
|
32
|
-
/** The round number rides the order's posting calls (`QCR_ROUND_NO=<k>`, with
|
|
33
|
-
* or without `export`); the head rides the Worktree line. */
|
|
34
|
-
function reviewIdentity(body) {
|
|
35
|
-
const round = /(?:^|\s)(?:export )?QCR_ROUND_NO=(\d+)(?=\s|$)/.exec(body);
|
|
36
|
-
const head = /\*\*Worktree\*\*:[^\n]* @ `([^`]+)`/.exec(body);
|
|
37
|
-
if (!round || !head)
|
|
38
|
-
return undefined;
|
|
39
|
-
const roundNo = Number(round[1]);
|
|
40
|
-
return Number.isInteger(roundNo) && roundNo > 0
|
|
41
|
-
? { round_no: roundNo, head_sha: head[1] }
|
|
42
|
-
: undefined;
|
|
43
|
-
}
|
|
44
|
-
function currentTool(progress) {
|
|
45
|
-
if (!Array.isArray(progress?.recent_tools))
|
|
46
|
-
return undefined;
|
|
47
|
-
const tools = progress.recent_tools;
|
|
48
|
-
const picked = [...tools].reverse().find((x) => x.status === "running") ?? tools.at(-1);
|
|
49
|
-
if (!picked)
|
|
50
|
-
return undefined;
|
|
51
|
-
const name = text(picked.tool) ?? "tool";
|
|
52
|
-
const args = text(picked.args);
|
|
53
|
-
return args ? `${name}: ${args.slice(0, 120)}` : name;
|
|
54
|
-
}
|
|
55
|
-
function asAgent(result, fallbackAgent, live = false) {
|
|
56
|
-
const agent = text(result.agent) ?? fallbackAgent;
|
|
57
|
-
if (!agent)
|
|
58
|
-
return undefined;
|
|
59
|
-
const progress = result.progress;
|
|
60
|
-
const native = result.native ?? { tool_count: 0 };
|
|
61
|
-
const error = text(progress?.error) ?? text(result.error_message);
|
|
62
|
-
const exit = result.exit_code;
|
|
63
|
-
const nativeFailed = (typeof exit === "number" && exit > 0) ||
|
|
64
|
-
result.stop_reason === "error" ||
|
|
65
|
-
result.stop_reason === "aborted";
|
|
66
|
-
const st = isLiveAgentStatus(progress?.status)
|
|
67
|
-
? progress.status
|
|
68
|
-
: nativeFailed
|
|
69
|
-
? "failed"
|
|
70
|
-
: live || exit === -1 || exit === undefined
|
|
71
|
-
? "running"
|
|
72
|
-
: status(undefined, exit);
|
|
73
|
-
const tool = currentTool(progress) ?? (live ? native.last_tool : undefined);
|
|
74
|
-
const last = text(progress?.last_message) ?? native.last_text;
|
|
75
|
-
return {
|
|
76
|
-
agent,
|
|
77
|
-
status: st,
|
|
78
|
-
duration_ms: number(progress?.duration_ms),
|
|
79
|
-
tool_count: number(progress?.tool_count) || native.tool_count,
|
|
80
|
-
...(tool ? { current_tool: tool } : {}),
|
|
81
|
-
...(last ? { last_message: last } : {}),
|
|
82
|
-
...(error ? { error } : {}),
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
export class RoundProgressProjection {
|
|
86
|
-
who;
|
|
87
|
-
snapshotState;
|
|
88
|
-
written;
|
|
89
|
-
log = [];
|
|
90
|
-
lastHeartbeat = 0;
|
|
91
|
-
clock = "";
|
|
92
|
-
agents = new Map();
|
|
93
|
-
callAgents = new Map();
|
|
94
|
-
runKeys = new Map();
|
|
95
|
-
endLogged = new Set();
|
|
96
|
-
/** The children of each open lens call, rebuilt from the changed slots. */
|
|
97
|
-
calls = new Map();
|
|
98
|
-
constructor(who) {
|
|
99
|
-
this.who = who;
|
|
100
|
-
}
|
|
101
|
-
/** The snapshot as its file would hold it now (the last write). */
|
|
102
|
-
get snapshot() {
|
|
103
|
-
return this.written === undefined
|
|
104
|
-
? undefined
|
|
105
|
-
: JSON.parse(this.written);
|
|
106
|
-
}
|
|
107
|
-
/** The bytes of `pr-<n>.json`. */
|
|
108
|
-
get snapshotJson() {
|
|
109
|
-
return this.written;
|
|
110
|
-
}
|
|
111
|
-
/** The transition lines, in order. */
|
|
112
|
-
get transitions() {
|
|
113
|
-
return this.log;
|
|
114
|
-
}
|
|
115
|
-
/** The bytes of `pr-<n>.jsonl` (before its size-cap rotation). */
|
|
116
|
-
get transitionsJsonl() {
|
|
117
|
-
return this.log.map((l) => `${JSON.stringify(l)}\n`).join("");
|
|
118
|
-
}
|
|
119
|
-
apply(event) {
|
|
120
|
-
this.clock = event.at;
|
|
121
|
-
switch (event.type) {
|
|
122
|
-
case "agent_start":
|
|
123
|
-
return this.agentStart();
|
|
124
|
-
case "message_start":
|
|
125
|
-
return this.messageStart(event.role, event.text);
|
|
126
|
-
case "turn_start":
|
|
127
|
-
case "message_delta":
|
|
128
|
-
return this.heartbeat();
|
|
129
|
-
case "tool_start":
|
|
130
|
-
return this.toolStart(event);
|
|
131
|
-
case "tool_update":
|
|
132
|
-
return this.toolUpdate(event);
|
|
133
|
-
case "tool_end":
|
|
134
|
-
return this.toolEnd(event);
|
|
135
|
-
case "agent_settled":
|
|
136
|
-
return this.settled();
|
|
137
|
-
default:
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
// ---- the extension's helpers --------------------------------------------------
|
|
142
|
-
nowIso() {
|
|
143
|
-
return this.clock;
|
|
144
|
-
}
|
|
145
|
-
bindRun(run, key) {
|
|
146
|
-
if (run === null)
|
|
147
|
-
return key;
|
|
148
|
-
const held = this.runKeys.get(run);
|
|
149
|
-
if (held !== undefined)
|
|
150
|
-
return held;
|
|
151
|
-
this.runKeys.set(run, key);
|
|
152
|
-
return key;
|
|
153
|
-
}
|
|
154
|
-
executionKeys(callId, names) {
|
|
155
|
-
const seen = new Map();
|
|
156
|
-
return names.map((agent) => {
|
|
157
|
-
const n = (seen.get(agent) ?? 0) + 1;
|
|
158
|
-
seen.set(agent, n);
|
|
159
|
-
return n === 1 ? `${callId}:${agent}` : `${callId}:${agent}#${n}`;
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
write() {
|
|
163
|
-
const snapshot = this.snapshotState;
|
|
164
|
-
if (!snapshot)
|
|
165
|
-
return;
|
|
166
|
-
snapshot.updated_at = this.nowIso();
|
|
167
|
-
snapshot.agents = [...this.agents.values()];
|
|
168
|
-
this.written = `${JSON.stringify(snapshot)}\n`;
|
|
169
|
-
}
|
|
170
|
-
transition(entry) {
|
|
171
|
-
const snapshot = this.snapshotState;
|
|
172
|
-
if (!snapshot)
|
|
173
|
-
return;
|
|
174
|
-
this.log.push({
|
|
175
|
-
at: this.nowIso(),
|
|
176
|
-
pr: this.who.pr,
|
|
177
|
-
session: this.who.session,
|
|
178
|
-
run_started_at: snapshot.run_started_at,
|
|
179
|
-
...(snapshot.command_id ? { command_id: snapshot.command_id } : {}),
|
|
180
|
-
...entry,
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
passEnded(agent) {
|
|
184
|
-
const key = agent.execution_id;
|
|
185
|
-
if (!key || agent.status === "running" || this.endLogged.has(key))
|
|
186
|
-
return;
|
|
187
|
-
this.endLogged.add(key);
|
|
188
|
-
this.transition({
|
|
189
|
-
event: "pass_end",
|
|
190
|
-
execution_id: key,
|
|
191
|
-
agent: agent.agent,
|
|
192
|
-
status: agent.status,
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
fanoutSeen() {
|
|
196
|
-
const snapshot = this.snapshotState;
|
|
197
|
-
if (!snapshot || this.agents.size === 0)
|
|
198
|
-
return;
|
|
199
|
-
snapshot.first_lens_started_at ??= this.nowIso();
|
|
200
|
-
snapshot.fanout_started_at ??= snapshot.first_lens_started_at;
|
|
201
|
-
}
|
|
202
|
-
activity(description) {
|
|
203
|
-
const snapshot = this.snapshotState;
|
|
204
|
-
if (!snapshot)
|
|
205
|
-
return;
|
|
206
|
-
snapshot.last_activity = description;
|
|
207
|
-
snapshot.last_activity_at = this.nowIso();
|
|
208
|
-
this.write();
|
|
209
|
-
}
|
|
210
|
-
heartbeat() {
|
|
211
|
-
if (!this.snapshotState)
|
|
212
|
-
return;
|
|
213
|
-
const now = Date.parse(this.clock);
|
|
214
|
-
if (now - this.lastHeartbeat < 1_000)
|
|
215
|
-
return;
|
|
216
|
-
this.lastHeartbeat = now;
|
|
217
|
-
this.write();
|
|
218
|
-
}
|
|
219
|
-
freshSnapshot(lastActivity) {
|
|
220
|
-
const at = this.nowIso();
|
|
221
|
-
this.agents.clear();
|
|
222
|
-
this.callAgents.clear();
|
|
223
|
-
this.runKeys.clear();
|
|
224
|
-
this.endLogged.clear();
|
|
225
|
-
return {
|
|
226
|
-
version: 2,
|
|
227
|
-
pr: this.who.pr,
|
|
228
|
-
session: this.who.session,
|
|
229
|
-
run_started_at: at,
|
|
230
|
-
updated_at: at,
|
|
231
|
-
parent_status: "active",
|
|
232
|
-
last_activity_at: at,
|
|
233
|
-
last_activity: lastActivity,
|
|
234
|
-
agents: [],
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
/** The children of a call after this line: the slots it carries laid over
|
|
238
|
-
* what the call's earlier lines left. */
|
|
239
|
-
merge(id, n, changed) {
|
|
240
|
-
const held = this.calls.get(id) ?? [];
|
|
241
|
-
for (const r of changed)
|
|
242
|
-
held[r.slot] = r;
|
|
243
|
-
const full = Array.from({ length: n }, (_, slot) => held[slot] ?? { slot });
|
|
244
|
-
this.calls.set(id, full);
|
|
245
|
-
return full;
|
|
246
|
-
}
|
|
247
|
-
// ---- the handlers ---------------------------------------------------------------
|
|
248
|
-
agentStart() {
|
|
249
|
-
// pi's automatic retry or queued continuation emits another agent_start
|
|
250
|
-
// inside the SAME outer run: an inner start keeps the run's command,
|
|
251
|
-
// milestones and passes.
|
|
252
|
-
if (this.snapshotState && this.snapshotState.parent_status === "active") {
|
|
253
|
-
this.activity("parent run continued");
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
this.snapshotState = this.freshSnapshot("parent run started");
|
|
257
|
-
this.write();
|
|
258
|
-
this.transition({ event: "parent_start" });
|
|
259
|
-
}
|
|
260
|
-
messageStart(role, body) {
|
|
261
|
-
const snapshot = this.snapshotState;
|
|
262
|
-
if (!snapshot || role !== "user" || !body)
|
|
263
|
-
return;
|
|
264
|
-
const marker = parseSessionCommandMarker(body);
|
|
265
|
-
if (!marker)
|
|
266
|
-
return;
|
|
267
|
-
// A different command in a run that has one already is a new execution.
|
|
268
|
-
let current = snapshot;
|
|
269
|
-
if (current.command_id !== undefined && current.command_id !== marker.id) {
|
|
270
|
-
current = this.freshSnapshot("parent run started");
|
|
271
|
-
this.snapshotState = current;
|
|
272
|
-
}
|
|
273
|
-
current.command_id = marker.id;
|
|
274
|
-
current.command_sha256 = marker.payloadSha256;
|
|
275
|
-
const identity = reviewIdentity(body);
|
|
276
|
-
if (identity) {
|
|
277
|
-
current.round_no = identity.round_no;
|
|
278
|
-
current.head_sha = identity.head_sha;
|
|
279
|
-
}
|
|
280
|
-
const firstAcceptance = current.command_accepted_at === undefined;
|
|
281
|
-
current.command_accepted_at ??= this.nowIso();
|
|
282
|
-
current.preparing_at ??= current.command_accepted_at;
|
|
283
|
-
if (firstAcceptance)
|
|
284
|
-
this.transition({
|
|
285
|
-
event: "command_accepted",
|
|
286
|
-
...(current.round_no !== undefined ? { round_no: current.round_no } : {}),
|
|
287
|
-
...(current.head_sha !== undefined ? { head_sha: current.head_sha } : {}),
|
|
288
|
-
});
|
|
289
|
-
this.activity(`command ${marker.id} accepted; parent preparing compact fanout context`);
|
|
290
|
-
}
|
|
291
|
-
toolStart(ev) {
|
|
292
|
-
const snapshot = this.snapshotState;
|
|
293
|
-
if (!snapshot)
|
|
294
|
-
return;
|
|
295
|
-
const executions = ev.executions ?? [];
|
|
296
|
-
if (executions.length > 0) {
|
|
297
|
-
const names = executions.map(({ agent }) => agent);
|
|
298
|
-
const at = this.nowIso();
|
|
299
|
-
this.callAgents.set(ev.id, names);
|
|
300
|
-
const keys = this.executionKeys(ev.id, names);
|
|
301
|
-
for (const [index, agent] of names.entries()) {
|
|
302
|
-
const key = keys[index];
|
|
303
|
-
const phase = executions[index]?.phase;
|
|
304
|
-
this.agents.set(key, {
|
|
305
|
-
execution_id: key,
|
|
306
|
-
agent,
|
|
307
|
-
...(phase ? { phase } : {}),
|
|
308
|
-
status: "running",
|
|
309
|
-
duration_ms: 0,
|
|
310
|
-
tool_count: 0,
|
|
311
|
-
started_at: at,
|
|
312
|
-
});
|
|
313
|
-
this.transition({ event: "pass_start", execution_id: key, agent });
|
|
314
|
-
}
|
|
315
|
-
snapshot.first_lens_started_at ??= at;
|
|
316
|
-
snapshot.fanout_started_at ??= snapshot.first_lens_started_at;
|
|
317
|
-
snapshot.active_tool = `subagent: ${names.join(", ")}`;
|
|
318
|
-
this.activity(`${names.join(", ")} started`);
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
snapshot.active_tool = ev.preview ? `${ev.tool}: ${ev.preview}` : ev.tool;
|
|
322
|
-
this.activity(snapshot.active_tool);
|
|
323
|
-
}
|
|
324
|
-
toolUpdate(ev) {
|
|
325
|
-
const snapshot = this.snapshotState;
|
|
326
|
-
if (!snapshot)
|
|
327
|
-
return;
|
|
328
|
-
const fallbacks = this.callAgents.get(ev.id) ?? [];
|
|
329
|
-
const reported = this.merge(ev.id, ev.n, ev.results);
|
|
330
|
-
// Native parallel mode marks a still-running slot with exitCode -1 and a
|
|
331
|
-
// finished one with its real exit code.
|
|
332
|
-
let changed = false;
|
|
333
|
-
const updated = reported.flatMap((result, index) => {
|
|
334
|
-
const agent = asAgent(result, fallbacks[index] ?? (fallbacks.length === 1 ? fallbacks[0] : undefined), ev.mode === "parallel" ? result.exit_code === -1 : true);
|
|
335
|
-
return agent ? [{ agent, run: result.id ?? null }] : [];
|
|
336
|
-
});
|
|
337
|
-
const updateKeys = this.executionKeys(ev.id, updated.map(({ agent }) => agent.agent));
|
|
338
|
-
for (const [index, { agent, run }] of updated.entries()) {
|
|
339
|
-
const key = this.bindRun(run, updateKeys[index]);
|
|
340
|
-
const previous = this.agents.get(key);
|
|
341
|
-
// A parallel slot can finish while its siblings still run: a terminal
|
|
342
|
-
// record carries its completion milestone from the first update that
|
|
343
|
-
// reports it, and a running one never carries one.
|
|
344
|
-
const { finished_at: previousFinished, ...kept } = previous ?? {};
|
|
345
|
-
const next = {
|
|
346
|
-
...kept,
|
|
347
|
-
...agent,
|
|
348
|
-
execution_id: key,
|
|
349
|
-
started_at: previous?.started_at ?? this.nowIso(),
|
|
350
|
-
...(agent.status === "running" ? {} : { finished_at: previousFinished ?? this.nowIso() }),
|
|
351
|
-
};
|
|
352
|
-
this.agents.set(key, next);
|
|
353
|
-
this.passEnded(next);
|
|
354
|
-
snapshot.active_tool = `subagent: ${agent.agent}`;
|
|
355
|
-
changed = true;
|
|
356
|
-
}
|
|
357
|
-
this.fanoutSeen();
|
|
358
|
-
if (changed)
|
|
359
|
-
this.write();
|
|
360
|
-
else
|
|
361
|
-
this.heartbeat();
|
|
362
|
-
}
|
|
363
|
-
toolEnd(ev) {
|
|
364
|
-
const snapshot = this.snapshotState;
|
|
365
|
-
if (!snapshot)
|
|
366
|
-
return;
|
|
367
|
-
const id = ev.id;
|
|
368
|
-
const fallbacks = this.callAgents.get(id) ?? [];
|
|
369
|
-
const reported = this.merge(id, ev.n, ev.results);
|
|
370
|
-
this.calls.delete(id);
|
|
371
|
-
const finalAgents = [];
|
|
372
|
-
const ended = reported.flatMap((result, index) => {
|
|
373
|
-
const agent = asAgent(result, fallbacks[index] ?? (fallbacks.length === 1 ? fallbacks[0] : undefined));
|
|
374
|
-
return agent ? [{ agent, run: result.id ?? null }] : [];
|
|
375
|
-
});
|
|
376
|
-
const endKeys = this.executionKeys(id, ended.map(({ agent }) => agent.agent));
|
|
377
|
-
for (const [index, { agent, run }] of ended.entries()) {
|
|
378
|
-
const key = this.bindRun(run, endKeys[index]);
|
|
379
|
-
const previous = this.agents.get(key);
|
|
380
|
-
// A child the call returns still running (a placeholder) carries no
|
|
381
|
-
// finish time, as in toolUpdate: the parser refuses the pair.
|
|
382
|
-
const { finished_at: previousFinished, ...kept } = previous ?? {};
|
|
383
|
-
const finalAgent = {
|
|
384
|
-
...kept,
|
|
385
|
-
...agent,
|
|
386
|
-
execution_id: key,
|
|
387
|
-
started_at: previous?.started_at ?? this.nowIso(),
|
|
388
|
-
...(agent.status === "running" ? {} : { finished_at: previousFinished ?? this.nowIso() }),
|
|
389
|
-
};
|
|
390
|
-
this.agents.set(key, finalAgent);
|
|
391
|
-
finalAgents.push(finalAgent);
|
|
392
|
-
this.passEnded(finalAgent);
|
|
393
|
-
}
|
|
394
|
-
if (finalAgents.length === 0 && fallbacks.length > 0) {
|
|
395
|
-
const failed = ev.is_error;
|
|
396
|
-
const fallbackKeys = this.executionKeys(id, fallbacks);
|
|
397
|
-
for (const [index, fallback] of fallbacks.entries()) {
|
|
398
|
-
const key = fallbackKeys[index];
|
|
399
|
-
const previous = this.agents.get(key);
|
|
400
|
-
if (previous && previous.status !== "running") {
|
|
401
|
-
finalAgents.push(previous);
|
|
402
|
-
continue;
|
|
403
|
-
}
|
|
404
|
-
const finalAgent = {
|
|
405
|
-
...(previous ?? {
|
|
406
|
-
execution_id: key,
|
|
407
|
-
agent: fallback,
|
|
408
|
-
duration_ms: 0,
|
|
409
|
-
tool_count: 0,
|
|
410
|
-
started_at: this.nowIso(),
|
|
411
|
-
}),
|
|
412
|
-
status: failed ? "failed" : "completed",
|
|
413
|
-
finished_at: previous?.finished_at ?? this.nowIso(),
|
|
414
|
-
};
|
|
415
|
-
this.agents.set(key, finalAgent);
|
|
416
|
-
finalAgents.push(finalAgent);
|
|
417
|
-
this.passEnded(finalAgent);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
this.fanoutSeen();
|
|
421
|
-
this.callAgents.delete(id);
|
|
422
|
-
snapshot.active_tool = undefined;
|
|
423
|
-
this.activity(finalAgents.length > 0
|
|
424
|
-
? `${finalAgents.map((agent) => `${agent.agent} ${agent.status}`).join(", ")}`
|
|
425
|
-
: `${ev.tool} ${ev.is_error ? "failed" : "completed"}`);
|
|
426
|
-
}
|
|
427
|
-
settled() {
|
|
428
|
-
const snapshot = this.snapshotState;
|
|
429
|
-
if (!snapshot)
|
|
430
|
-
return;
|
|
431
|
-
snapshot.parent_status = "settled";
|
|
432
|
-
snapshot.active_tool = undefined;
|
|
433
|
-
this.activity("parent session settled");
|
|
434
|
-
this.transition({ event: "parent_settle" });
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
/** Project a main log. The identity (PR, session) comes from its
|
|
438
|
-
* `session_start` line; a log with none, or for a round that has no PR number,
|
|
439
|
-
* projects to nothing - as the extension does nothing outside a numbered PR
|
|
440
|
-
* session. */
|
|
441
|
-
export function projectRoundProgress(events) {
|
|
442
|
-
const start = events.find((e) => e.type === "session_start");
|
|
443
|
-
if (start?.type !== "session_start" || start.stream !== "main" || start.pr === null)
|
|
444
|
-
return undefined;
|
|
445
|
-
const projection = new RoundProgressProjection({ pr: start.pr, session: start.session });
|
|
446
|
-
for (const e of events)
|
|
447
|
-
projection.apply(e);
|
|
448
|
-
return projection;
|
|
449
|
-
}
|
|
450
|
-
//# sourceMappingURL=projection.js.map
|