# Compaction observer revocation and runtime generations

WI-3 connects Claude and Pi compaction/session lifecycle events to the existing authenticated lease-broker state machine. It does not add a second lease store or let runtime hooks assert identity. Each observer inherits the broker-minted session, resolves the current private runtime generation, and sends the existing `revoke_lease` action over the authenticated Unix socket.

## Observer matrix

| Runtime          | Lifecycle signal                                                 | Action                                                                                                                       |
| ---------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| Claude / Claudex | `PreCompact`                                                     | Revoke the current lease before compaction. A non-zero hook result blocks the lifecycle transition.                          |
| Claude / Claudex | `SessionStart` with matcher `compact`                            | Revoke again after compacted context starts.                                                                                 |
| Claude / Claudex | `SessionStart` with matcher `resume\|clear`                      | Atomically advance the private generation, then revoke the replacement incarnation.                                          |
| Pi               | `session_before_compact`                                         | Revoke before compaction; return `{ cancel: true }` if revocation cannot be confirmed.                                       |
| Pi               | `session_compact` then the first `context`                       | Arm and run an independent post-compaction revoke. A failed post observer blocks later tools locally until a retry succeeds. |
| Pi               | `session_start` with reason `reload`, `new`, `resume`, or `fork` | Atomically advance the private generation, then revoke the replacement incarnation before reuse.                             |

The first observer that reaches the broker deletes pending promotion tokens and makes the lease `UNVERIFIED`. The second compaction observer is deliberate redundancy, not a prerequisite for the first. Claudex receives the same mandatory hooks in its isolated `CLAUDE_CONFIG_DIR`; hook merging preserves unrelated isolated settings and rejects malformed or symlinked settings fail-closed.

## Private generation authority

`launch-runtime.py` still registers before `exec`, preserving the kernel-authenticated PID/starttime anchor. It now also creates `generation-<broker-session>.state` beside the broker socket. The file is owner-only mode `0600` under the broker's mode-`0700` directory. Hook descendants read that file instead of relying only on an immutable inherited environment value.

Generation changes use an exclusive file lock, validate owner/type/mode/size, increment monotonically, truncate and write the complete new value, and `fsync` before contacting the broker. Therefore reload, new-session, resume, and fork events may retain the same PID/starttime while still becoming a new broker incarnation. The higher generation causes the broker to atomically discard prior tokens and lease authority; the replacement generation inherits no VERIFIED lease.

If an observer fires while broker transport is unavailable, `revoke-lease.py` advances the private generation as a local fence before returning non-zero. Every later all-tools gate reads that higher value. When the broker is reachable again, authentication of that value performs the same old-generation revocation before authorization. Pi also keeps a process-local post-compaction/rollover failure latch that blocks tool calls. An unsafe or unreadable generation file itself makes both lifecycle revocation and tool authorization fail closed.

## Threat contract and stopping boundary

### BOUNDED RESIDUAL STALE WINDOW

If **both** pre- and post-compaction observers are missed entirely, no revocation signal exists. During the remaining unexpired lease, **within-TTL consequential actions are allowed**. Their count and timing are **bounded by lease expiry, not by the mutator gate**. WI-3 makes no claim that it bounds mutator actions inside this stale interval. The broker's monotonic lease TTL is capped at 300 seconds; after expiry, the next consequential tool is denied with `LEASE_EXPIRED`.

This is the named D2-v5 T-A residual. It is distinct from an observer that fires but cannot contact the broker: the latter creates a local generation fence and fails closed. It is also distinct from T-C total rot, where the lifecycle observers and the all-tools gate are both absent or replaced. Server-side branch protection, required CI, and independent review remain the irreducible backstop for T-C.

| Condition                                                       | Result                                                                                                                |
| --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| Either compaction observer succeeds                             | Existing lease and pending promotion tokens are revoked immediately.                                                  |
| Observer runs but broker confirmation fails                     | Lifecycle transition is denied where supported; local generation fence and runtime latch prevent inherited authority. |
| Both observers are missed, lease unexpired                      | **ALLOWED** inside the bounded residual stale window. No within-window mutator bound is claimed.                      |
| Both observers are missed, lease expired                        | **DENIED** by monotonic TTL expiry.                                                                                   |
| Generation advances on reload/new/resume/fork                   | Prior incarnation revoked; replacement starts `UNVERIFIED`.                                                           |
| Lifecycle observers and all-tools gate both fail or are removed | T-C total-hook-miss residual; protected-branch controls remain required.                                              |

## T-C server-side branch-protection posture

The required posture is that `main` is push-blocked and PR-only-merge is **MANDATORY**, regardless
of client-gate state. The client-side gate narrows the exposure window only; it is not the T-C
guarantee. The server-side protected-branch configuration is the irreducible guarantee for protected
repository actions. Status-check enforcement and approval enforcement are **RECOMMENDED**.

## Current-vs-required gap (recorded, not enacted)

The current empirical configuration is recorded here without re-probing or mutating live branch
protection. `enable_push=False` (push-block present), so the mandatory push-block/PR-only-merge core
holds. `require_approvals=0` (approvals not enforced), `enable_status_check=False` (status checks not
enforced), and `block_on_official_review=False` (official review not enforced). Those recommended
merge-quality controls are the current gap; changing them is a separate, owner-gated operations
decision and is not enacted by this documentation change.

The permanent T12b/T30 acceptance case prints both required outcomes: dual-hook miss within TTL is **ALLOWED**, and the same lease after TTL is **DENIED**. Separate real-socket tests prove each Claude observer and same-PID generation rollover; Pi lifecycle tests exercise pre/post observers, all four replacement reasons, and local failure closure.
