# 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-.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. | 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.