# CPU load incident — agent session sprawl (2026-08-11)

status: ACTIVE (owner resumed work; task #6 executing)
owner: main session
source requests:
- owner, 2026-08-11 14:20 — "another incident: cpu load is 100% for the last few hours again"
- owner, 2026-08-11 — attached-session safety correction: `ipz-wip-triage` disappeared while owner was attached; recover it and prevent recurrence

task IDs: #1, #2, #3, #4, #5, #6

## Outcome

Laptop usable under normal agent workload, and the load source no longer accumulates
silently across days.

## Measured state at incident open (14:21)

- 16 cores. load avg 74/66/71. CPU 58 us / 39 sy / 0.9 idle. PSI cpu some avg60 = 65.8.
- RAM 40G/61G used, swap 32G/77G used → swap thrash contributing to the 39% sys time.
- 1352% of 1600% consumed. Breakdown: claude 628%, tsserver 281%, bun 115%, other 351%.
- 56 top-level `claude` sessions, 108 claude processes, 27 tmux sessions on
  `~/.local/state/human-session/tmux.sock` (15 attached, 12 detached).
- 22 `tsserver` processes, 8.4 GB RSS, top two at 2249 MB and 1963 MB.
- 61 `*.jsonl.live` transcripts; only 2 written in the last 10 minutes → ~59 sessions
  idle yet each burning 5–40% CPU.

## Root cause (measured, not inferred)

Two independent contributors:

1. **Session sprawl.** Sessions accumulate across days (oldest 101 h) and are never
   reaped. Detached tmux sessions idle >24 h were still running full claude trees.
2. **Idle burn.** An idle claude session is not free: 34 claude-core processes drew
   512% combined while idle, plus 115% across 38 `quietmode` plugin processes and one
   orphaned `claude daemon run --origin transient` at 101%. tsserver adds ~1 GB RSS per
   session that ever opened a TS file.

## Restoration performed

| action | result |
| --- | --- |
| Killed 22 tsserver + 12 typescript-language-server + nemo | freed 8.4 GB RSS |
| Killed 7 detached tmux sessions idle >20 h (ThemeFactory-010205, claude-…013255Z, claude-…013552Z, claude-…070126Z, international-press-zone-160700, international-press-zone-175927, multideal-163053) | 27 → 20 tmux sessions |
| Attempted reap of orphaned ThemeFactory daemon (pid 110791, 101% CPU) | **BLOCKED** by the kill guard |

Result: 1352% → ~1180% busy, load 74 → 51, RAM 40 → 31 G, swap 32 → 23 G,
PSI cpu some avg10 68 → 41.

## Cleanup mechanism — shipped (owner request, 14:50: "we need a cleanup mechanism … when a
session is idle and detached from tmux it should exit automatically")

`agent-sessions reap` + `agent-session-reap.timer` (5-minute interval) landed as
`78f98264` and was deployed, then disabled after attached-session safety report. Original
implementation selected live, detached, transcript-idle, CPU-idle sessions and used direct
process signals plus `tmux kill-session`; hardening below replaces that unsafe termination
path before timer reactivation.

First installed run stopped 5 sessions (one idle 2 h, four idle 46–47 h) and took the
orphaned ThemeFactory daemon with them. Load 74 → 18, busy 1352% → 798%.

**Bug found and fixed on the way:** `tmuxHasAttachedClient` asked
`display-message -p -t <session> '#{session_attached}'`, which resolves its format against a
*client* and so answers empty for any session nobody is looking at — every attached session
read as detached. A dry run flagged two live owner sessions. It now reads `list-sessions -F`
and fails closed: an unreadable tmux server counts as attached, never as reapable.

## Attached-session safety incident — DONE

Manual cleanup terminated `international-press-zone-175927` while owner reports its
`ipz-wip-triage` Claude conversation was attached. Automatic cleanup journal does not list
this IPZ session; unsafe action was manual tmux termination, not a proven timer reap.
Conversation transcript remains intact at Claude session
`19389435-1c18-4602-9935-323388f8b11e`; original tmux process/window is gone.

### Acceptance criteria

1. Disable `agent-session-reap.timer` until every criterion below passes.
2. Relaunch session UUID `19389435-1c18-4602-9935-323388f8b11e` under owner-visible
   `ipz-wip-triage`; prove named tmux session exists and has resumable conversation state.
3. Cleanup MUST bind process identity and tmux session identity across classification and
   termination. PID reuse, missing server/session, malformed output, command failure, or
   changed identity MUST keep session alive.
4. Cleanup MUST atomically prove exact identity and zero attachments before tmux removal.
   Any attachment or identity change MUST abort cleanup; cleanup MUST NOT signal a PID.
5. Deterministic regressions MUST cover real attachment, attachment after classification,
   pane/session/server replacement, simulated PID reuse, unreadable state, and helper crash.
6. Install candidate runtime, prove attached sessions survive through installed entrypoint,
   then and only then re-enable timer.

### Preserved WIP

- Worktree: `/home/user/Projects/overdeck/.worktrees/cpu-load-incident`
- Recovery transcript: `/home/user/.claude/projects/-home-user-Projects-Press-zone-wordpress-wp-content-plugins-international-press-zone/19389435-1c18-4602-9935-323388f8b11e.jsonl`
- Existing landed cleanup: `78f98264`; keep disabled, do not delete ledger/transcripts.

### Execution steps

1. Restoration owner `main`: disable timer; restore named conversation; prove attachability.
2. Hardening owner `main`: reproduce races; implement identity-bound fail-closed checks.
3. Run focused and installed tests; adversarial review; re-enable only after live proof.
4. Full module gates; commit; land; deploy landed source; repeat installed proof.


`tmux-spawn-17d97d5b-…scope` holds only leftovers of the **already-killed**
ThemeFactory tmux session: the transient daemon (101% CPU), two bg-pty hosts, a stale
`versions/2.1.226` process and two quietmode processes. Both `kill` and
`systemctl --user stop <scope>` are refused by the human-slice kill guard, which has no
override. The guard is correct in intent but over-broad: it protects a scope whose
owning tmux session no longer exists. Durable fix = teach the guard (or a reaper) that
a `tmux-spawn-*` scope with no live tmux session is reapable.

## Orphaned tmux scope cleanup — DONE

Source requirement: owner requested a durable cleanup mechanism for `tmux-spawn-*` scopes left
behind after their tmux server dies; future incidents MUST NOT require broad/manual process kills.

Acceptance criteria:

1. Only transient `tmux-spawn-<UUID>.scope` units with the exact tmux-generated description are
   eligible. Missing/malformed properties or unexpected cgroup placement MUST keep the scope.
2. Recorded pane PID MUST be absent from the exact recorded tmux server, or that server MUST be gone,
   before and after recursive scope freeze. Unreadable socket state, non-tmux PID reuse, or any matching
   live pane MUST keep the scope.
3. Bind unit invocation ID, control-group path, and opened cgroup directory before action. Revalidate
   them plus tmux pane absence after freeze; unit/pane replacement or property drift MUST keep scope.
4. Cleanup acts only through the bound cgroup FD; NEVER kill by process name or unresolved PID.
   Normal session reap MUST empty the same bound exclusive scope after atomic tmux removal so
   daemonized descendants cannot survive. Independent watchdog MUST thaw on helper death.
5. Deterministic regressions cover live tmux owner, absent owner, malformed identity, property race,
   helper crash/thaw, dry-run, and successful orphan cleanup.
6. Disable timer before candidate installation; prove installed live-owner survival and orphan cleanup,
   then land/deploy/reinstall and re-arm only after landed installed proof.

## Hardening

1. Reaper for `tmux-spawn-*` scopes whose tmux session is gone — closes the guard gap.
2. DONE — idle+detached session reap, above.
3. Investigate per-session idle burn (claude-core ~15% each while idle; quietmode
   plugin 115% across 38 processes) — this is the multiplier that makes sprawl fatal.
4. Consider a concurrent-session cap; this is at least the third recurrence.
5. LSP policy: tsserver per session on this monorepo costs ~1 GB; disable or share.

## Two defects found, not yet fixed

- `quietmode/start.mjs` (pid 437215) holds a steady 100% of one core inside a live session;
  38 quietmode processes drew 115% combined. Task #2.
- `deckctl units apply` aborts on `agent.slice`, which `packaging/deploy-local.sh` installs as
  a regular file while `units apply` demands a symlink. Two owners for one unit; the reap
  units had to be linked by hand. Pre-existing, unrelated to this incident.
- The kill guard resolves only literal words, so `tmux -S "$S" kill-session` walks straight
  past it while the same command with the socket spelled out is refused. Task #3.

### Task #3 acceptance delta

1. Deny every destructive tmux server/session/window/pane command from agent execution; raw
   tmux destruction has no safe pathname authorization because socket aliases and check/use races
   prevent identity proof.
2. Enforce after shell expansion through the inherited PATH shim, not only by parsing PreToolUse
   source text. Hosted Claude sessions MUST NOT receive the shim's human-at-TTY bypass.
3. Cover variables, quote concatenation, wrappers, nested shells, absolute paths, unique command
   abbreviations, socket aliases, and direct argv. Unknown or malformed destructive forms fail closed.
4. Preserve owner tmux commands outside agent runtimes. Inside agent runtimes, allow only `tmux -V`:
   measured tmux behavior proves custom aliases can shadow both built-in aliases and exact canonical
   command names, so no server command can be classified as non-destructive before execution.
5. Install atomically with rollback, prove the actual installed hook and shim, then land, deploy, and
   repeat installed proof.

## Task #6 — disk-fill notifier CPU/memory burn

Acceptance criteria:

1. Act only on exact `disk-fill-notify.service` after binding its unit, MainPID, cgroup, and command.
   NEVER signal by process name or touch owner sessions.
2. Contain current load by stopping only that service; verify inactive state and measure CPU/memory relief.
3. Locate canonical owning source before changing behavior; NEVER reconstruct deleted code from guesses.
4. Reproduce the retirement defect, add a deterministic regression, install corrected state atomically with
   exact rollback, and prove the obsolete unit remains absent.
5. Land/deploy reviewed source, repeat installed proof, and NEVER re-arm the superseded notifier.

Preserved state and rollback:

- Exact unit: `disk-fill-notify.service`; observed MainPID `2726887`, Python 3.14 executable,
  command `/usr/bin/python3 /home/user/.local/bin/disk-fill-notify`.
- Initial accounting: `MemoryCurrent=11691462656`, `CPUUsageNSec=24755004733000`.
- Original unit target is preserved at
  `~/.local/state/overdeck/rollback/disk-fill-notify-20260812/disk-fill-notify.service`.
- Canonical contract is `modules/monitor/SPEC.md`: system-monitor supersedes and retires both legacy
  notifiers. Correct rollback restores the recorded unit link only; it does not invent the missing script.

## Current receipt

- `ipz-wip-triage` restored with exact Claude UUID; owner attachment proven.
- Timer remained disabled throughout attached-session candidate validation.
- Candidate removes direct PID signals and requires launch-time UUID identity.
- pidfd + `SIGSTOP` candidate was rejected: tmux resumes stopped pane leaders.
- Replacement uses an exact per-session cgroup freezer plus an out-of-scope crash watchdog.
  Helper refuses shared/unrecognized scopes, binds pidfd + start ticks + cgroup + tmux fingerprint,
  freezes before atomic zero-attachment removal, and always thaws.
- New human sessions launch in unique `human-agent-<UUID>.scope`; confined and unsafe launchers
  already create unique eligible scopes. Legacy/shared sessions remain ineligible.
- Exact-worktree regressions pass: reaper safety (including helper `SIGKILL` thaw),
  `run-all.sh agent-session` 5/5, `run-all.sh human-session` 3/3, ledger 85/85, human
  launcher 18/18.
- Review-found ledger collision is fixed: full UUID IDs, exclusive reservation/publication,
  retry on collision, no overwrite. Human launcher now refuses any pre-existing prebound tmux
  target instead of accepting or retagging it. Deterministic regressions cover both paths.
- Review-found runtime replacement race is fixed: helper now binds classified runtime PID + start
  ticks with pidfd, proves scope membership, freezes recursively, and revalidates runtime identity
  before the atomic tmux close. Stale runtime identity regression passes.
- Exact reaper test and both session clusters pass after the fix; independent adversarial review
  is clean.
- Candidate bin/lib runtime was live through `~/.claude`; installed CLI proved a detached eligible
  synthetic session becomes non-reapable while a real tmux client is attached. Candidate service
  ran with `Result=success`; real-ledger dry run found zero eligible sessions.
- A concurrent Overdeck deploy advanced the deploy clone to `dd4b4260` and correctly repointed
  `~/.claude/bin`/`lib` away from the unlanded candidate. Timer was immediately disabled again;
  its one intervening run emitted no reap rows and exited cleanly. At that checkpoint timer state was
  `not-found` / inactive; it remained disabled until landed deployed provenance contained the fix.
- Bare full-suite runner correctly refused on workstation because it includes two exhaustion-class
  dangerlab suites. Both lab-only suites passed in dangerlab (14/14 shim gate plus forkbomb suite).
  Candidate merged 55 current `origin/main` commits without code conflict; both focused clusters
  remain green.
- Broad safe sweep: 71 passed, 13 baseline failures, zero path overlap with this branch's 12-file
  delta. Failures are stale unrelated contracts/config (`agent-build-path`, `agent-unsafe`, CPU/local
  dispatch, remote build, seat slice), six Bun-only prompt tests incorrectly launched by the generic
  Node runner, and Trello timing. `agent-build-path` reproduces unchanged on deployed `origin/main`.
  These do not invalidate the focused reaper/ledger/human-session gates; none is suppressed.
- Hardening landed on main as `dc8b1c473` and deployed successfully from that exact clean commit.
  Installed `~/.claude/bin/agent-sessions`, reader library, service, and timer all resolve into the
  landed deploy clone.
- Final installed-entrypoint attached-client proof passed against landed runtime:
  `installed-entrypoint-attached-survival: ok`.
- Landed service completed with `Result=success`, emitted no reap/error rows, and timer is now
  `enabled` + `active`. Cleanup is armed only after installed proof, satisfying criteria 1–6.
- Post-arm load is 9.88/11.45/12.28 on 16 cores; CPU PSI avg60=18.03 and memory PSI remains zero.
- Orphan-scope candidate binds exact unit invocation/cgroup inode, proves pane absence before and after
  recursive freeze, and empties only the bound cgroup. Normal session reap now empties daemonized
  descendants after atomic tmux close. Recovery watchdog survives its own `SIGKILL`, distinguishes
  clean helper exit from helper `SIGKILL`, thaws when the exact tmux identity remains, and cleans an
  independently re-proven orphan after helper death.
- Deterministic candidate tests cover live panes, pane appearance, malformed/unreadable tmux and
  systemd state, owner PID reuse, unit-property and cgroup-directory replacement, dry-run, recursive
  descendants, helper/watchdog `SIGKILL`, and CLI ledger-write failure after successful cleanup.
  Focused reaper test passes; session clusters pass `5/5` and `3/3` (agent-session cluster required
  its documented out-of-jail invocation; the in-jail refusal was rerun correctly, not suppressed).
- Candidate installed through `~/.claude/bin` + `~/.claude/lib` worktree links after timer removal.
  Installed entrypoint proof created one exact live-pane scope and one absent-owner orphan scope in the
  same run: live scope survived, orphan scope was recursively removed, result
  `installed-reaper-live-pane-and-orphan: ok`. Temporary fixture and proof script were removed.
- Active timer is `not-found` / inactive pending landed deployment. Concurrent deploys may repoint
  Claude runtime links; installed proof asserted candidate realpath before invocation rather than
  trusting link state.
- Independent review required three adversarial rounds. Fixed findings: recovery freezes before
  revalidation; empty/malformed or changed live tmux identity keeps scope; ambiguous cgroup-kill
  failure stays watchdog-armed; ledger intent is locked, file-flushed, rename-atomic, and directory-
  fsynced before destruction; linked windows fail closed through atomic `window_linked=0` predicate.
  Final independent verdict: clean.
- Deploy reported shim drift in unrelated active worktrees. Live shims match `origin/main`, so this
  does not contaminate deployed provenance; do not alter other owners' preserved WIP. Track through
  each owning worktree rather than treating the warning as benign or suppressing it.
- Orphan cleanup landed directly on main as `db23a26d703fea99517914888e9acbd43862bbcf` after
  focused regressions and final independent review passed. Deploy clone resolves to that exact commit.
- Landed installed-entrypoint proof passed: live pane survived and exact absent-owner scope was removed
  (`landed-installed-reaper-proof: ok`). Service completed with `Result=success` / `ExecMainStatus=0`;
  timer is `enabled` and `active`.
- General `packaging/deploy-local.sh` stopped after backing up then removing
  `~/.claude/buildbox-hosts.json`; `deckctl sync apply claude buildbox-hosts.json` failed closed because
  the source was absent. Restored the exact `20260811T151731Z` backup with mode `0600`; linked only the
  landed reaper units manually. This separate deployment defect is unresolved and not suppressed.
- Post-arm load remained excessive: 26 active `tmux-spawn-*` scopes were retained because cleanup
  could not prove them orphaned. Initial consumer sample found active Claude workers plus RustDesk.
- Task #2 root cause is confirmed in QuietContext storage: one worker accumulated a 1.03 GB DB plus
  540 MB WAL, including a 104.7 MB single chunk and 618 MB trigram index, while synchronous FTS
  `optimize` ran every 50 inserts and on close. Its process tree exited naturally; no owner session
  was signalled during this investigation.
- Isolated QuietContext candidate caps sources at 8 MiB before parsing/indexing, byte-caps Markdown,
  plain-text, and JSON chunks, and removes synchronous full-index optimization. Remote focused tests
  pass 3/3; full store suite passes 124/124; TypeScript/build/bundle assertions pass. Independent
  review is executing before atomic installed-runtime proof.
- QuietContext task #2 subsequently landed as `694821f4a4ef49b6495483a796e50fe7e03c833d`;
  landed source is installed and actual MCP entrypoint rejection/index/search proof passed.
- Task #3 candidate now enforces tmux destruction at two layers: PreToolUse catches static and
  absolute-path forms; new inherited `tmux` PATH shim evaluates expanded argv and never grants the
  hosted-human exemption. Regressions pass 82/82; shared shim invariants pass 12/12 with only the
  documented dangerlab-only fork test skipped.
- Final independent review found a remaining custom-alias bypass: tmux accepts abbreviated
  `command-alias` option names, and a pre-existing custom alias can override a trusted built-in alias.
  Isolated-server measurement confirmed both `display` and exact canonical `list-sessions` can be
  shadowed. Therefore no server command spelling is intrinsically safe: candidate must deny every tmux
  server command inside agent runtimes, allow only `tmux -V`, and preserve unguarded owner tmux use only
  when runtime ancestry plus human-session classification prove the caller is outside an agent.
  Candidate remains uninstalled; live runtime and rollback state are unchanged.
- Final independent review rejected the PATH-only design on three grounds: absolute `/usr/bin/tmux`
  bypass, forgeable same-UID `human.slice` classification after reparenting, and fail-open behavior when
  the guard module is missing. Missing-guard execution now fails closed. The first two findings are
  architectural: a same-UID PATH shim is defense-in-depth, not an unbypassable authority boundary.
  Task #3 remains uninstalled and MUST move enforcement to a runtime boundary that absolute paths and
  reparenting cannot escape before shipment.
- Task #6 root cause is confirmed in `modules/monitor/install.sh`: the system-monitor contract retires
  both legacy notifiers, but the installer disabled only `mem-pressure-notify.service`, deleted the
  `disk-fill-notify` script, and left its already-running service alive indefinitely. That deleted-code
  Python process reached 11.7 GB cgroup memory and 24,755 seconds cumulative CPU.
- Exact bound unit `disk-fill-notify.service` / invocation
  `dd23a62b0a3e49ba8629c1965d5a0878` was stopped without touching any other process. The service is now
  `not-found` / inactive / dead. `systemctl disable` removed its repo-owned unit symlink before reporting
  the already-stopped unit not loaded; rollback was immediately reconstructed from the previously read
  exact target at `~/.local/state/overdeck/rollback/disk-fill-notify-20260812/disk-fill-notify.service`.
- Task #6 candidate disables both legacy services before removing both scripts and unit links. Initial
  dry-run regression passed 1/1; monitor tests passed 159/159, agent-guard tests 33/33, compile check and
  installer dry run passed. Independent review then found five blocking gaps: disable failures were
  ignored, dangling symlinks survived, dry-run home differed from live, uninstall restored only one unit,
  and the regression asserted labels rather than effects.
- Shell transaction review is NOT clean despite green tests. Six blocking defects remain: partial staging
  copies can be mistaken for valid backups; archive publication is not transaction-safe; reruns overwrite
  original service-state receipt; pipeline-subshell rollback status is false; restore rollback omits prior
  service state; tests do not inject copy/remove/reload/publication/restore failures or real `not-found`.
- Task #6 stdlib replacement passed 9 focused + 200 monitor + 33 guard tests, but independent review
  rejected shipment on six remaining defects: rollback generation publishes after destructive mutation;
  no post-removal/reload inactivity proof; `linked`/`failed` service rollback is not exact; archived objects
  lose UID/GID; recreated destination directories become root-owned; required removal/publication/enable/
  rollback/crash fault injections are absent. Candidate remains uninstalled.
- Sixth-review corrections implemented: archive flock serializes retire/restore and first receipt publication;
  rollback restores captured originals before services and never publishes stale backup copies; absent directory
  metadata is skipped; symlink timestamps and supported xattrs are copied/restored. Seventeen focused tests pass.
- Seventh adversarial review found three rollback defects: retirement's inner capture failure left captures for
  outer double-restore; failed restore left newly published files whose original destination was absent; capture
  cleanup did not consume entries incrementally, so a later cleanup failure retried deleted captures and could skip
  service rollback. All three are corrected with state-consuming capture helpers, absent-destination unlink, and
  rollback failure aggregation that always attempts service-state restoration. Deterministic regressions added;
  nineteen focused tests pass. Latest broad gates pass: 177 monitor pytest, 33 guard unittest, Python compile,
  installer/uninstaller shell syntax. Eighth independent adversarial review has not inspected code yet: first launch
  lacked its local persona, second parsed the persona delimiter as a CLI option; third launch uses the global persona
  plus explicit option terminator. That reviewer then incorrectly attempted nested dispatch and produced no verdict.
  Direct independent Opus review found two defects: restore rollback can delete a concurrent post-publication
  replacement because it did not bind the published leaf identity; failed restore left transaction-created target
  directories that were originally absent. Both are corrected: publication identities gate rollback unlink; created
  directories are inode-bound and removed deepest-first. Automated security review also flagged pathname re-traversal;
  bound directories now walk from a validated home FD with `openat` + `O_NOFOLLOW` rather than reopening absolute
  paths. Two deterministic regressions added; twenty-one focused tests pass.
- Ninth review found four durability/rollback defects: capture rename can precede unrecorded fsync failure; leaf
  publication can precede unrecorded fsync failure; capture deletion can become irreversible before its fsync; failed
  restore leaves archived metadata on pre-existing directories. Current action: move durability sync to transaction
  state that already records each mutation, make cleanup the commit boundary rather than a rollback-capable phase,
  restore original directory metadata on failure, add fault-injection regressions, then re-review. All four are fixed:
  capture/publication state is recorded before durability sync; capture cleanup is the explicit commit boundary rather
  than a rollback-capable phase; failed restore restores pre-existing directory metadata. Two fault regressions added;
  twenty-three focused tests pass.
- Tenth review found two directory-creation transaction defects: partial creations are lost when `ensure_directories`
  raises before returning its journal; a directory recorded absent may appear concurrently and be accepted/mutated.
  Current action: pass a caller-owned incremental creation journal, exclusively claim every expected-absent component
  with `mkdirat`, reject concurrent presence, add deterministic regressions, and re-review. Both are fixed with a
  caller-owned incremental journal and exclusive expected-absent claims. Two regressions added; twenty-five focused
  tests pass.
- Eleventh review found two remaining directory races: a successful `mkdirat` is journaled after fallible sync/open;
  expected-present directories are not compared to their recorded inode before mutation. Current action: journal the
  created inode immediately after the exclusive claim, validate every expected-present component against its recorded
  identity, retain validated descriptors through publication, add regressions, and re-review. Both are corrected: failed
  post-claim open/fstat removes the exclusive claim; successful claims are journaled before parent sync; every expected-
  present component is dev/inode validated. Two regressions added; twenty-seven focused tests pass.
- Twelfth review found three defects: capture transaction state is memory-only across process crash; object identity can
  miss same-inode/same-size writes with restored mtime; ownership restoration after mode can clear setgid/setuid bits.
  Current action: add a durable fsynced intent manifest recovered under the archive lock, bind content with ctime/digest,
  apply ownership before mode, add crash/content/setgid regressions, and re-review. All three are addressed: a durable
  fsynced manifest records backup/capture/publication/directory/service state and is recovered under the archive lock;
  SHA-256 binds content; ownership precedes mode restoration. Crash/content/setgid regressions added; thirty focused
  tests pass.
- Thirteenth review found three manifest state-machine gaps: capture deletion precedes durable committed phase; restore
  publication precedes durable publication intent; created directories are journaled only after creation completes.
  Current action: persist commit before cleanup, persist expected portable publication identity before rename and
  reconcile both capture/destination states, atomically claim directories through journaled hidden siblings, add crash
  regressions at every boundary, then re-review. All three manifest boundaries are corrected; thirty focused tests
  pass.
- Fourteenth review found two gaps: generic missing-component creation skipped the durable callback; committed cleanup
  did not bind capture identity. Both are corrected: every creation journals before parent sync; every capture's portable
  identity is persisted after capture and verified before deletion. Thirty focused tests pass.
- Fifteenth review found three recovery defects: rollback recovery did not validate persisted capture identity; committed
  cleanup could wedge after an unlink-before-manifest-clear crash; backup validation accepted `..`. Current action:
  validate captures before recovery promotion, persist per-capture cleanup completion/accept proven absence, strictly
  confine backup names/inodes beneath archive, add regressions, and re-review. All three are corrected; thirty focused
  tests pass.
- Sixteenth review found four defects: `current` accepts dot/symlink generations; capture validation+unlink remains
  raceable; cleanup journals before directory sync; recovery accepts unsafe `created` paths. Current action: strictly
  validate/open generation by archive FD, atomically move verified captures to transaction-owned trash before deletion,
  fsync before journaling completion, validate every manifest-relative path, add regressions, and re-review. All four
  are corrected; thirty focused tests pass.
- Seventeenth review found three gaps: expected-absent leaf capture names can be unjournaled; recovery treats a
  journaled-but-not-durable created directory as fatal; directory metadata accepts empty/dot paths targeting home.
  All three are corrected. Capture names are durably journaled before rename and normalized to `None` after a
  proven absent leaf; missing journaled created directories are accepted as already cleaned; empty/dot directory
  receipt paths are rejected. Four direct regressions added. Python compile, 34 focused tests, 192 full monitor
  tests, 84 kill-guard tests, and installer/uninstaller shell syntax pass without warnings. Live service remains
  `not-found` / inactive / dead with `MainPID=0` and empty cgroup. Eighteenth independent adversarial review found
  two shipping blockers outside the transaction owner: root install and uninstall follow user-controlled home symlinks,
  allowing privileged writes/deletion outside home. Both are fixed through descriptor-relative no-follow operations
  with ownership checks; symlink regressions prove no outside write/deletion. Nineteenth review found three remaining
  defects: directory creation can race into `fchown` of a substituted inode; failed first retirement leaves its staged
  generation advertised by `current`; a crash between `mkdir` and journaling leaves residue. Current action: drop helper
  privileges before target-home access, split generation staging from committed `current` publication, and journal
  creation intent. Twentieth review proved those fixes incomplete: crash between `current` publication and committed
  manifest wedges recovery; restored-directory substitution can still receive privileged `fchown`; identity-less intent
  can delete a concurrent directory. Current action: eliminate restore-time directory creation entirely (retirement never
  removes directories), fail closed if externally removed, and add a recoverable publication phase. Twenty-first review
  found four defects, two shipping blockers: temporary-file metadata is applied by pathname after its descriptor closes;
  recovery lacks recorded directory dev/inode and deterministically wedges. It also found identityless managed-link
  deletion and portable-only capture cleanup identity. Twenty-second review found two committed-cleanup defects:
  crash after trash unlink but before manifest update wedges recovery, and committed recovery validates capture names
  too late, allowing absolute-path manifest entries. Those and a later source-backup pathname TOCTOU are fixed with
  early capture validation and retained `O_NOFOLLOW` source descriptors. Latest reviews found three more blockers:
  identities omitted ownership/xattrs; privileged backup copies were unbounded; uninstall left installed user services
  and links active. Ownership/xattr identity, bounded copy, fail-closed managed-link publication, target-directory
  ownership, pre-hash admission, xattr preservation, and absent-empty restore filtering are implemented; 37 focused
  tests pass. The legacy root uninstall receipt gap predates this incident change and is recorded but stays outside the
  exact notifier-retirement scope; changed user-home removals are identity-gated. Twenty-fifth review found three defects:
  exact existing managed symlink ownership was unchecked; symlink xattrs were omitted; regular-file identity hashing
  reopened an admitted path. All three are fixed with uid/gid validation, symlink xattr copy, and descriptor-bound
  O_NOFOLLOW identity hashing with pre/post size checks. Twenty-sixth review found three more defects: managed unlink
  did not validate ownership; restore publication copied regular sources without inode/size binding; symlink xattr errors
  could be ignored. Fixed with uid/gid unlink validation, descriptor-bound 16 MiB exact restore copy, strict object-type
  admission, and fail-closed symlink xattr propagation. Twenty-seventh review found empty restore indexed absent parent
  descriptors and symlink backup still suppressed xattr publication failures. Fixed by explicitly journaling/skipping
  proven absent source+destination pairs and making backup xattr propagation fail closed. Final independent review: CLEAN.
  Latest gates: focused 37/37, monitor 195/195, kill-guard 84/84, Python compile, shell syntax, diff check, and installer
  dry-run clean. Full installer live attempt failed closed before notifier mutation because existing managed user links correctly
  target the deployed checkout rather than this candidate worktree; no object was overwritten. Current action: install only
  the reviewed notifier-retirement seam through its exact privileged entrypoint. Exact seam attempt failed closed before
  mutation because live `mem-pressure-notify.service` has valid `UnitFileState=linked`, previously unsupported. Added exact
  linked-state capture/validation/rollback semantics passed narrow review, but exact install exposed systemd behavior:
  `disable --now` removes a linked unit symlink then exits 5. Transaction rollback could not restore because mutation
  happened inside failed systemctl before capture; exact symlink was manually restored from the immutable backup and
  live state re-proved loaded/linked/inactive. Fixed sequencing: enabled units use disable; linked/disabled units stop only
  when active, leaving file removal to identity-bound capture. Narrow review CLEAN. Exact reviewed transaction then recovered
  the durable manifest and completed live retirement. Installed proof: both units load=not-found, inactive/dead, MainPID=0,
  empty cgroups; all four notifier paths absent; zero notifier processes; immutable current generation
  `generation-3a7cb44ecda047fd98c8024db458bf5a`; no transaction manifest; rerun preserved the same generation. Post-install gates:
  focused 37/37, monitor 195/195, kill-guard 84/84, compile/shell/diff/install-dry-run clean. Committed `2687a8e75`,
  merged current origin/main as `77bb38175`, reran post-merge gates (37/37, 231/231, 84/84), and direct-landed
  `77bb38175` to origin/main. Canonical deploy completed successfully and deployed current origin/main
  `698ff3a7b171a26552d16af61ad7b11a4c815702`; deploy clone HEAD matches origin/main. Buildbox registry SHA-256 remains
  `61cd599c5987be4b738b00e450d66ebd5df1a7ead25d44c799f55f0f37829082`. Landed runtime reproof: both notifier units
  load=not-found, inactive, MainPID=0, empty cgroups; all four notifier paths absent; zero notifier processes; immutable
  generation remains `generation-3a7cb44ecda047fd98c8024db458bf5a`; no transaction manifest. Task #6 DONE.
- Live containment re-proved after tests: `disk-fill-notify.service` is `not-found` / inactive / dead,
  `MainPID=0`, empty cgroup. No owner process/session was signalled.
- Task #6 implementation is replacing the unsafe shell transaction with one stdlib transaction owner.
  Required invariant: stage and verify before mutation; atomically publish immutable first receipt/archive;
  rollback files, daemon-reload, then every prior service state; any rollback failure must be visible.
- 2026-08-12 owner resumed work. Task #6 is ACTIVE under main; task #3 remains pending until task #6
  lands or an independent worker owns its runtime-bound redesign.

## Next executable action

Resume task #3 with a kernel/runtime boundary; PATH interception cannot satisfy absolute-path and same-UID reparenting
safety. Preserve its current uncommitted WIP until the replacement design is proven.
