instar 1.3.225 → 1.3.227
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/cli.js +10 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/devCiFailures.d.ts +39 -0
- package/dist/commands/devCiFailures.d.ts.map +1 -0
- package/dist/commands/devCiFailures.js +133 -0
- package/dist/commands/devCiFailures.js.map +1 -0
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +20 -1
- package/dist/commands/server.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +11 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +18 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/StateManager.d.ts +22 -1
- package/dist/core/StateManager.d.ts.map +1 -1
- package/dist/core/StateManager.js +45 -6
- package/dist/core/StateManager.js.map +1 -1
- package/dist/core/types.d.ts +46 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/monitoring/BurnDetector.d.ts +11 -0
- package/dist/monitoring/BurnDetector.d.ts.map +1 -1
- package/dist/monitoring/BurnDetector.js +15 -3
- package/dist/monitoring/BurnDetector.js.map +1 -1
- package/dist/monitoring/SessionReaper.d.ts +74 -1
- package/dist/monitoring/SessionReaper.d.ts.map +1 -1
- package/dist/monitoring/SessionReaper.js +146 -6
- package/dist/monitoring/SessionReaper.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +9 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +56 -25
- package/dist/server/AgentServer.js.map +1 -1
- package/package.json +1 -1
- package/scripts/generate-builtin-manifest.cjs +1 -0
- package/src/data/builtin-manifest.json +59 -51
- package/src/scaffold/templates.ts +9 -0
- package/upgrades/1.3.226.md +111 -0
- package/upgrades/1.3.227.md +109 -0
- package/upgrades/burn-detector-activity-gate.eli16.md +53 -0
- package/upgrades/dev-ci-failures-tool.eli16.md +21 -0
- package/upgrades/reaper-busy-orphan-detect.eli16.md +33 -0
- package/upgrades/reaper-cpu-aware-keep.eli16.md +30 -0
- package/upgrades/side-effects/burn-detector-activity-gate.md +110 -0
- package/upgrades/side-effects/dev-ci-failures-tool.md +54 -0
- package/upgrades/side-effects/reaper-busy-orphan-detect.md +85 -0
- package/upgrades/side-effects/reaper-cpu-aware-keep.md +90 -0
- package/upgrades/side-effects/statemanager-listsessions-cache.md +72 -0
- package/upgrades/statemanager-listsessions-cache.eli16.md +36 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: minor -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
The `SessionReaper` gains an **observe-only** busy-orphan detector behind a new
|
|
9
|
+
dark-by-default flag `busyOrphanDetection` — the inverse of #722's
|
|
10
|
+
`cpuAwareActiveProcessKeep`, closing the blind spot that fix can't reach.
|
|
11
|
+
|
|
12
|
+
**The gap.** #722 made the `active-process` keep require *positive CPU progress*
|
|
13
|
+
under load, so a wedged/**idle** child (≈0 CPU) no longer pins an idle session.
|
|
14
|
+
But CPU-progress is only a *proxy* for "useful" — a **useless process that
|
|
15
|
+
SPINS** (a hot-loop, a wedged job burning CPU) looks "active" and would keep the
|
|
16
|
+
session pinned. So #722 catches the idle orphan; it does not catch the *busy*
|
|
17
|
+
orphan.
|
|
18
|
+
|
|
19
|
+
**The detector.** Under CPU pressure, when a session is kept ONLY by an
|
|
20
|
+
`active-process` veto whose child is provably **burning CPU** (the descendant
|
|
21
|
+
CPU-seconds delta is above the idle floor) yet the session ITSELF looks fully
|
|
22
|
+
idle (a positive ready-prompt + a flat, non-growing transcript) across an
|
|
23
|
+
extended dwell (`busyOrphanConfirmTicks`, default 5 ≈ 10 min), the reaper records
|
|
24
|
+
a `busy-orphan-suspected` row in the decision audit (`logs/reaper-audit.jsonl`),
|
|
25
|
+
and a `busy-orphan-cleared` row when a confirmed suspect recovers.
|
|
26
|
+
|
|
27
|
+
**It NEVER changes the keep/kill decision.** This is pure observation — it makes
|
|
28
|
+
the "useless-but-busy child pins an idle session" case *measurable* so that safe
|
|
29
|
+
auto-reclaim can graduate later with real data (the instar observe-first → act
|
|
30
|
+
pattern). Zero false-kill risk by construction.
|
|
31
|
+
|
|
32
|
+
**Conservative by construction:**
|
|
33
|
+
- No-op at `normal` pressure, when the CPU dep is absent, on the first sighting
|
|
34
|
+
(no transcript-growth baseline yet), and whenever the transcript is unresolved
|
|
35
|
+
or growing — all resolve to "not a suspect".
|
|
36
|
+
- Only the `active-process` keep-reason is considered; every earlier reason
|
|
37
|
+
(recent-user, commitment, recovery, …) is untouched.
|
|
38
|
+
- Emits the suspect row exactly ONCE — the tick the dwell crosses the threshold —
|
|
39
|
+
not every tick after (no per-tick audit flood).
|
|
40
|
+
|
|
41
|
+
Ships dark fleet-wide; live on development agents via the `developmentAgent`
|
|
42
|
+
gate (same as #722), so it's dogfooded on a real loaded box.
|
|
43
|
+
|
|
44
|
+
`StateManager.listSessions()` now serves from a short-TTL in-memory cache instead
|
|
45
|
+
of re-reading and re-parsing every session file from disk on every call.
|
|
46
|
+
|
|
47
|
+
**The bug (a systemic CPU hot-loop).** `listSessions()` did `readdirSync` +
|
|
48
|
+
`readFileSync` + `JSON.parse` of **every** session JSON file on **every** call —
|
|
49
|
+
and it's called each tick by the SessionReaper and the sentinels via
|
|
50
|
+
`listRunningSessions()`. So the cost was O(sessions × pollers × tick-rate) disk
|
|
51
|
+
reads, which kept agent servers spinning at 50–67% CPU (a CDP profile showed ~30%
|
|
52
|
+
of server CPU was literally `readFileUtf8`, with `listSessions` on top). It was
|
|
53
|
+
systemic (every agent with sessions + active pollers) and survived restarts —
|
|
54
|
+
which is exactly why bouncing a hot server never helped.
|
|
55
|
+
|
|
56
|
+
**The fix.** A 1-second TTL cache on the full session list. The many sub-second
|
|
57
|
+
read-only callers within a tick now share one disk read; the cache is invalidated
|
|
58
|
+
on every session write (`saveSession` / `removeSession`), so a freshly spawned or
|
|
59
|
+
terminated session is visible on the very next call — no staleness for the cases
|
|
60
|
+
that matter. `listSessions` returns shallow copies, so a caller can't corrupt the
|
|
61
|
+
shared cache. StateManager is the sole in-process writer of session files, so the
|
|
62
|
+
invalidation is complete (cross-machine writes carry at most the 1s TTL of
|
|
63
|
+
staleness, which the reaper/scheduler already tolerate).
|
|
64
|
+
|
|
65
|
+
Net effect: the per-tick session-directory churn collapses to near-zero, fixing
|
|
66
|
+
the agent-server CPU hot-loop fleet-wide with no behavior change.
|
|
67
|
+
|
|
68
|
+
## What to Tell Your User
|
|
69
|
+
|
|
70
|
+
Nothing — it is dark by default and observe-only (it never changes behavior, just
|
|
71
|
+
writes audit rows). If surfaced at all, surface it as Experimental observability
|
|
72
|
+
that makes the busy-orphan case visible.
|
|
73
|
+
|
|
74
|
+
Nothing required — it's an internal performance fix. If your agent's server was
|
|
75
|
+
running hot / the machine load was high with no obvious cause, this is a likely
|
|
76
|
+
culprit and it's now fixed; servers idle much cooler.
|
|
77
|
+
|
|
78
|
+
## Summary of New Capabilities
|
|
79
|
+
|
|
80
|
+
- `monitoring.sessionReaper.busyOrphanDetection` (boolean, default false;
|
|
81
|
+
`developmentAgent` agents default true) — observe-only flagging of idle
|
|
82
|
+
sessions pinned by a CPU-burning child.
|
|
83
|
+
- `monitoring.sessionReaper.busyOrphanConfirmTicks` (number, default 5) — the
|
|
84
|
+
consecutive-suspect-tick dwell before a row is emitted.
|
|
85
|
+
- New audit events: `busy-orphan-suspected`, `busy-orphan-cleared`.
|
|
86
|
+
|
|
87
|
+
- None (no new API, route, or config). `StateManager.listSessions()` is now cached
|
|
88
|
+
(1s TTL, write-invalidated); behavior is unchanged, cost drops from O(files) per
|
|
89
|
+
call to one read per second of read-only traffic.
|
|
90
|
+
|
|
91
|
+
## Evidence
|
|
92
|
+
|
|
93
|
+
- Motivated by a real review catch (the gap a busy/hot orphan defeats the
|
|
94
|
+
CPU-progress proxy) and a live instance (agent servers hot-looping at ~50% CPU
|
|
95
|
+
while otherwise idle).
|
|
96
|
+
- Tests: `tests/unit/session-reaper-busy-orphan.test.ts` — 9 tests, both sides of
|
|
97
|
+
every boundary (flags only on busy-child + idle-session + dwell; never on
|
|
98
|
+
CPU-flat / working-frame / flag-off / off-pressure / no-dep / pre-dwell; emits
|
|
99
|
+
once; clears on recovery; verdict always unchanged) + a wiring-integrity
|
|
100
|
+
assertion. The 56 existing reaper + #722 tests stay green.
|
|
101
|
+
|
|
102
|
+
- Pinned with node's inspector: SIGUSR1 → CDP CPU profile of a hot agent server
|
|
103
|
+
showed 30.8% `readFileUtf8` + 7% `listSessions` (StateManager.js:151), called via
|
|
104
|
+
the reaper/sentinel ticks — the symbolicated JS frame macOS `sample` couldn't show.
|
|
105
|
+
- Tests: `tests/unit/state-manager-listsessions-cache.test.ts` — 6 tests (cache HIT
|
|
106
|
+
within TTL; MISS after TTL; immediate invalidation on saveSession + removeSession;
|
|
107
|
+
status-filter on cached data; copy-safety; corrupt-file skip). The 71 existing
|
|
108
|
+
StateManager tests + 255 consumer tests (reaper, scheduler, restart-all, recovery)
|
|
109
|
+
stay green.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# ELI16: Burn-detector activity gate (stop the repeating token-burn noise)
|
|
2
|
+
|
|
3
|
+
Instar watches how many tokens each part of an agent is using. If one part suddenly hogs more than a
|
|
4
|
+
quarter of a whole day's spend, it raises a heads-up: "a component is using more than a quarter of
|
|
5
|
+
the token budget." That's the **token-burn detector**, and it's meant to catch a runaway loop or an
|
|
6
|
+
unexpected cost spike early.
|
|
7
|
+
|
|
8
|
+
## The bug
|
|
9
|
+
|
|
10
|
+
The detector only asked one question: "Over the last 24 hours, did one part use more than 25% of the
|
|
11
|
+
total?" It never asked the obvious follow-up: "Is that still happening right now?"
|
|
12
|
+
|
|
13
|
+
So here's what went wrong. One big working session ran for a while, used a lot of tokens, and then
|
|
14
|
+
finished. For the next 24 hours that finished session still counted as a big slice of the trailing-day
|
|
15
|
+
total — so the alarm kept going off, over and over, every time the cooldown expired. And because the
|
|
16
|
+
session had stopped, the alarm even said "Projected 0 tokens in next 24h" — it was literally
|
|
17
|
+
announcing "this thing is burning a lot" and "this thing is spending nothing right now" in the same
|
|
18
|
+
breath. That's the noise the user saw: a full day of contradictory, useless heads-ups for something
|
|
19
|
+
that was already over.
|
|
20
|
+
|
|
21
|
+
## The fix
|
|
22
|
+
|
|
23
|
+
Add one missing check: only raise the alarm if the component is **actually spending right now** —
|
|
24
|
+
meaning it used tokens in the last hour. A big 24-hour share with a zero current rate is a finished
|
|
25
|
+
burst, not a live burn, so it stays quiet. The detector's other trigger (a sudden spike versus a
|
|
26
|
+
component's own 7-day normal) already had this kind of "is it active now" check; this just brings the
|
|
27
|
+
25%-share trigger up to the same standard.
|
|
28
|
+
|
|
29
|
+
## What already existed vs. what's new
|
|
30
|
+
|
|
31
|
+
- **Already there:** the burn detector, the 25% trigger, the 1-hour spend lookup, and the throttle
|
|
32
|
+
runbook that decides whether to just alert or actually slow a component down.
|
|
33
|
+
- **New:** (1) the activity gate on the 25% trigger; (2) an operator off-switch and tuning knobs under
|
|
34
|
+
a burn-detection config block, so anyone can silence or retune the alerts without a code change; (3)
|
|
35
|
+
a cleaner alert message (the old internal "Phase 3/Phase 4" wording is gone); (4) a one-paragraph
|
|
36
|
+
note added to every agent's instructions on update so any agent can explain the alert and offer the
|
|
37
|
+
off-switch.
|
|
38
|
+
|
|
39
|
+
## Safeguards (plain terms)
|
|
40
|
+
|
|
41
|
+
- The change only ever makes the alarm **quieter** — it adds a condition that must also be true before
|
|
42
|
+
alerting. It can never cause a new false alarm.
|
|
43
|
+
- The detector is still just a *reporter*. It doesn't slow anything down or block anything by itself;
|
|
44
|
+
a separate piece decides whether to act. That separation is unchanged.
|
|
45
|
+
- All the new config is optional. If you set nothing, you get the fixed-but-sensible defaults.
|
|
46
|
+
- Rolling it back is a one-line revert with no data cleanup. Worst case, the old noise comes back —
|
|
47
|
+
there's no risk to correctness or to stored data.
|
|
48
|
+
|
|
49
|
+
## What's NOT fixed (on purpose)
|
|
50
|
+
|
|
51
|
+
It still labels un-attributed spend as `unknown::<some-id>`. That just means "we couldn't pin this
|
|
52
|
+
spend to a named component" — it's not a problem by itself, and giving those a real name is a bigger,
|
|
53
|
+
separate piece of work.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# `instar dev:ci-failures` — ELI16
|
|
2
|
+
|
|
3
|
+
Viewable private artifact:
|
|
4
|
+
https://echo.dawn-tunnel.dev/view/41bc3523-a95d-4038-bd43-af8a942e0008?sig=e785278aea6a2e82502cc55fe56b98e55ab4cef543b71ddf32c953db7985c191
|
|
5
|
+
|
|
6
|
+
When a pull request's automated tests go red, you want to know *which* test broke and where.
|
|
7
|
+
Normally you'd open the CI logs — but in some setups the command that fetches those logs comes
|
|
8
|
+
back completely empty, so a red run tells you "something failed" and nothing else. I hit this
|
|
9
|
+
repeatedly while shipping fixes this run, and had to re-discover a workaround each time.
|
|
10
|
+
|
|
11
|
+
This turns that workaround into a permanent tool. `instar dev:ci-failures <pr>` reads a
|
|
12
|
+
*different* GitHub endpoint (the per-check "annotations") that still reports the exact failing
|
|
13
|
+
file, line number, and assertion message even when the logs are blank. It also tidies the
|
|
14
|
+
output: it merges the duplicate failure that the Node-20 and Node-22 test shards both report,
|
|
15
|
+
and it hides the generic CI-runner noise ("Process completed with exit code 1").
|
|
16
|
+
|
|
17
|
+
It's read-only and informational — it never changes anything, it just shows you the failures.
|
|
18
|
+
Pairs with `instar dev:preflight` as the contributor dev-loop toolkit.
|
|
19
|
+
|
|
20
|
+
_Tier-2 · branch `echo/dev-ci-failures-tool` · the first of the "turn this run's friction into
|
|
21
|
+
infrastructure" improvements._
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Catching the "busy but useless" process
|
|
2
|
+
|
|
3
|
+
## The one-sentence version
|
|
4
|
+
Last fix (#722) taught the cleanup robot to ignore a *frozen* leftover program. But a leftover program can also be *spinning uselessly* — burning the engine while going nowhere — and that one still looks "busy." This change makes the robot **notice and write down** those cases (without touching anything yet), so we can see how often it happens before we ever let it act.
|
|
5
|
+
|
|
6
|
+
## Back to the garage
|
|
7
|
+
Earlier rule: "don't tow a car just because it has an engine — only if the engine is also cold (off)." Good for the junked car with a dead battery.
|
|
8
|
+
|
|
9
|
+
But what about a car parked in its spot with **the engine revving, nobody inside, going nowhere** — a stuck throttle? Its engine is hot, so the "is the engine cold?" test says *keep it*. Yet it's just as useless as the dead one, and it's burning fuel (CPU) and making noise (load) the whole time.
|
|
10
|
+
|
|
11
|
+
This change doesn't tow that car. It puts a **note on the windshield**: "spot occupied by a revving-but-empty car for 10+ minutes." Collect enough notes and we'll know whether stuck-throttle cars are a real problem worth a tow rule — backed by data, not a guess.
|
|
12
|
+
|
|
13
|
+
## What it actually does
|
|
14
|
+
- Watches for a session that is **sitting idle** (at its ready prompt, transcript not growing) while a child program **keeps burning CPU**.
|
|
15
|
+
- After that's been true for a while (a dwell, ~10 minutes), it writes one line: `busy-orphan-suspected`.
|
|
16
|
+
- When the situation clears, it writes `busy-orphan-cleared`.
|
|
17
|
+
- That's the whole feature. **It never reaps, kills, nudges, or notifies.** Pure note-taking.
|
|
18
|
+
|
|
19
|
+
## Why note-taking first (instead of just towing)
|
|
20
|
+
The one honest reason *not* to tow a revving car: maybe the driver started it on purpose to warm it up and stepped away — i.e. an agent legitimately kicked off a real background job and went back to its prompt. Telling those two apart reliably is hard. So instead of risking a wrong tow, we **measure first**: gather real examples, learn the signatures, and only then design a safe auto-action. That's the instar way — observe, then act.
|
|
21
|
+
|
|
22
|
+
## The safety rails
|
|
23
|
+
1. **Changes no decisions.** The keep/kill logic is byte-for-byte the same; this only adds an audit line.
|
|
24
|
+
2. **Only under load**, only when CPU is actually measurable, only after a real dwell — never on a hair trigger.
|
|
25
|
+
3. **One note per episode**, not per tick — no log flood.
|
|
26
|
+
4. **Dark everywhere except me** (the development agent), so it proves itself on a real loaded machine first.
|
|
27
|
+
|
|
28
|
+
## Why it matters
|
|
29
|
+
Together with #722 this closes both halves of "a process pins an idle session": the *idle* leftover (#722 can now reclaim it) and the *busy* leftover (this makes it visible). You can't responsibly manage machine load you can't see — this is the seeing.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
**Rendered (verified HTTP 200):** https://echo.dawn-tunnel.dev/view/a6ad4a97-c59b-4a61-81c8-4d2aa8d3d7b4?sig=333db8be8c2b9624668753699e48aaa03a5b208ae963c841293ae4ba4ec0c5f7
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Why the reaper got smarter about "busy" sessions
|
|
2
|
+
|
|
3
|
+
## The one-sentence version
|
|
4
|
+
Before, the cleanup robot thought a session was *busy* if it had **any** helper program still attached — even a frozen, do-nothing one. Now, when the machine is overloaded, it also checks whether that helper is *actually using the CPU*. A frozen helper no longer keeps a dead-idle session alive and hogging the machine.
|
|
5
|
+
|
|
6
|
+
## Picture it
|
|
7
|
+
Think of a parking garage that only tows cars when it's full. The old rule: "never tow a car if its engine is installed." Trouble is, a junked car with a cold, dead engine *still has an engine installed* — so it sat there forever, taking a spot while the garage overflowed.
|
|
8
|
+
|
|
9
|
+
The new rule (only when the garage is full): "don't tow a car just because it has an engine — tow it only if the engine is also **stone cold** (not running) AND the driver's seat is empty AND nothing's happening inside." A car that's actually running, or has someone in it, is never touched.
|
|
10
|
+
|
|
11
|
+
## What changed, precisely
|
|
12
|
+
- The reaper used to keep a session whenever a child process **existed**.
|
|
13
|
+
- Now, **under CPU load only**, it also asks: did that child burn any CPU recently?
|
|
14
|
+
- If the child is **flat (≈0 CPU)** *and* the session is sitting idle at a ready prompt *and* its transcript isn't growing → it becomes reclaimable.
|
|
15
|
+
- If the child is doing real work (CPU rising), or the session might be mid-task → it's kept, exactly like before.
|
|
16
|
+
|
|
17
|
+
## The safety rails (why this won't kill your work)
|
|
18
|
+
1. **Off when the machine is calm.** Zero change unless CPU pressure is real.
|
|
19
|
+
2. **Never kills on the CPU check alone** — it still has to pass the "is it truly idle?" proof and the slow, repeated-confirmation + budget gates.
|
|
20
|
+
3. **Can't measure? Keep it.** First look, missing data, weird readings → always keep.
|
|
21
|
+
4. **Only the cleanup robot changed.** The guard that stops *other* things from killing your sessions is untouched.
|
|
22
|
+
5. **Dark by default.** Off across the fleet; on only for the development agent (me) so I can prove it on a real loaded machine first.
|
|
23
|
+
6. **Leaves a paper trail.** Every time it relaxes the rule it writes a line you can read in `logs/reaper-audit.jsonl`.
|
|
24
|
+
|
|
25
|
+
## Why it matters
|
|
26
|
+
Idle sessions with a stuck helper (a hung MCP server, a wedged `codex` job) were quietly piling up and starving the machine — which is what makes new sessions fail to spawn and messaging go silent. This lets the reaper finally reclaim exactly those, without ever risking a session that's actually working.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
**Rendered (verified) view:** _set below after creating the tunnel view._
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Side-Effects Review — Burn-detector absolute-share activity gate
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `burn-detector-activity-gate`
|
|
4
|
+
**Date:** `2026-06-03`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `not required`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
The `BurnDetector` absolute-share trigger fired whenever one attribution key crossed 25% of the
|
|
11
|
+
trailing-24h token spend, with no check that the key was still spending. A heavy session that
|
|
12
|
+
finished hours ago kept re-tripping the alarm every cooldown for a full 24h with self-contradictory
|
|
13
|
+
"consumed 67% of 24h spend … Projected 0 tokens" messages. This change adds an activity gate
|
|
14
|
+
(`tokens1h > absoluteShareActivityFloorTokens`, default 0) so the absolute-share trigger only fires
|
|
15
|
+
for a key actively spending in the last hour. It also exposes the previously-hardcoded
|
|
16
|
+
burn-detection config as `monitoring.burnDetection.*` (master off-switch + tuning knobs), cleans the
|
|
17
|
+
alert text, and adds a content-sniffed `PostUpdateMigrator` awareness section. Files touched:
|
|
18
|
+
`src/monitoring/BurnDetector.ts`, `src/server/AgentServer.ts`, `src/core/types.ts`,
|
|
19
|
+
`src/scaffold/templates.ts`, `src/core/PostUpdateMigrator.ts`, plus
|
|
20
|
+
`tests/unit/burn-detection-phase-3.test.ts` and `tests/unit/PostUpdateMigrator-tokenBurnAlerts.test.ts`.
|
|
21
|
+
|
|
22
|
+
## Decision-point inventory
|
|
23
|
+
|
|
24
|
+
- `BurnDetector.tick` absolute-share trigger — **modify** — adds an AND-condition (`isActivelySpending`)
|
|
25
|
+
before the existing `share > threshold` test. Strictly narrows when the trigger fires; never widens.
|
|
26
|
+
- `AgentServer` burn-detection startup — **modify** — reads `monitoring.burnDetection`; `enabled:false`
|
|
27
|
+
skips the whole subsystem; other knobs are passed through to `BurnDetector` / `BurnThrottleRunbook`
|
|
28
|
+
only when explicitly set (partial configs built without `undefined` keys so a default is never clobbered).
|
|
29
|
+
- `PostUpdateMigrator.migrateClaudeMd` — **add** — one idempotent, content-sniffed section append.
|
|
30
|
+
|
|
31
|
+
## 1. Over-block
|
|
32
|
+
|
|
33
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
34
|
+
|
|
35
|
+
A genuine live burn that happens to log zero tokens in the most recent hour would be gated out. In
|
|
36
|
+
practice this can't be a sustained burn — `tokens1h` is computed from a real 1h ledger query, so a
|
|
37
|
+
key actively spending necessarily has positive last-hour tokens. The only "rejected" case is a key
|
|
38
|
+
whose 24h share is high but whose current rate is zero, which is by definition not a live burn. The
|
|
39
|
+
default floor of 0 is the most conservative choice: it suppresses only the provably-finished case.
|
|
40
|
+
|
|
41
|
+
## 2. Under-block
|
|
42
|
+
|
|
43
|
+
**What failure modes does this still miss?**
|
|
44
|
+
|
|
45
|
+
It does not improve attribution — spend still labelled `unknown::<sessionId>` (resolver ran, found no
|
|
46
|
+
named component) is a coverage gap, not a burn, and is out of scope here. A burst that ends but
|
|
47
|
+
leaves a small warm-session cache trickle in the last hour (tokens1h slightly > 0) would still fire
|
|
48
|
+
if its 24h share exceeds the threshold; operators who see that can raise
|
|
49
|
+
`absoluteShareActivityFloorTokens`. The baseline-divergence trigger is unchanged.
|
|
50
|
+
|
|
51
|
+
## 3. Level-of-abstraction fit
|
|
52
|
+
|
|
53
|
+
**Is this at the right layer?** Yes. The gate lives inside `BurnDetector.tick` beside the existing
|
|
54
|
+
`rollingBaselineFloor` activity check on the sibling trigger — same layer, same shape. The config
|
|
55
|
+
read lives in `AgentServer` where every other monitoring subsystem is wired. The migration lives
|
|
56
|
+
beside the other `migrateClaudeMd` sections. No re-implementation of an existing primitive.
|
|
57
|
+
|
|
58
|
+
## 4. Signal vs authority compliance
|
|
59
|
+
|
|
60
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
61
|
+
|
|
62
|
+
- [x] No — this change produces a signal consumed by an existing smart gate.
|
|
63
|
+
|
|
64
|
+
`BurnDetector` is a signal producer: it emits to `DegradationReporter`; the `BurnThrottleRunbook` is
|
|
65
|
+
the authority that decides alert-vs-throttle. This change makes the signal *more* conservative (fewer
|
|
66
|
+
false positives) — it adds no blocking authority and removes none. The off-switch is operator config,
|
|
67
|
+
not runtime decision logic.
|
|
68
|
+
|
|
69
|
+
## 5. Interactions
|
|
70
|
+
|
|
71
|
+
- **Shadowing:** The activity gate runs before the baseline-divergence trigger in the same key loop.
|
|
72
|
+
When absolute-share is gated out, baseline-divergence is still evaluated normally (and has its own
|
|
73
|
+
`rollingBaselineFloor` gate), so a finished burst produces zero signals from both — confirmed by the
|
|
74
|
+
new "finished burst" unit test.
|
|
75
|
+
- **Double-fire:** None. One signal per key per cooldown, unchanged.
|
|
76
|
+
- **Races:** None. `BurnDetector` reads the ledger; it shares no mutable state with concurrent code.
|
|
77
|
+
- **Feedback loops:** The runbook self-attribution prefix (`burn-throttle-runbook::`) remains exempt,
|
|
78
|
+
so throttle activity can't feed back into the detector.
|
|
79
|
+
|
|
80
|
+
## 6. External surfaces
|
|
81
|
+
|
|
82
|
+
The user-visible Telegram alert text changes (drops the "Phase 3/Phase 4" jargon; "at current rate"
|
|
83
|
+
→ "at the current rate"). The `BurnVerifier.extractTokensLast1h` regex that parses the projected
|
|
84
|
+
number back out of the alert still matches the new wording (the captured group precedes the changed
|
|
85
|
+
suffix) — verified against existing fixtures. Volume of alerts drops sharply (finished bursts no
|
|
86
|
+
longer re-alarm). New optional `monitoring.burnDetection` config keys; absence preserves defaults.
|
|
87
|
+
Existing agents get the CLAUDE.md awareness section on next update via the idempotent migration. No
|
|
88
|
+
database/ledger schema change.
|
|
89
|
+
|
|
90
|
+
## 7. Rollback cost
|
|
91
|
+
|
|
92
|
+
Pure code + docs change. Revert and ship as the next patch. No persistent state, no data migration,
|
|
93
|
+
no agent-state repair. During the rollback window the only regression is the return of the original
|
|
94
|
+
noise — there is no correctness or data risk. An operator who needs immediate relief without a
|
|
95
|
+
release can set `monitoring.burnDetection.enabled: false` and restart sessions.
|
|
96
|
+
|
|
97
|
+
## Conclusion
|
|
98
|
+
|
|
99
|
+
The review surfaced no blocking concerns. The change narrows an over-firing signal-producer with a
|
|
100
|
+
gate that mirrors an existing sibling check, adds an operator off-switch, and is fully reversible by
|
|
101
|
+
revert or config. The only residual is the unchanged attribution-coverage gap (`unknown::<id>`),
|
|
102
|
+
explicitly out of scope and tracked as a separate, larger effort. Clear to ship.
|
|
103
|
+
|
|
104
|
+
## Evidence pointers
|
|
105
|
+
|
|
106
|
+
- `tests/unit/burn-detection-phase-3.test.ts` — 3 new activity-gate tests (both sides of the boundary
|
|
107
|
+
+ the configurable-floor path); 65 burn-detection tests green across phases 3–6.
|
|
108
|
+
- `tests/unit/PostUpdateMigrator-tokenBurnAlerts.test.ts` — 5 new migration tests (add / idempotent /
|
|
109
|
+
preserve / skip-missing / template-emits).
|
|
110
|
+
- `tsc --noEmit` clean.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Side-Effects Review — `instar dev:ci-failures`
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `dev-ci-failures-tool`
|
|
4
|
+
**Date:** `2026-06-03`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `not required (Tier-2 — new read-only dev CLI command; no runtime/server surface, no decision boundary)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
New `instar dev:ci-failures <pr>` CLI command (`src/commands/devCiFailures.ts`, wired in
|
|
11
|
+
`src/cli.ts`) that prints a PR's failing tests via the GitHub check-run annotations API.
|
|
12
|
+
Read-only: it shells out to `gh` GET endpoints only. Adds a manifest catalog entry
|
|
13
|
+
(`cli:dev-ci-failures`, colon-free id per Codey's #716 convention fix) + awareness docs
|
|
14
|
+
(CLAUDE.md, templates.ts, site cli reference). No server route, no config, no state.
|
|
15
|
+
|
|
16
|
+
## Decision-point inventory
|
|
17
|
+
|
|
18
|
+
1. **Annotations API vs log parsing.** Chose the check-run *annotations* endpoint because
|
|
19
|
+
`gh run view --log[-failed]` returns 0 bytes in some environments (the friction that
|
|
20
|
+
motivated this). Annotations carry `path:line` + the assertion even when logs are empty.
|
|
21
|
+
2. **Diagnostic, not a gate (exit 0 even with failures).** It informs; it does not fail the
|
|
22
|
+
shell on a red PR. Exit 1 is reserved for operational errors (PR unresolvable / API
|
|
23
|
+
error) so a script can tell "couldn't run" from "ran, here are the failures."
|
|
24
|
+
3. **Shard-pair dedup.** The node-20 / node-22 shard checks report the identical failure;
|
|
25
|
+
the command de-dupes by the formatted line so each distinct failure prints once.
|
|
26
|
+
4. **Noise filter.** Drops `.github/...` annotations and `Process completed with exit code N`
|
|
27
|
+
(CI-runner artifacts, not test failures). When a failed check has no test-level
|
|
28
|
+
annotations (a build/lint/type step), it prints a note rather than silently showing nothing.
|
|
29
|
+
|
|
30
|
+
## 1. Blast radius / reversibility
|
|
31
|
+
|
|
32
|
+
Additive: one new command file + one cli.ts command block + a manifest entry + doc lines.
|
|
33
|
+
Fully reversible. No existing behavior touched. Read-only external calls (gh GET).
|
|
34
|
+
|
|
35
|
+
## 2. Failure modes
|
|
36
|
+
|
|
37
|
+
- `gh` not installed / not authenticated → the command surfaces the gh error and exits 1.
|
|
38
|
+
- An annotations-endpoint hiccup for one check → that check is skipped, others still print.
|
|
39
|
+
- A PR with no failed checks → prints "No failed checks", exit 0.
|
|
40
|
+
All handled; none crash.
|
|
41
|
+
|
|
42
|
+
## 3. Manifest / docs
|
|
43
|
+
|
|
44
|
+
`cli:dev-ci-failures` added to the generator's command list → colon-free manifest id
|
|
45
|
+
(passes the type:name convention). CLAUDE.md and `templates.ts`'s `generateClaudeMd` were
|
|
46
|
+
edited in sync so `verify-deployed-templates` stays green (locally confirmed). Manifest
|
|
47
|
+
`generatedAt` timestamp aside, the generator output is deterministic.
|
|
48
|
+
|
|
49
|
+
## 4. Tests
|
|
50
|
+
|
|
51
|
+
`tests/unit/devCiFailures.test.ts` — pure `extractFailureLines` (both sides: keeps real
|
|
52
|
+
failures, drops warnings/`.github`/`Process completed`, truncates long messages, handles
|
|
53
|
+
missing path) + `runDevCiFailures` with injected `gh` deps (red PR prints the test; shard
|
|
54
|
+
dedup; green PR; unresolvable PR → exit 1; build-step-with-no-test-annotations note). 10 tests.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Side-Effects Review — SessionReaper observe-only busy-orphan detection
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `reaper-busy-orphan-detect`
|
|
4
|
+
**Date:** `2026-06-03`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `not required (observe-only — never changes a keep/kill verdict; reaper-only; dark by default)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Under CPU pressure, when a session is kept by an `active-process` veto whose
|
|
11
|
+
child is provably burning CPU (cpuFlat===false) yet the session itself is idle
|
|
12
|
+
(positive idle prompt + flat transcript) across an extended dwell, the reaper
|
|
13
|
+
records a `busy-orphan-suspected` audit row (and `busy-orphan-cleared` on
|
|
14
|
+
recovery). It is the inverse of #722's `cpuAwareActiveProcessKeep` and closes the
|
|
15
|
+
gap where a *busy* useless process defeats the CPU-progress proxy. Gated by
|
|
16
|
+
`busyOrphanDetection` (dark; dev agents on via `developmentAgent`).
|
|
17
|
+
|
|
18
|
+
## Decision-point inventory
|
|
19
|
+
|
|
20
|
+
1. **Suspect predicate** — flag iff ALL hold: `busyOrphanDetection` on, under CPU
|
|
21
|
+
pressure (tier ≠ normal), `blocked.reason === 'active-process'`,
|
|
22
|
+
`cpuFlat === false` (descendant CPU delta above the idle floor), transcript
|
|
23
|
+
delta `static` (resolved + not grown vs the prior tick), and a positive
|
|
24
|
+
idle-prompt frame. Any miss ⇒ not a suspect.
|
|
25
|
+
2. **Dwell** — emit `busy-orphan-suspected` exactly the tick the consecutive
|
|
26
|
+
suspect streak crosses `busyOrphanConfirmTicks` (default 5). A brief
|
|
27
|
+
background job that finishes within the dwell is never flagged.
|
|
28
|
+
3. **Clear** — when a confirmed suspect (streak ≥ threshold) stops being a suspect,
|
|
29
|
+
emit `busy-orphan-cleared`. Streaks below the threshold reset silently.
|
|
30
|
+
|
|
31
|
+
## 1. Over-block (does it ever reap something it shouldn't?)
|
|
32
|
+
|
|
33
|
+
**No — it cannot reap anything.** This change is observe-only: the keep/kill
|
|
34
|
+
verdict path is untouched. Every code path through the detector returns the
|
|
35
|
+
SAME `keep('active-process')` it returned before; the only effect is an audit
|
|
36
|
+
row. The exact "idle session + legit busy background job" case that
|
|
37
|
+
`cpuAwareActiveProcessKeep` was deliberately conservative about is, here, merely
|
|
38
|
+
*logged* — never acted on. A false suspect costs one JSONL line, nothing more.
|
|
39
|
+
|
|
40
|
+
## 2. Under-block
|
|
41
|
+
|
|
42
|
+
Nothing is loosened. No existing protection, gate, or veto is weakened — the
|
|
43
|
+
shared `ReapGuard`/`ReapAuthority` path and the reaper's own stateful checks are
|
|
44
|
+
entirely unchanged. The detector reads the same already-captured frame and
|
|
45
|
+
transcript (no extra tmux/fs work) and the same `cpuFlat` signal #722 already
|
|
46
|
+
computes.
|
|
47
|
+
|
|
48
|
+
## 3. Blast radius / rollout
|
|
49
|
+
|
|
50
|
+
Dark by default (`busyOrphanDetection: false`). Dev agents get it via the
|
|
51
|
+
`developmentAgent` gate at the server wiring site (riding #722's IIFE). On the
|
|
52
|
+
fleet the flag is false ⇒ the detection branch is never entered and
|
|
53
|
+
`cpuProgressFlat` returns early exactly as before #722 ⇒ identical behavior.
|
|
54
|
+
|
|
55
|
+
## 4. Migration parity
|
|
56
|
+
|
|
57
|
+
No `migrateConfig` — same reasoning as #722: the default resolves at the server
|
|
58
|
+
wiring site via the `developmentAgent` gate, so writing an explicit `false` into
|
|
59
|
+
existing configs would wrongly override the gate on dev agents.
|
|
60
|
+
`busyOrphanConfirmTicks` falls back to the code default. The new-agent template
|
|
61
|
+
(`generateClaudeMd`) gains an awareness line; no `migrateClaudeMd` (dark feature).
|
|
62
|
+
|
|
63
|
+
## 5. Interaction with #722 (cpuAwareActiveProcessKeep)
|
|
64
|
+
|
|
65
|
+
The two are mutually exclusive on the CPU signal and complementary:
|
|
66
|
+
`cpuAwareActiveProcessKeep` acts when `cpuFlat === true` (relaxes the veto);
|
|
67
|
+
busy-orphan detection observes when `cpuFlat === false` (flags). They share the
|
|
68
|
+
`cpuProgressFlat` sampler, whose gate now runs when EITHER feature is on. With
|
|
69
|
+
both off (fleet default), the sampler returns `undefined` immediately and neither
|
|
70
|
+
path engages.
|
|
71
|
+
|
|
72
|
+
## 6. Audit volume
|
|
73
|
+
|
|
74
|
+
`busy-orphan-suspected` fires at most once per suspect episode (on the dwell
|
|
75
|
+
crossing), `busy-orphan-cleared` once on recovery — bounded, not per-tick. Lands
|
|
76
|
+
in `logs/reaper-audit.jsonl` like the other reaper decision rows. Silent (no
|
|
77
|
+
Telegram, no attention item).
|
|
78
|
+
|
|
79
|
+
## 7. What it does NOT do
|
|
80
|
+
|
|
81
|
+
- Does not reap, kill, nudge, or notify — observe-only.
|
|
82
|
+
- Does not run off-pressure or without the CPU dep.
|
|
83
|
+
- Does not change `hasActiveProcesses` or any guard.
|
|
84
|
+
- Does not attempt to *fix* a busy orphan (e.g. tree-kill) — that is a separate,
|
|
85
|
+
deliberately-staged follow-up that would build on this measurement.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Side-Effects Review — SessionReaper CPU-aware active-process keep
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `reaper-cpu-aware-keep`
|
|
4
|
+
**Date:** `2026-06-03`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `not required (reaper-only, dark-by-default, no new external surface)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Under CPU pressure, the reaper's `active-process` existence-veto additionally
|
|
11
|
+
requires positive descendant-CPU progress (sampled across ticks via the #706
|
|
12
|
+
`descendantCpuSeconds` helper). A CPU-flat child no longer keeps an idle session
|
|
13
|
+
un-reapable; the reaper falls through to its existing transcript-growth +
|
|
14
|
+
positive-idle checks. Gated by `cpuAwareActiveProcessKeep` (dark; dev agents on
|
|
15
|
+
via `developmentAgent`). Reaper-only — the shared `ReapGuard`/`ReapAuthority`
|
|
16
|
+
path is untouched.
|
|
17
|
+
|
|
18
|
+
## Decision-point inventory
|
|
19
|
+
|
|
20
|
+
1. **Pressure gate** — `tier === 'normal'` ⇒ never tighten. Off-load behavior is
|
|
21
|
+
byte-for-byte unchanged.
|
|
22
|
+
2. **CPU-flat threshold** — `ratePerSec < cpuActiveMinRatePerSec` (default 0.02).
|
|
23
|
+
Below ⇒ "flat" (relax the veto); at/above ⇒ working (veto stands).
|
|
24
|
+
3. **Can't-tell ⇒ KEEP** — feature off, dep absent, sample error, non-finite read,
|
|
25
|
+
first sighting (no prior delta), or CPU went backwards ⇒ `undefined` ⇒ the veto
|
|
26
|
+
is NOT relaxed.
|
|
27
|
+
4. **Scope of relaxation** — only `blocked.reason === 'active-process'` is relaxed.
|
|
28
|
+
Every earlier keep-reason (protected, recovery, recent-user, commitment,
|
|
29
|
+
subagent, structural-long-work, …) returns first and is never affected.
|
|
30
|
+
5. **Fall-through, not kill** — relaxing the veto does not reap; the stateful
|
|
31
|
+
transcript-growth + positive-idle gates (then hysteresis + two-phase + budget)
|
|
32
|
+
still must all clear.
|
|
33
|
+
|
|
34
|
+
## 1. Over-block (false KEEP removed — could it now reap something it shouldn't?)
|
|
35
|
+
|
|
36
|
+
The only sessions newly reachable for reaping are: idle-at-a-ready-prompt,
|
|
37
|
+
transcript-not-growing, under CPU pressure, whose ONLY surviving keep-reason was
|
|
38
|
+
an existing-but-CPU-flat child. That is precisely the wedged/idle-child case we
|
|
39
|
+
want reclaimed. A genuinely-working session is still protected three ways:
|
|
40
|
+
(a) if any descendant burns CPU the delta is positive ⇒ veto stands;
|
|
41
|
+
(b) `descendantCpuSeconds` includes the main process, so a CPU-burning
|
|
42
|
+
extended-think keeps it; (c) the positive-idle gate requires an affirmative
|
|
43
|
+
ready-prompt with no working-footer, so a mid-turn session (footer present)
|
|
44
|
+
returns `no-positive-idle` and is kept regardless. A wedged-but-not-idle session
|
|
45
|
+
(no ready prompt) is likewise kept by positive-idle — the reaper stays
|
|
46
|
+
conservative there (that case is the StaleSessionBackstop's operator-ask job, not
|
|
47
|
+
the reaper's).
|
|
48
|
+
|
|
49
|
+
## 2. Under-block (did we loosen a real protection?)
|
|
50
|
+
|
|
51
|
+
No protection is loosened for any non-reaper caller. The shared `ReapGuard` used
|
|
52
|
+
by `terminateSession` (the authority that vetoes OTHER killers) is unchanged, so
|
|
53
|
+
external kills still honor the full existence-veto. Only the deliberate,
|
|
54
|
+
pressure-gated, hysteresis-bounded reaper relaxes it, and only under load.
|
|
55
|
+
|
|
56
|
+
## 3. Blast radius / rollout
|
|
57
|
+
|
|
58
|
+
Dark by default (`cpuAwareActiveProcessKeep: false`). Dev agents get it via the
|
|
59
|
+
`developmentAgent` gate at the server wiring site (`?? !!config.developmentAgent`)
|
|
60
|
+
— an explicit config value always wins. On the fleet (no `developmentAgent`) the
|
|
61
|
+
flag is false ⇒ the new code path is never entered ⇒ identical behavior to
|
|
62
|
+
today. Echo (developmentAgent) dogfoods it live on a genuinely loaded box.
|
|
63
|
+
|
|
64
|
+
## 4. Migration parity
|
|
65
|
+
|
|
66
|
+
- **Config:** no `migrateConfig` entry is added *by design*. The default is
|
|
67
|
+
resolved at the server wiring site via the `developmentAgent` gate, NOT written
|
|
68
|
+
into `config.json`. Writing an explicit `false` into existing configs would
|
|
69
|
+
*wrongly override* the gate on dev agents (explicit beats `??`). Existing
|
|
70
|
+
agents therefore receive the correct behavior (dev on, fleet off) through the
|
|
71
|
+
code path with no config write. `cpuActiveMinRatePerSec` falls back to the
|
|
72
|
+
`DEFAULT_SESSION_REAPER_CONFIG` value for agents without the field.
|
|
73
|
+
- **CLAUDE.md:** the new-agent template (`generateClaudeMd`) gains an awareness
|
|
74
|
+
line. No `migrateClaudeMd` entry — the feature is dark on the fleet, so existing
|
|
75
|
+
agents don't need the awareness until a future fleet-promotion change (which
|
|
76
|
+
will add it), per the maturity-honesty principle (don't document a dark feature
|
|
77
|
+
to existing agents as if it's live).
|
|
78
|
+
|
|
79
|
+
## 5. Observability
|
|
80
|
+
|
|
81
|
+
Each veto-relaxation emits a `cpu-keep-tightened` row to `logs/reaper-audit.jsonl`
|
|
82
|
+
(tier, verdict, keptBy, the active threshold). A kill-path behavior change leaves
|
|
83
|
+
a durable trail — never silent.
|
|
84
|
+
|
|
85
|
+
## 6. What it does NOT do
|
|
86
|
+
|
|
87
|
+
- Does not change `hasActiveProcesses` itself (so `UpdateGate`, `SessionManager`
|
|
88
|
+
idle-detection, and `SessionRecovery` are untouched).
|
|
89
|
+
- Does not kill on CPU-flatness alone (always falls through to the idle proof).
|
|
90
|
+
- Does not run off-pressure or when CPU is unmeasurable.
|