instar 1.3.591 → 1.3.593
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/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +23 -0
- package/dist/commands/server.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts +10 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +83 -12
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/QuotaAwareScheduler.d.ts +25 -0
- package/dist/core/QuotaAwareScheduler.d.ts.map +1 -1
- package/dist/core/QuotaAwareScheduler.js +36 -0
- package/dist/core/QuotaAwareScheduler.js.map +1 -1
- package/dist/core/types.d.ts +15 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/monitoring/GreenPrAutoMerger.d.ts +90 -5
- package/dist/monitoring/GreenPrAutoMerger.d.ts.map +1 -1
- package/dist/monitoring/GreenPrAutoMerger.js +269 -6
- package/dist/monitoring/GreenPrAutoMerger.js.map +1 -1
- package/dist/monitoring/MergeRunner.d.ts +21 -0
- package/dist/monitoring/MergeRunner.d.ts.map +1 -1
- package/dist/monitoring/MergeRunner.js +33 -5
- package/dist/monitoring/MergeRunner.js.map +1 -1
- package/dist/monitoring/QuotaTracker.d.ts +56 -0
- package/dist/monitoring/QuotaTracker.d.ts.map +1 -1
- package/dist/monitoring/QuotaTracker.js +112 -4
- package/dist/monitoring/QuotaTracker.js.map +1 -1
- package/dist/monitoring/greenPrAutomergeWiring.d.ts +7 -0
- package/dist/monitoring/greenPrAutomergeWiring.d.ts.map +1 -1
- package/dist/monitoring/greenPrAutomergeWiring.js +44 -2
- package/dist/monitoring/greenPrAutomergeWiring.js.map +1 -1
- package/dist/monitoring/greenPrLogic.d.ts +39 -1
- package/dist/monitoring/greenPrLogic.d.ts.map +1 -1
- package/dist/monitoring/greenPrLogic.js +33 -1
- package/dist/monitoring/greenPrLogic.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +2 -1
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/CapabilityIndex.js +1 -1
- package/dist/server/CapabilityIndex.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +29 -4
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/scripts/safe-merge.mjs +27 -1
- package/src/data/builtin-manifest.json +63 -63
- package/src/scaffold/templates.ts +2 -1
- package/upgrades/1.3.592.md +40 -0
- package/upgrades/1.3.593.md +28 -0
- package/upgrades/side-effects/mergerunner-auto-arm-handoff-parity-track.md +60 -0
- package/upgrades/side-effects/mergerunner-auto-arm-handoff.md +48 -0
- package/upgrades/side-effects/pool-aware-quota-throttle.md +123 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
The global quota brake (`QuotaTracker.shouldSpawnSession`) was account-blind: it stopped the WHOLE agent
|
|
9
|
+
on a single account's usage, so when one Claude account hit its weekly limit the agent froze even though
|
|
10
|
+
other pooled accounts sat at 0% (the 2026-06-15 "two accounts untouched while work stalls" incident; a
|
|
11
|
+
degraded JSONL estimate reading 186% jammed it harder). It now reasons over the whole pool by asking the
|
|
12
|
+
placement layer's OWN `selectAccount` predicate (via a provider wired in `server.ts`): the brake never
|
|
13
|
+
halts the agent while a placeable account has headroom, and never allows work placement can't land — the
|
|
14
|
+
90–95% "allowed-but-unplaceable" respawn-loop band is closed by construction (`allowed ⟹ placeable`).
|
|
15
|
+
Missing/implausible/degraded readings trigger a BOUNDED mode (shed low priority, allow medium+, honor an
|
|
16
|
+
authoritative 5-hour wall) instead of a stall or unbounded fail-open.
|
|
17
|
+
|
|
18
|
+
## What to Tell Your User
|
|
19
|
+
|
|
20
|
+
If you run several Claude accounts pooled together, the agent no longer freezes when one account hits its
|
|
21
|
+
weekly limit — it keeps working on the accounts that still have room, and it won't get stuck in the
|
|
22
|
+
"session respawned" restart loop. Single-account agents are completely unaffected (byte-identical
|
|
23
|
+
behavior). Nothing to configure; it's on by default because it removes a bug.
|
|
24
|
+
|
|
25
|
+
## Summary of New Capabilities
|
|
26
|
+
|
|
27
|
+
- `QuotaTracker.setPoolQuotaProvider()` — the brake shares placement's exact eligibility predicate, so a
|
|
28
|
+
maxed account can never stop the whole agent while a placeable account exists.
|
|
29
|
+
- Bounded degraded-data handling (shared by the pool and single-account paths): non-authoritative /
|
|
30
|
+
out-of-range / missing readings shed low priority and allow medium+, instead of stalling or running blind.
|
|
31
|
+
- The "use-it-or-lose-it" optimizations (drain near-reset accounts first, burn-rate balancing) are the
|
|
32
|
+
tracked SECOND PR — this one is the robust+safe foundation.
|
|
33
|
+
|
|
34
|
+
## Evidence
|
|
35
|
+
|
|
36
|
+
Converged over 3 `/spec-converge` rounds (6 internal reviewers + codex GPT-5.5); the first two designs were
|
|
37
|
+
rejected by review (one dead in production, one with a staleness/loop gap) and redesigned. 253 quota tests
|
|
38
|
+
pass with no regressions, including a test proving `allowed ⟹ placeable` across a 0→100 usage sweep and the
|
|
39
|
+
90–95% band → stop. Spec: `docs/specs/POOL-AWARE-QUOTA-THROTTLE-SPEC.md`. Acceptance: the two previously-idle
|
|
40
|
+
accounts demonstrably carry load after the fix (live proof).
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
The green-PR auto-merge watcher (the background machinery that merges an agent's own green, mergeable, non-held PRs without a human clicking merge) now **arms GitHub's native auto-merge and hands off**, instead of running a merge command and synchronously watching it land inside a time-boxed helper. Once armed, GitHub merges the PR itself the instant every required check passes; the watcher confirms the landed merge on a later reconciliation tick. This frees the watcher's single-work slot in seconds instead of holding it for up to 25 minutes, and it survives the watcher's own merge triggering a server restart (GitHub owns the merge, so a restart can't kill it).
|
|
9
|
+
|
|
10
|
+
Because GitHub now owns an armed merge, the operator's stop controls were upgraded to actually reach in-flight merges: rollback, emergency-pause, pool-disarm, and an explicit per-PR HOLD now actively turn OFF auto-merge on each armed PR. A new honest signal flags the one accepted residual risk (a write-capable push landing between arm and merge could merge an un-vetted head — surfaced post-hoc, never silent). Ships dark (off fleet-wide; armed per dev agent; soaks in dry-run first).
|
|
11
|
+
|
|
12
|
+
## What to Tell Your User
|
|
13
|
+
|
|
14
|
+
If your agent runs the green-PR auto-merge watcher, it now hands each merge to GitHub's own auto-merge instead of babysitting it — so a server restart in the middle no longer leaves a merge in limbo, and the watcher frees up faster. One important behavior change for operators: once a merge is armed on GitHub, simply pausing the watcher or adding a hold label no longer stops it — so the stop controls now actively turn the armed auto-merge off for you. The feature ships off by default and soaks in a no-op dry-run mode first, so nothing changes for you unless you explicitly run this watcher.
|
|
15
|
+
|
|
16
|
+
## Summary of New Capabilities
|
|
17
|
+
|
|
18
|
+
- The auto-merge watcher arms GitHub native auto-merge and hands off, surviving a restart-mid-merge and freeing its work slot in seconds.
|
|
19
|
+
- Operator stop controls (rollback, emergency-pause, per-PR hold) now actively disarm in-flight armed merges.
|
|
20
|
+
- An armed-but-stuck PR keeps being watched and re-surfaces rather than being silently dropped, and a merge at an unexpected commit is flagged.
|
|
21
|
+
|
|
22
|
+
## Evidence
|
|
23
|
+
|
|
24
|
+
- `src/monitoring/MergeRunner.ts`, `GreenPrAutoMerger.ts`, `greenPrLogic.ts`, `greenPrAutomergeWiring.ts` — the arm path, the new armed/armed-overdue states, the reconciliation tick, the disarm reach, the config threading.
|
|
25
|
+
- `scripts/safe-merge.mjs` — a distinct refused-auto-arm-unavailable result so the watcher can tell "repo setting off" from a transient failure.
|
|
26
|
+
- `src/server/routes.ts`, `src/core/PostUpdateMigrator.ts`, `src/core/types.ts`, `src/commands/server.ts` — observability, the content-sniff CLAUDE.md migration for existing agents, config defaults, wiring.
|
|
27
|
+
- 302 tests across 10 suites green (unit + integration + wiring-integrity + migration); tsc clean; full lint clean (dark-gate, guard-manifest, capabilities-discoverability).
|
|
28
|
+
- Spec `docs/specs/mergerunner-auto-arm-handoff.md` — CONVERGED over 3 review rounds (6 internal lenses + conformance gate each).
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Side-Effects Review — Track `mergeStrategy` in Migration-Parity guard test
|
|
2
|
+
|
|
3
|
+
**Change:** Add `mergeStrategy` to `legacyMigratorSections` in
|
|
4
|
+
`tests/unit/feature-delivery-completeness.test.ts`. Test-only. Tier 1.
|
|
5
|
+
|
|
6
|
+
**Context:** PR #1191's `PostUpdateMigrator.migrateClaudeMd` uses
|
|
7
|
+
`else if (!content.includes('mergeStrategy'))` as the updated-copy content-sniff to
|
|
8
|
+
detect+replace a STALE copy of the already-tracked "Green-PR Auto-Merge" section.
|
|
9
|
+
The parity guard's section-detector treats every `content.includes('X')` literal as
|
|
10
|
+
a section name and flagged `mergeStrategy` as untracked. The fix records it as the
|
|
11
|
+
re-detection marker for the already-tracked `/green-pr-automerge` section.
|
|
12
|
+
|
|
13
|
+
## Phase 1 — Principle check (signal vs authority)
|
|
14
|
+
|
|
15
|
+
Does this change involve a decision point that gates information flow, blocks
|
|
16
|
+
actions, filters messages, or constrains agent behavior? **No.** It is a one-line
|
|
17
|
+
addition to a test's allowlist of known migrator section markers. It adds no
|
|
18
|
+
runtime logic, no detector, no authority. The guard test it touches is itself a
|
|
19
|
+
signal (it fails CI; it never blocks runtime behavior). No signal-vs-authority
|
|
20
|
+
concern.
|
|
21
|
+
|
|
22
|
+
## Phase 4 — Side-effects answers
|
|
23
|
+
|
|
24
|
+
1. **Over-block** — None. The guard now correctly recognizes one more legitimate
|
|
25
|
+
marker. It does NOT loosen the guard generally: any genuinely-new section name
|
|
26
|
+
still trips it. Over-blocking would be the guard staying red on a legitimate
|
|
27
|
+
marker — which is the bug being fixed.
|
|
28
|
+
2. **Under-block** — Could adding `mergeStrategy` mask a future real section that
|
|
29
|
+
happened to also contain that literal? Negligible: the detector matches the
|
|
30
|
+
exact literal inside a `content.includes('mergeStrategy')` guard in the
|
|
31
|
+
migrator; `mergeStrategy` is a specific marker for one section. A future
|
|
32
|
+
unrelated section would carry its own distinct sniff literal.
|
|
33
|
+
3. **Level-of-abstraction fit** — Correct layer. The tracking list is exactly where
|
|
34
|
+
the established pattern (`/corrections`, `/cutover-readiness/import-dryrun`,
|
|
35
|
+
`unlabeledCallShare`) records sub-line / re-detection sniff keys for parent
|
|
36
|
+
sections that are tracked elsewhere. This entry is identical in kind.
|
|
37
|
+
4. **Signal vs authority compliance** — Compliant. No blocking authority added;
|
|
38
|
+
the test is a CI signal, not a runtime gate.
|
|
39
|
+
5. **Interactions** — None. The entry sits alongside the existing
|
|
40
|
+
`/green-pr-automerge` tracking entry; it doesn't shadow or double-count it (the
|
|
41
|
+
detector lists each distinct literal once; both are accounted for).
|
|
42
|
+
6. **External surfaces** — None. Test-only; no runtime, route, message, or
|
|
43
|
+
cross-agent surface changes.
|
|
44
|
+
7. **Multi-machine posture** — N/A (test-only, machine-agnostic). The parent
|
|
45
|
+
feature's own multi-machine posture was reviewed in
|
|
46
|
+
`mergerunner-auto-arm-handoff.md`; this fix changes nothing about it.
|
|
47
|
+
8. **Rollback cost** — Trivial. Revert the one-line test addition. No release, no
|
|
48
|
+
migration, no state repair. The parent PR's runtime behavior is untouched
|
|
49
|
+
either way.
|
|
50
|
+
|
|
51
|
+
## Phase 4.5 — No deferrals
|
|
52
|
+
|
|
53
|
+
This fix is complete; nothing is deferred. It is the entirety of the parity-guard
|
|
54
|
+
correction for #1191.
|
|
55
|
+
|
|
56
|
+
## Phase 5 — Second-pass review
|
|
57
|
+
|
|
58
|
+
Not required: the change carries no block/allow authority, no session-lifecycle,
|
|
59
|
+
coherence, idempotency, or runtime-gate surface. It is a test-tracking-list
|
|
60
|
+
addition.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Side-Effects Review — MergeRunner Auto-Arm Handoff
|
|
2
|
+
|
|
3
|
+
**Spec:** `docs/specs/mergerunner-auto-arm-handoff.md` (CONVERGED 3 rounds + approved:true; report `docs/specs/reports/mergerunner-auto-arm-handoff-convergence.md`). Dev-cycle P0.
|
|
4
|
+
**Change:** switch the green-PR auto-merge watcher (`MergeRunner`/`GreenPrAutoMerger`) from "spawn `safe-merge --admin` and synchronously watch the merge land" to "arm GitHub native auto-merge (`safe-merge --auto`) and hand off," confirming the eventual merge on a later reconciliation tick. New non-terminal `armed`/`armed-overdue` episode states; operator-disarm reach via `--disable-auto`; GitHub `autoMergeRequest` as the cross-machine source of truth.
|
|
5
|
+
|
|
6
|
+
## Phase 1 — Principle check (signal vs authority)
|
|
7
|
+
Touches a decision point (whether/when to merge a PR), so the principle applies. **No new brittle blocking authority.** Arming native auto-merge is a *handoff*, not a new gate — GitHub enforces every required check (strictly STRICTER than `--admin`, which *bypassed* them). The merge AUTHORITY is unchanged (still the existing eligibility gates: protected-paths, identity, dual-latch, lease, holds — all upstream of arming and unmodified). The new pieces are SIGNALS or operator-authorized actions: the `merged-at-unexpected-head` detector, `armed-overdue`, `auto-merge-unavailable`, and the `unconfirmedArmAttempts` ceiling are attention lines (never forced give-ups); the `--disable-auto` disarm is the explicit reach of an EXISTING operator kill switch (the operator pressed rollback / set the HOLD), not a new autonomous mutation. The B10 honesty invariant is preserved exactly — `merged` is recorded only after an independent `gh pr view` MERGED read, now on the reconciliation tick. Fail-open throughout (UNKNOWN read → leave armed, no ladder, no breaker). Compliant (`docs/signal-vs-authority.md`).
|
|
8
|
+
|
|
9
|
+
## Phase 2 — Plan
|
|
10
|
+
Built in `.worktrees/mergerunner-auto-switch` off `JKHeadley/main` #1188 (`git remote -v` → JKHeadley; converged+approved spec present). Decision points touched: the act-path strategy (`mergeStrategy:'auto'` default | `'admin'` legacy) + the B10 confirm timing. Existing detectors interacted with: the per-PR failure ladder, the global breaker, the dual-latch, the lease. Rollback: `mergeStrategy:'admin'` config lever + the dual-latch/dark flags + revert the PR (additive code, no data migration).
|
|
11
|
+
|
|
12
|
+
## 1. Over-block
|
|
13
|
+
The change does not block inputs — it routes the merge to GitHub. The inverse risk is **over-merge** (auto-merge merging something it shouldn't). Bounded by: the SAME upstream eligibility gates (protected-paths, identity, holds, dual-latch, lease) all run BEFORE arming and are unchanged; native auto-merge cannot merge a red/pending PR (GitHub enforces required checks — stricter than the old `--admin` bypass); the documented residual race (a write-capable push between arm and green could merge an unvetted head) is surfaced post-hoc via the `mergeCommitOid`/`expectedHeadOid` vs `armedHead` mismatch detector + attention line; and the whole watcher ships dark (`monitoring.greenPrAutoMerge.enabled` off fleet-wide; `dryRun` soak first).
|
|
14
|
+
|
|
15
|
+
## 2. Under-block
|
|
16
|
+
A PR armed-then-stuck (CI perma-red after arming) is NOT silently dropped — `armed-overdue` keeps reconciling and re-surfaces a deduped attention line (Close the Loop). A confirm gap that persists is bounded by the head-keyed `unconfirmedArmAttempts` ceiling (surfaces, never spins invisibly). The one accepted residual (write-capable-push head substitution) is documented + post-hoc-detected, not silently missed — and the realistic write-capable-pusher set on `JKHeadley/instar` is small.
|
|
17
|
+
|
|
18
|
+
## 3. Level-of-abstraction fit
|
|
19
|
+
Correct layer. The switch lives in the existing `MergeRunner`/`GreenPrAutoMerger` (the one place the watcher acts) + a thin `safe-merge.mjs` slug refinement + a config-threading chain. It reuses the existing episode/ladder/breaker/lease machinery rather than building a parallel revival path; the new `armed` outcome is a clean third branch of `applyOutcome` that does not alter the existing terminal branches. GitHub's `autoMergeRequest` (not a new local store) is the cross-machine truth.
|
|
20
|
+
|
|
21
|
+
## 4. Signal vs authority compliance
|
|
22
|
+
See Phase 1. No brittle check gains blocking authority. The only new mutation (`--disable-auto`) is operator-authorized + audited + namespace-checked + null-safe. Compliant.
|
|
23
|
+
|
|
24
|
+
## 5. Interactions
|
|
25
|
+
- **The B10 line (`GreenPrAutoMerger.ts:452`)** is the central interaction: it is left gated on `merged` ONLY (explicit comment forbids generalizing it to `armed` — `confirmedMerged:false` is correct/expected for armed). Verified by a positive + negative test.
|
|
26
|
+
- **Reconciliation vs candidate path:** `gather()` excludes any PR with a local `armedAt` OR GitHub `autoMergeArmed`, so an armed PR never re-enters the act path until reconciliation clears it — no arm/re-arm thrash (the mirror-decision-methods lesson: the reconciler's "leave armed" is matched by gather's exclusion).
|
|
27
|
+
- **Breaker:** reconciliation read-failures (UNKNOWN) and the non-ladder retry classes feed NO breaker signal (the breaker still only takes busy/deadline/tick-failed); arming already succeeded, so a flaky read can't open the breaker.
|
|
28
|
+
- **Disarm vs tick:** disarm runs IN-LINE in the rollback/pool-disarm routes (the tick is latch-gated and would never reach it), so the operator kill switch genuinely reaches in-flight armed merges.
|
|
29
|
+
- **No double-fire:** the `armed` (candidate) and reconciliation paths are mutually exclusive on a given PR within a tick.
|
|
30
|
+
|
|
31
|
+
## 6. External surfaces
|
|
32
|
+
- `GET /green-pr-automerge` gains non-optional `armedCount`/`armed:[]` (observability — an in-flight async merge is now first-class visible).
|
|
33
|
+
- `safe-merge.mjs` gains a `refused:auto-arm-unavailable` result + `--capabilities` entry (back-compat: a new slug; existing callers unaffected).
|
|
34
|
+
- CLAUDE.md template gains the corrected behavior text (new agents via `generateClaudeMd`; EXISTING agents via a dedicated content-sniff REPLACE migration — the Migration-Parity fix, since the old install-if-`/green-pr-automerge`-absent sniff would skip already-armed agents and never deliver the load-bearing "a HOLD label alone does NOT stop GitHub auto-merge" fact).
|
|
35
|
+
- Depends on a runtime condition: "Allow auto-merge" enabled on the repo (it is, on `JKHeadley/instar`); if disabled, `refused:auto-arm-unavailable` → terminal-non-ladder attention (operator enables it or sets `mergeStrategy:'admin'`).
|
|
36
|
+
|
|
37
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
38
|
+
**Machine-local episodes + GitHub-global merge authority, coordinated by the existing lease + GitHub `autoMergeRequest`.** Episode state (`green-pr-automerge.json`) is machine-local-by-design and does NOT replicate — but that no longer strands a lease move, because GitHub-side `autoMergeRequest` (read in the widened `gather()`/`refetchPr` projections) is the source of truth for "already armed": the new lease holder reads GitHub and skips/reconciles rather than re-arming. The async handoff genuinely IMPROVES the multi-machine story (a lease move between arm and merge no longer loses the merge — GitHub owns it). No new cross-machine state, no generated URL, no one-voice notice surface introduced. Stated explicitly in the spec's Multi-machine posture section.
|
|
39
|
+
|
|
40
|
+
## 8. Rollback cost
|
|
41
|
+
Cheap and layered. (a) **Dark by default:** the whole watcher is off fleet-wide; armed per dev agent. (b) **`mergeStrategy:'admin'` lever:** restores the exact prior poll+admin behavior (runbook documents that a code-level rollback with armed PRs on GitHub leaves them for old code to redundantly `--admin`-merge — benign, both paths enforce required checks; or disarm-first). (c) **Dual-latch:** rollback/emergency-pause/pool-disarm now actively `--disable-auto` armed PRs. (d) **Full back-out:** revert the PR — additive code, new optional config/episode fields are forward-compatible via the `loadState` spread, no data migration, no agent-state repair.
|
|
42
|
+
|
|
43
|
+
## No-deferrals (Phase 4.5)
|
|
44
|
+
No deferrals. Every Round-2/Round-3 finding was resolved in the converged spec and implemented (the convergence report is the audit trail). The one accepted residual (write-capable-push head race) is an explicitly-documented, post-hoc-detected, bounded risk — the honest scope, not a partial fix.
|
|
45
|
+
|
|
46
|
+
## Phase 5 — Second-pass review
|
|
47
|
+
*(reaper/merge-class — REQUIRED; appended below)*
|
|
48
|
+
**Concur with the review.** An independent reaper/merge-class reviewer audited the ACTUAL implementation diff (not the artifact's claims) against all 7 load-bearing properties at real file:line — B10 not corrupted (`GreenPrAutoMerger.ts:579` gated on `merged` only; `armed` reaches its own branch; `act()` truthy for armed + negative misimpl test), re-arm exclusion airtight (`gather()` `localArmed||githubArmed` :451-461; widened projection derives `autoMergeArmed`), disarm reach real (rollback/pool-disarm in-line null-safe → `--disable-auto`; honest per-PR failure split), reconciliation fail-open (UNKNOWN→leave armed, no ladder/breaker; MERGED compares `expectedHeadOid`/`headRefOid` not squash `mergeCommitOid`), multi-machine (GitHub `autoMergeRequest` source of truth, machine-local episodes, no strand), `--disable-auto` namespace-safe (only `state.episodes` from `@me`-gated PRs, `--repo` pinned), B24 scoped to admin path. tsc clean; new optional fields forward-compatible; no existing terminal `applyOutcome` branch altered. No blockers.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Side-Effects Review — Pool-Aware Quota Throttle
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `pool-aware-quota-throttle`
|
|
4
|
+
**Date:** `2026-06-16`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** spec-converge multi-reviewer panel (6 internal + codex GPT-5.5), 3 rounds — see docs/specs/reports/POOL-AWARE-QUOTA-THROTTLE-convergence.md
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Makes the global quota brake (`QuotaTracker.shouldSpawnSession`) pool-aware. Instead of stopping the
|
|
11
|
+
whole agent when ONE account's usage is high, it consults a new `poolHeadroom` helper in the placement
|
|
12
|
+
module (via an injected provider wired in `server.ts`). `poolHeadroom` shares `selectAccount`'s EXACT
|
|
13
|
+
eligibility predicate — so `placeable ⟺ selectAccount() !== null` (the never-loop invariant) — but
|
|
14
|
+
gates on the MOST-HEADROOM eligible account, so non-critical work runs whenever ANY account has room
|
|
15
|
+
(the 2026-06-16 live-proof fix: gating on `selectAccount`'s use-it-or-lose-it drain-first winner
|
|
16
|
+
wrongly shed medium/autonomous work even with a fresh 0% reserve). Placement (`selectAccount`) still
|
|
17
|
+
drains the soonest-to-reset account. A non-authoritative/implausible/missing reading triggers a
|
|
18
|
+
BOUNDED degraded mode (shed low priority, allow medium+, honor an authoritative 5h wall) rather than
|
|
19
|
+
an unbounded fail-open or a whole-agent stall. Files: `src/monitoring/QuotaTracker.ts` (provider +
|
|
20
|
+
bounded helper + data-quality guard), `src/core/QuotaAwareScheduler.ts` (the shared `poolHeadroom`
|
|
21
|
+
helper), `src/commands/server.ts` (wiring), `src/scaffold/templates.ts` (CLAUDE.md awareness).
|
|
22
|
+
Decision point: the spawn/job quota gate.
|
|
23
|
+
|
|
24
|
+
## Decision-point inventory
|
|
25
|
+
|
|
26
|
+
- `QuotaTracker.shouldSpawnSession` (the spawn/job quota brake) — **modify** — now reasons over pool
|
|
27
|
+
placeability (shared with placement's `selectAccount`) instead of a single account's usage; adds a
|
|
28
|
+
bounded degraded mode. This is the only decision surface touched.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 1. Over-block
|
|
33
|
+
|
|
34
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
35
|
+
|
|
36
|
+
The change REDUCES over-block — its whole purpose is to stop the brake from falsely halting the agent
|
|
37
|
+
when fresh accounts exist. New possible over-block: if EVERY account is status `rate-limited` (not
|
|
38
|
+
just high-usage), `selectAccount` returns null → STOP. This is correct (placement genuinely can't
|
|
39
|
+
land), and it is NOT latched — it re-evaluates every call and self-clears when a window resets. The
|
|
40
|
+
bounded degraded mode sheds `low` priority during untrustworthy readings — intended, conservative.
|
|
41
|
+
|
|
42
|
+
## 2. Under-block
|
|
43
|
+
|
|
44
|
+
**What failure modes does this still miss?**
|
|
45
|
+
|
|
46
|
+
A throttle "allow" guarantees placeability AT DECISION TIME; a concurrent spawn could consume the last
|
|
47
|
+
account before placement runs — placement then returns null, which the caller handles as a normal
|
|
48
|
+
back-off (NOT the old respawn loop, which came from throttle/placement disagreeing — now impossible).
|
|
49
|
+
The provider trusts the pool's own quota-poll freshness (same data placement already relies on); a
|
|
50
|
+
genuinely stale-but-present reading is the pool layer's concern, not newly introduced here.
|
|
51
|
+
|
|
52
|
+
## 3. Level-of-abstraction fit
|
|
53
|
+
|
|
54
|
+
The fix lives in the existing quota-gating module (`QuotaTracker`) and the existing server wiring —
|
|
55
|
+
the correct layer. It reuses placement's `selectAccount`/`bindingUtilization` rather than duplicating
|
|
56
|
+
eligibility logic, so there is one source of truth for "is this account placeable."
|
|
57
|
+
|
|
58
|
+
## 4. Signal vs authority compliance
|
|
59
|
+
|
|
60
|
+
`QuotaTracker` is an EXISTING authority (it gates real spawns). The change does not add a new brittle
|
|
61
|
+
blocking check — it makes the existing authority's decision pool-aware and adds fail-open/bounded
|
|
62
|
+
behavior in the safe direction. Fail-open on degraded data is exactly the Signal-vs-Authority guidance
|
|
63
|
+
(don't let an expensive false-positive stop everything). Lessons-aware reviewer confirmed PASS across 3
|
|
64
|
+
rounds.
|
|
65
|
+
|
|
66
|
+
## 5. Interactions
|
|
67
|
+
|
|
68
|
+
- **Placement (`QuotaAwareScheduler.selectAccount`):** now shared by the throttle — by construction
|
|
69
|
+
they cannot diverge (closes the respawn-loop band). No change to selectAccount itself.
|
|
70
|
+
- **SubscriptionPool:** read-only (`list()`); no mutation.
|
|
71
|
+
- **QuotaCollector / QuotaManager:** unchanged (the earlier collector edit was reverted). The file
|
|
72
|
+
path (single-account / legacy) is byte-identical except the bounded degraded mode.
|
|
73
|
+
- **Scheduler / can-start / spawn gates:** all consume `shouldSpawnSession`/`canRunJob` and benefit
|
|
74
|
+
automatically; no signature change.
|
|
75
|
+
|
|
76
|
+
## 6. External surfaces
|
|
77
|
+
|
|
78
|
+
No new routes, no new config keys, no new dashboard surface. The only user-facing surface is the
|
|
79
|
+
CLAUDE.md template awareness blurb (additive). No external network calls added (the provider reads the
|
|
80
|
+
in-memory pool synchronously).
|
|
81
|
+
|
|
82
|
+
## 6b. Operator-surface quality
|
|
83
|
+
|
|
84
|
+
No operator-facing UI/route changes. The behavior the operator observes ("the agent no longer freezes
|
|
85
|
+
when one account maxes out; idle accounts get used") is the intended improvement, documented in the
|
|
86
|
+
CLAUDE.md awareness section so an agent can explain it conversationally.
|
|
87
|
+
|
|
88
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
89
|
+
|
|
90
|
+
**Machine-local BY DESIGN.** `quota-state.json` and the SubscriptionPool the throttle reads are
|
|
91
|
+
per-machine: each host polls its own account credentials and places sessions locally. Replicating
|
|
92
|
+
would be wrong (machine A's stale view would gate machine B's spawns). The existing
|
|
93
|
+
`GET /subscription-pool?scope=pool` is the unchanged cross-machine quota surface. No durable state is
|
|
94
|
+
introduced that could strand on a topic transfer. Documented in the spec's "Multi-machine posture"
|
|
95
|
+
section.
|
|
96
|
+
|
|
97
|
+
## 8. Rollback cost
|
|
98
|
+
|
|
99
|
+
Low. Pure code change, no persisted-state migration. Disable per-agent by leaving the provider unset
|
|
100
|
+
(or `setPoolQuotaProvider(undefined)`) → file-based gating returns. Full revert = revert the commit +
|
|
101
|
+
rebuild + restart sessions. Solo (single-account) agents are unaffected (no provider wired). No
|
|
102
|
+
dark-gate flag (this is a correctness fix, default-on, fail-safe — the prior behavior was the bug).
|
|
103
|
+
|
|
104
|
+
## Conclusion
|
|
105
|
+
|
|
106
|
+
The change removes a false-positive whole-agent halt, shares placement's exact eligibility so the
|
|
107
|
+
throttle and placer can't diverge (no respawn loop), and bounds degraded-data behavior in the safe
|
|
108
|
+
direction. Reviewed to convergence over 3 rounds (the first two designs were rejected and redesigned).
|
|
109
|
+
All three test tiers green; 253 quota tests pass with no regressions.
|
|
110
|
+
|
|
111
|
+
## Second-pass review (if required)
|
|
112
|
+
|
|
113
|
+
Done — the spec-converge panel (6 internal reviewers + codex GPT-5.5 external) served as the
|
|
114
|
+
multi-reviewer second pass across 3 rounds; both high-signal reviewers returned "CONVERGED — no
|
|
115
|
+
material findings" in the final round. Concurred: true.
|
|
116
|
+
|
|
117
|
+
## Evidence pointers
|
|
118
|
+
|
|
119
|
+
- Spec: `docs/specs/POOL-AWARE-QUOTA-THROTTLE-SPEC.md` (review-convergence + approved)
|
|
120
|
+
- ELI16: `docs/specs/POOL-AWARE-QUOTA-THROTTLE-SPEC.eli16.md`
|
|
121
|
+
- Convergence report: `docs/specs/reports/POOL-AWARE-QUOTA-THROTTLE-convergence.md`
|
|
122
|
+
- Round-1 findings: `docs/specs/reports/POOL-AWARE-QUOTA-THROTTLE-round1-findings.md`
|
|
123
|
+
- Tests: `tests/unit/quota-tracker-pool-aware.test.ts`, `tests/unit/quota-tracker-invalid-input.test.ts`, `tests/integration/pool-aware-quota-canstart.test.ts`
|