instar 1.2.82 → 1.3.0
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/init.js +14 -3
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +143 -1
- package/dist/commands/server.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +23 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts +2 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +268 -3
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/SessionManager.d.ts +43 -0
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +123 -24
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/installCodexHooks.d.ts.map +1 -1
- package/dist/core/installCodexHooks.js +3 -2
- package/dist/core/installCodexHooks.js.map +1 -1
- package/dist/core/types.d.ts +26 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/monitoring/SessionReaper.d.ts +153 -0
- package/dist/monitoring/SessionReaper.d.ts.map +1 -0
- package/dist/monitoring/SessionReaper.js +376 -0
- package/dist/monitoring/SessionReaper.js.map +1 -0
- package/dist/monitoring/TokenLedger.d.ts +12 -0
- package/dist/monitoring/TokenLedger.d.ts.map +1 -1
- package/dist/monitoring/TokenLedger.js +22 -0
- package/dist/monitoring/TokenLedger.js.map +1 -1
- package/dist/monitoring/transcriptProber.d.ts +44 -0
- package/dist/monitoring/transcriptProber.d.ts.map +1 -0
- package/dist/monitoring/transcriptProber.js +57 -0
- package/dist/monitoring/transcriptProber.js.map +1 -0
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +6 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.d.ts +3 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +1 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/routes.d.ts +3 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +20 -2
- package/dist/server/routes.js.map +1 -1
- package/dist/server/stopGate.d.ts +8 -2
- package/dist/server/stopGate.d.ts.map +1 -1
- package/dist/server/stopGate.js +42 -2
- package/dist/server/stopGate.js.map +1 -1
- package/package.json +1 -1
- package/playbook-scripts/build-state.py +39 -1
- package/scripts/analyze-release.js +16 -8
- package/scripts/generate-builtin-manifest.cjs +2 -1
- package/src/data/builtin-manifest.json +76 -67
- package/src/scaffold/templates.ts +6 -0
- package/src/templates/hooks/build-stop-hook.sh +62 -0
- package/src/templates/hooks/settings-template.json +10 -0
- package/upgrades/1.2.83.md +26 -0
- package/upgrades/1.3.0.md +27 -0
- package/upgrades/side-effects/build-stop-hook-session-scoping.md +133 -0
- package/upgrades/side-effects/fresh-session-stop-gate-shadow-wiring.md +35 -0
- package/upgrades/side-effects/session-reaper.md +42 -0
|
@@ -135,6 +135,16 @@
|
|
|
135
135
|
}
|
|
136
136
|
],
|
|
137
137
|
"Stop": [
|
|
138
|
+
{
|
|
139
|
+
"matcher": "",
|
|
140
|
+
"hooks": [
|
|
141
|
+
{
|
|
142
|
+
"type": "command",
|
|
143
|
+
"command": "node .instar/hooks/instar/stop-gate-router.js",
|
|
144
|
+
"timeout": 5000
|
|
145
|
+
}
|
|
146
|
+
]
|
|
147
|
+
},
|
|
138
148
|
{
|
|
139
149
|
"matcher": "",
|
|
140
150
|
"hooks": [
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Upgrade Guide — NEXT
|
|
2
|
+
|
|
3
|
+
<!-- bump: minor -->
|
|
4
|
+
<!-- Valid values: patch, minor, major -->
|
|
5
|
+
<!-- patch = bug fixes, refactors, test additions, doc updates -->
|
|
6
|
+
<!-- minor = new features, new APIs, new capabilities (backwards-compatible) -->
|
|
7
|
+
<!-- major = breaking changes to existing APIs or behavior -->
|
|
8
|
+
|
|
9
|
+
## What Changed
|
|
10
|
+
|
|
11
|
+
**SessionReaper — pressure-aware cleanup of idle-but-alive sessions.** A new monitor that reaps sessions sitting idle at a ready prompt (holding memory) — but ONLY when the machine is under memory pressure, and it NEVER reaps a session that might be working. It requires *positive* proof of idleness (turn complete + at a ready prompt + screen byte-static across several checks + no running process + no transcript growth) and KEEPs on any ambiguity. Ships **OFF + dry-run by default** — the only monitor that kills on a heuristic, so it stays dark until an operator validates the dry-run log and opts in. Closes the gap behind the 2026-05-25 fleet pileup (idle sessions accumulated until the machine starved and cross-agent messaging silently failed because agents could no longer spawn).
|
|
12
|
+
|
|
13
|
+
New read-only endpoint `GET /sessions/reaper` shows the live pressure tier and, per session, the verdict + the exact gate that kept it. `SessionManager` gains a single-writer `terminateSession()` so the existing idle-kill and the reaper can never double-kill. The zombie-kill recovery veto now also defers to the socket + silence sentinels.
|
|
14
|
+
|
|
15
|
+
## What to Tell Your User
|
|
16
|
+
|
|
17
|
+
- **Idle sessions get cleaned up under memory pressure — safely.** When your machine fills up with idle agent sessions, this sweeps them so new sessions (and incoming cross-agent messages) don't get refused. It will never reap a session that's actually working. It's off by default; ask me to turn it on after we watch its dry-run log.
|
|
18
|
+
- **You won't notice anything unless you enable it.** No behavior change on update.
|
|
19
|
+
|
|
20
|
+
## Summary of New Capabilities
|
|
21
|
+
|
|
22
|
+
| Capability | How to Use |
|
|
23
|
+
|-----------|-----------|
|
|
24
|
+
| SessionReaper (idle-session cleanup under pressure) | `monitoring.sessionReaper.enabled:true` (leave `dryRun:true` first). Off by default. |
|
|
25
|
+
| Reaper observability | `GET /sessions/reaper` — pressure tier + per-session verdict + keptBy |
|
|
26
|
+
| Single-writer session termination | `SessionManager.terminateSession()` — idle-kill + reaper share one CAS kill path |
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Upgrade Guide — NEXT
|
|
2
|
+
|
|
3
|
+
<!-- bump: patch -->
|
|
4
|
+
<!-- Valid values: patch, minor, major -->
|
|
5
|
+
<!-- patch = bug fixes, refactors, test additions, doc updates -->
|
|
6
|
+
<!-- minor = new features, new APIs, new capabilities (backwards-compatible) -->
|
|
7
|
+
<!-- major = breaking changes to existing APIs or behavior -->
|
|
8
|
+
|
|
9
|
+
## What Changed
|
|
10
|
+
|
|
11
|
+
**The `/build` stop-hook is now session-scoped — it only nags the session that actually owns the build.** Before, the hook that keeps a build from quitting half-done had no idea *which* of your concurrent sessions started the build, so it fired its "keep working" block into every session — trapping unrelated ones and, worse, spending the owning build's reinforcement budget on each misfire (when that budget hit its cap, the hook stopped protecting the real builder too).
|
|
12
|
+
|
|
13
|
+
Now `build-state.py` stamps the owning session (its tmux session name, and optionally the Claude session UUID) at build start, and the hook blocks **only** the proven owner. Every other session approve-exits without touching the owner's budget. A build with no owner stamp (legacy state) gets a conservative no-adopt: the hook goes quiet rather than guessing — it never traps a session and never claims ownership.
|
|
14
|
+
|
|
15
|
+
The hook ships via the always-overwrite path (the inline `getBuildStopHook()` twin in `PostUpdateMigrator`, kept byte-identical to `src/templates/hooks/build-stop-hook.sh` and asserted by a drift test), so every agent gets it on update.
|
|
16
|
+
|
|
17
|
+
## What to Tell Your User
|
|
18
|
+
|
|
19
|
+
- **No action needed; this just stops cross-talk between your sessions.** If you run more than one session at once, a build in one of them will no longer pester the others or drain its own "keep going" budget. You won't notice anything unless you run concurrent sessions, in which case it gets quieter.
|
|
20
|
+
|
|
21
|
+
## Summary of New Capabilities
|
|
22
|
+
|
|
23
|
+
| Capability | How to Use |
|
|
24
|
+
|-----------|-----------|
|
|
25
|
+
| Session-scoped build stop-hook | Automatic. `build-state.py init` stamps `owner.{tmux,session,stampedAt}`; the hook blocks only the owner. |
|
|
26
|
+
| Owner-stamp flags on `build-state.py init` | `--owner-session "$CLAUDE_CODE_SESSION_ID"` (precision; SKILL wiring is a fast-follow), `--owner-tmux` (override seam). Tmux name is auto-resolved by default. |
|
|
27
|
+
| Conservative no-adopt for un-stamped builds | Automatic. No owner stamp → hook approves without claiming ownership (never traps, never drains). |
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Side-Effects Review — Build Stop-Hook Session-Scoping
|
|
2
|
+
|
|
3
|
+
**Slug:** `build-stop-hook-session-scoping`
|
|
4
|
+
**Date:** `2026-05-26`
|
|
5
|
+
**Author:** Echo
|
|
6
|
+
**Spec:** `docs/specs/BUILD-STOP-HOOK-SESSION-SCOPING-SPEC.md` (converged round 1, approved by Justin via Telegram topic 13352)
|
|
7
|
+
**Second-pass reviewer:** independent general-purpose review agent (3 findings, all incorporated — see spec §"Review Findings Incorporated")
|
|
8
|
+
|
|
9
|
+
## Summary of the change
|
|
10
|
+
|
|
11
|
+
The `/build` Stop hook had no notion of which session owns a build. With one
|
|
12
|
+
shared `build-state.json` and one hook in a checkout, a build started by session
|
|
13
|
+
A fired its "keep working" block into every concurrent session of the same agent
|
|
14
|
+
— trapping unrelated sessions and, on every misfire, incrementing the shared
|
|
15
|
+
`reinforcementsUsed` counter, which drains the owning build's protection budget
|
|
16
|
+
to zero.
|
|
17
|
+
|
|
18
|
+
This change stamps the owning session at `/build` start (`build-state.py init`
|
|
19
|
+
writes `owner.{tmux,session,stampedAt}`) and teaches the hook to block **only**
|
|
20
|
+
the proven owner. Any other session approve-exits **without** incrementing the
|
|
21
|
+
counter. A build with no owner stamp gets a conservative no-adopt (approve,
|
|
22
|
+
never claim ownership) — it never traps a session and never inverts ownership.
|
|
23
|
+
|
|
24
|
+
## Files changed (in gate scope = behavior)
|
|
25
|
+
|
|
26
|
+
- `src/core/PostUpdateMigrator.ts` — the inline `getBuildStopHook()` (the
|
|
27
|
+
shipping artifact; written to `.instar/hooks/instar/build-stop-hook.sh` on every
|
|
28
|
+
migration via always-overwrite, and by `init.ts` via `getHookContent`). Added
|
|
29
|
+
the ownership block between the terminal-phase early-exit and the counter
|
|
30
|
+
mutation.
|
|
31
|
+
- `src/templates/hooks/build-stop-hook.sh` — the canonical reference template +
|
|
32
|
+
builtin-manifest fingerprint. Kept byte-identical to the inline twin (asserted
|
|
33
|
+
by a new drift test).
|
|
34
|
+
|
|
35
|
+
Out of gate scope but part of the change:
|
|
36
|
+
- `playbook-scripts/build-state.py` — `cmd_init` stamps `owner`; new
|
|
37
|
+
`--owner-session` / `--owner-tmux` flags; `resolve_owner_tmux()` helper.
|
|
38
|
+
- `tests/unit/build-stop-hook-session-scoping.test.ts` (new, 12 tests),
|
|
39
|
+
`tests/unit/PostUpdateMigrator-buildStopHook.test.ts` (+1 drift test).
|
|
40
|
+
- `docs/specs/*`, `upgrades/NEXT.md` (docs / release note).
|
|
41
|
+
|
|
42
|
+
## Decision-point inventory
|
|
43
|
+
|
|
44
|
+
- **Added**: hook ownership gate — between terminal-phase exit and counter
|
|
45
|
+
mutation. Decides block (owner) vs approve-no-increment (non-owner / unknown /
|
|
46
|
+
un-stamped). This is the new decision boundary.
|
|
47
|
+
- **Added**: `build-state.py` owner stamp at init (records identity; no runtime
|
|
48
|
+
decision, pure data).
|
|
49
|
+
- **Unchanged**: no-state-file exit, terminal-phase exit, and the
|
|
50
|
+
counter/reinforcement block logic itself (the owner path falls through to the
|
|
51
|
+
exact pre-existing code).
|
|
52
|
+
|
|
53
|
+
## Over-block / under-block analysis
|
|
54
|
+
|
|
55
|
+
- **Over-block risk (trapping a non-owner):** eliminated. A non-owner returns
|
|
56
|
+
`approve` before reaching the counter. The only block path requires a positive
|
|
57
|
+
owner match (tmux or session). Tested: non-owner tmux, non-owner session,
|
|
58
|
+
identity-unknown, and legacy/un-stamped all return approve.
|
|
59
|
+
- **Under-block risk (owner not protected):** bounded and acceptable. The owner
|
|
60
|
+
is protected whenever `owner.tmux` matches the live tmux (the load-bearing
|
|
61
|
+
path, proven live) or `owner.session` matches stdin `session_id`. The only
|
|
62
|
+
under-protection case is an **un-stamped** build (legacy state, or an
|
|
63
|
+
environment where stamping didn't run) — by deliberate design the hook goes
|
|
64
|
+
quiet there rather than guess. Forfeiting protection for a stale build is the
|
|
65
|
+
correct trade vs. trapping the wrong session (spec §"Why conservative-no-adopt").
|
|
66
|
+
- **Bootstrap inversion (the rejected alternative):** an earlier draft let the
|
|
67
|
+
first session to Stop adopt ownership. The independent review showed this
|
|
68
|
+
inverts ownership in the real incident pattern (busy owner never stops first).
|
|
69
|
+
Removed entirely; replaced with conservative no-adopt. Tested: un-stamped state
|
|
70
|
+
yields approve with `owner` NOT written.
|
|
71
|
+
|
|
72
|
+
## Level-of-abstraction fit
|
|
73
|
+
|
|
74
|
+
The fix lives at the same layer as the bug: the Stop hook and the state writer.
|
|
75
|
+
It mirrors the already-shipped autonomous stop-hook's session-scoping ladder
|
|
76
|
+
(tmux-name primary, session-UUID backstop, fail-open) without merging the two
|
|
77
|
+
(explicit non-goal — bash hooks don't share code cleanly; premature abstraction
|
|
78
|
+
avoided). No new module, no new service.
|
|
79
|
+
|
|
80
|
+
## Signal-vs-authority compliance
|
|
81
|
+
|
|
82
|
+
The hook is a low-context filter making a binary ownership decision from
|
|
83
|
+
locally-verifiable identifiers (tmux `#S`, stdin `session_id`). It does not
|
|
84
|
+
arrogate higher-level judgment — it only declines to block a session it cannot
|
|
85
|
+
prove it owns. Conservative-by-construction: every ambiguous case resolves to
|
|
86
|
+
`approve` (release), never to `block` (trap). It emits no user-facing messages.
|
|
87
|
+
|
|
88
|
+
## Interactions
|
|
89
|
+
|
|
90
|
+
- **Reinforcement counter:** the owner path is byte-for-byte the prior logic, so
|
|
91
|
+
graduated protection (3/5/10) is unchanged for the owner. Non-owners no longer
|
|
92
|
+
touch the counter at all.
|
|
93
|
+
- **Restart reconcile:** writes `owner.session` ONLY on a confirmed tmux-owner
|
|
94
|
+
match with a rotated UUID. Gated strictly behind the tmux match — a non-owner
|
|
95
|
+
can never clobber `owner.session` (tested explicitly).
|
|
96
|
+
- **stdin consumption:** the hook now reads stdin (`cat`). Stop hooks deliver and
|
|
97
|
+
close stdin in production (the autonomous hook relies on this), so no hang.
|
|
98
|
+
Even with no stdin/session, tmux-scoping alone is sufficient (proven live).
|
|
99
|
+
- **Worktree topology:** ownership is keyed on the cwd-independent tmux name, so
|
|
100
|
+
it is correct whether the owner launched at the main root and `cd`'d into a
|
|
101
|
+
worktree or launched rooted inside the worktree. The old, fragile `$PWD`-based
|
|
102
|
+
stopgap is NOT carried forward.
|
|
103
|
+
- **Migration parity:** inline twin is always-overwritten → every agent gets the
|
|
104
|
+
new hook on update. `build-state.py` rides the repo checkout (the only place
|
|
105
|
+
the bug occurs — see spec §Migration Parity 2). Drift test prevents
|
|
106
|
+
template/inline divergence.
|
|
107
|
+
|
|
108
|
+
## Rollback cost
|
|
109
|
+
|
|
110
|
+
Low and clean. Revert the two src files (and optionally build-state.py); the
|
|
111
|
+
always-overwrite migration restores the prior hook on next update. The added
|
|
112
|
+
`owner` block in state is additive JSON ignored by the old hook — no destructive
|
|
113
|
+
schema migration. No data loss path.
|
|
114
|
+
|
|
115
|
+
## Tracked deferral
|
|
116
|
+
|
|
117
|
+
The SKILL change to pass `--owner-session "$CLAUDE_CODE_SESSION_ID"` (session-UUID
|
|
118
|
+
precision; + its dedicated PostUpdateMigrator migration per Migration Parity §5)
|
|
119
|
+
is a deliberate fast-follow per the approved phasing (Justin approved one-PR-now +
|
|
120
|
+
tiny-follow-up). The flag is already plumbed and tested in `build-state.py`; only
|
|
121
|
+
the SKILL invocation + migration remain. This is tracked, not orphaned.
|
|
122
|
+
|
|
123
|
+
## Verification
|
|
124
|
+
|
|
125
|
+
- 3-tier behavior tests (12) drive the **real shipping hook** (from
|
|
126
|
+
`getHookContent`) against the **real `build-state.py`** with real stdin/tmux
|
|
127
|
+
seams — covering owner-block, non-owner-no-drain, repeated-non-owner,
|
|
128
|
+
session-only owner, identity-unknown fail-open, legacy no-adopt, restart
|
|
129
|
+
reconcile, anti-clobber, terminal-phase. Plus build-state stamp tests (3) and
|
|
130
|
+
the template/inline drift test (1).
|
|
131
|
+
- Live test-as-self: ran the shipping hook with **real** `tmux display-message`
|
|
132
|
+
resolution (no seam) in this session (`echo-build-stop-hook-session-scoping`) —
|
|
133
|
+
confirmed owner→block, non-owner→approve with zero counter drain.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Side-effects review — fresh-session stop-gate shadow wiring
|
|
2
|
+
|
|
3
|
+
**Scope**: Complete the conservative first rollout for the fresh-session stop-gate: wire the existing server authority/database, install a Stop-hook router, and default to observe-only shadow mode when the gate is healthy.
|
|
4
|
+
|
|
5
|
+
**Files touched**:
|
|
6
|
+
- `src/commands/server.ts` — constructs `StopGateDb` and `UnjustifiedStopGate`, persists mode state, passes both into `AgentServer`.
|
|
7
|
+
- `src/server/stopGate.ts` — persists mode flips to `server-data/stop-gate-mode.json`.
|
|
8
|
+
- `src/server/routes.ts` — records SessionStart rows in `StopGateDb` when hook events arrive.
|
|
9
|
+
- `src/core/PostUpdateMigrator.ts` — installs `stop-gate-router.js` and patches `.claude/settings.json` Stop hooks.
|
|
10
|
+
- `src/commands/init.ts` and `src/templates/hooks/settings-template.json` — include the router on fresh installs.
|
|
11
|
+
- `src/core/installCodexHooks.ts` — mirrors the Stop router into Codex hook config.
|
|
12
|
+
- Focused tests cover route mode persistence, hook behavior, Codex registration, and update migration.
|
|
13
|
+
|
|
14
|
+
**Under-block**: Intentional for this PR. Default mode is `shadow` only when the authority and SQLite log initialize successfully; otherwise the gate is `off`. In shadow mode the hook submits evaluations but always lets the agent exit. Enforcement is reserved for a later explicit operator flip.
|
|
15
|
+
|
|
16
|
+
**Over-block**: Minimal. The router only emits `{decision:"block"}` when the server is already in `enforce` mode and the server authority returns `continue` with a reminder. All network errors, malformed hook payloads, missing config, hot-path failures, compaction-in-flight, kill-switch, and degraded initialization paths fail open.
|
|
17
|
+
|
|
18
|
+
**Signal vs authority**: Compliant. The hook collects evidence metadata and simple signals, but never decides whether a Stop is unjustified. The server-side `UnjustifiedStopGate` remains the sole authority for `continue`; the hook is a transport/router.
|
|
19
|
+
|
|
20
|
+
**External surfaces**:
|
|
21
|
+
- New installed hook file: `.instar/hooks/instar/stop-gate-router.js`.
|
|
22
|
+
- Existing routes become live for real Stop events: `GET /internal/stop-gate/hot-path` and `POST /internal/stop-gate/evaluate`.
|
|
23
|
+
- New persisted mode file: `server-data/stop-gate-mode.json`.
|
|
24
|
+
- Existing SQLite event log: `server-data/stop-gate.db`.
|
|
25
|
+
|
|
26
|
+
**Migration parity**:
|
|
27
|
+
- Post-update migration writes the router and patches existing Claude settings.
|
|
28
|
+
- Fresh `instar init` installs the router template.
|
|
29
|
+
- Codex hook installer places the router first in the Stop chain before the existing review trio.
|
|
30
|
+
|
|
31
|
+
**Rollback cost**: Revert this change set. Existing `stop-gate-mode.json` and `stop-gate.db` files can remain on disk; without the router/server wiring they are inert. Emergency runtime rollback without code revert: `instar gate mode off` or set the kill-switch.
|
|
32
|
+
|
|
33
|
+
**Tests**:
|
|
34
|
+
- `npm test -- --run tests/unit/routes-stopGate.test.ts tests/unit/stop-gate-router-hook.test.ts tests/unit/installCodexHooks.test.ts tests/unit/PostUpdateMigrator-codexHooks.test.ts`
|
|
35
|
+
- `npx tsc --noEmit`
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Side-Effects Review — SessionReaper
|
|
2
|
+
|
|
3
|
+
Spec: `docs/specs/SESSION-REAPER-SPEC.md` (v2 CONVERGED + ratified). Build branch `build/session-reaper`.
|
|
4
|
+
|
|
5
|
+
## What changes for a deployed agent
|
|
6
|
+
|
|
7
|
+
- A new monitor (`SessionReaper`) is constructed and started at server boot. **Default OFF + dry-run** (`monitoring.sessionReaper.enabled:false, dryRun:true`), so deployed agents get **no behavior change** until an operator opts in. New config block arrives via the standard `ConfigDefaults`/`applyDefaults` migration; operator-set values are never overwritten.
|
|
8
|
+
- New read-only endpoint `GET /sessions/reaper` (503 when unwired, 200 snapshot otherwise).
|
|
9
|
+
- `SessionManager` gains `terminateSession()` (single-writer CAS), `isRelayLeaseActive()`, and `markReaping/clearReaping/isReaping`. The existing idle-kill now funnels through `terminateSession` and skips reaping-leased sessions; `killSession` shares the CAS guard and now sets `endedReason` (its event emissions are unchanged — still no `sessionComplete`).
|
|
10
|
+
- The zombie-kill recovery veto (`activeRecoveryChecker`) is recomposed to include the socket + silence sentinels (previously compaction + rate-limit only) — a strict superset; nothing is dropped.
|
|
11
|
+
|
|
12
|
+
## Over/under-block analysis (the hard requirement)
|
|
13
|
+
|
|
14
|
+
The reaper must never reap a working session. Safety rests on positive evidence, not absence of activity:
|
|
15
|
+
- **Under-block (fails to reap a genuinely idle session):** acceptable — the existing 15m/4h idle-kill still runs; the reaper is additive pressure relief.
|
|
16
|
+
- **Over-block (reaps a working session):** the failure that matters. Mitigations: (1) requires a *positive* turn-complete idle-prompt signal; (2) render-stasis — pane byte-identical across all confirm ticks; (3) process + transcript must be quiet, and any *unresolvable* signal (no `claudeSessionId`, Codex/missing/rotated transcript, uninspectable process) forces KEEP, never "quiet"; (4) hysteresis; (5) two-phase reap with a final-grace re-check that aborts on any frame change; (6) Normal pressure tier reaps nothing; (7) bounded per-tick/per-hour budget; (8) auto-disable to dry-run on any ambiguous/failed reap; (9) ships OFF + dry-run.
|
|
17
|
+
|
|
18
|
+
## Level-of-abstraction / signal-vs-authority
|
|
19
|
+
|
|
20
|
+
Signals carry confidence and only *recommend*; kill authority sits behind the budget + dry-run + single-writer `terminateSession` CAS + auto-disable. The reaper computes a verdict; it does not own an unbounded kill.
|
|
21
|
+
|
|
22
|
+
## Interactions
|
|
23
|
+
|
|
24
|
+
- Composes with (does not fight) existing watchdogs: gate G defers to any recovery-in-flight (now incl. socket/silence); disjoint from OrphanProcessReaper (untracked procs) and SessionWatchdog (active-but-stuck); shares the single-writer kill path with the idle-kill so no double-kill / double-event.
|
|
25
|
+
- Pressure source is freemem-tiered for v1 (advisory; macOS under-reports). Crucially, an over-eager pressure tier can only reap a *genuinely-idle* session sooner — it cannot cause a working session to be reaped, because the classifier protects working sessions independent of tier.
|
|
26
|
+
|
|
27
|
+
## Rollback
|
|
28
|
+
|
|
29
|
+
Set `monitoring.sessionReaper.enabled:false` (the default) — fully inert. No data migration; `endedReason` is additive/optional. Revert the branch to remove code; no persisted state needs cleanup beyond an optional `state/session-reaper.json` (absent unless restart-durability is later wired).
|
|
30
|
+
|
|
31
|
+
## Tests
|
|
32
|
+
|
|
33
|
+
3-tier: unit (transcript prober, terminateSession CAS, classifier incl. every false-reap vector, config/migration), integration (`/sessions/reaper` + dry-run), e2e (feature-alive + dangerous cases). Wiring-integrity guards the construct→start→pass chain. Live test-as-self on a real in-flight build + a real Codex session precedes merge.
|
|
34
|
+
|
|
35
|
+
## Phase-3 review fixes (post multi-agent code review)
|
|
36
|
+
|
|
37
|
+
Independent review confirmed NO blocker to the hard requirement (cannot reap a working session) and surfaced safety-net hardening, all applied:
|
|
38
|
+
- **Reaping-lease leak:** when a matured reap is budget/tier-gated, the reaping lease is now released — previously it could permanently disable the fast idle-kill for that session.
|
|
39
|
+
- **Protected-list wiring:** gate A now reads `SessionManager.getProtectedSessions()` (the resolved list including the `<project>-server` default) rather than the raw config field, preventing spurious auto-disable when the server session goes idle.
|
|
40
|
+
- **Robustness:** `tick()` and `snapshot()` treat a throwing protect-signal as KEEP — never reap on a failed evaluation, and the `/sessions/reaper` route never 500s.
|
|
41
|
+
- **`killSession` contract preserved:** unconditional pane kill retained (only the in-flight guard added; no terminal-status early-return).
|
|
42
|
+
- **Known v1 gap (documented, not a false-reap vector):** the optional `mainProcessActive` CPU/IO-delta signal is not wired in v1; render-stasis is the real-time liveness channel that covers in-process work. Promoting `mainProcessActive` is a tracked enhancement, validated during the dry-run rollout.
|