Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/context-guard/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "context-guard",
"version": "0.7.64",
"version": "0.7.65",
"description": "Per-session context-window observability plus the first shipped consumer: a statusline wrapper tees each session's context_window fields to a per-session snapshot file, a zone resolver classifies usage into smart/acceptable/dumb bands (percentage bands plus window-class token bands, conservative-min combination, zones.json SSOT with shipped defaults), a reader contract fixes how consuming sessions interpret the snapshots, and zone-crossing hooks report once per transition into a worse zone across two channels: the continuation menu to the operator, who owns that choice, and to the model only the zone determination plus the counter-steer that a zone word is not a decay signal (advisory by default; an optional blocking mode gates new mutating work on a fresh dumb-zone snapshot with handoff-writing exempt), with a PostCompact hook persisting an evidence-degraded marker.",
"author": {
"name": "Melodic Software",
Expand Down
7 changes: 7 additions & 0 deletions plugins/context-guard/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ All notable changes to the `context-guard` plugin.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.7.65]

### Changed

- hooks: `zone-crossing-inject.sh` skips the zone resolver when nothing it reads has moved. A `$STATE_DIR/$SESSION.seen` mark, stamped with a redirection and compared with `-nt` (both builtins), records the inputs behind the last COMPLETED resolve; when the snapshot, `zones.json` and the compaction marker are all no newer than it, the fire exits before starting a process. The mark moves only after the markers persist, so a resolver failure, an `unknown` reading and a failed marker write are each retried. The envelope parse now uses `hook::jq_fields`' builtin parser on a payload within its proof ceiling and keeps the single here-string `jq` above it, because the helper's oversize fallback reads through a process substitution and costs four process creations against that `jq`'s two. Process creations under a Windows job object (5 reps, identical across reps; the subject's own floor is 3): small envelope, first fire 11 → 9, repeat with nothing moved 9 → **3**, snapshot rewritten 9 → 7; 150 KB batch payload, 11 → 11, 9 → **5**, 9 → 9. No cell is worse than before. Median wall for the small repeat fire, on a host whose timings are bimodal, 1,448 ms → 237 ms. The one failure mode: a snapshot written DURING a resolve is marked as seen, so its crossing waits for the next statusline render, since the window is the resolve rather than an mtime tick, and a missed crossing is late, never lost, because skipping only ever chooses silence. Crossing messages are byte-identical, asserted against a control session driven through the same zone sequence with no skipped fire. The per-batch budgets the contract test pins move with the paths: the steady fire now spawns nothing (0 commands, 0 process creations, 1 program launch) and a resolving fire spawns the resolver alone (1 command, 2 process creations, 3 program launches).
- hooks: the same skip also requires EXISTENCE parity, not mtimes alone. The mark carries one line recording whether `zones.json` and the compaction marker existed behind the last completed resolve, read back with the `read` builtin, and the skip is taken only when the three `-nt` tests are false and those flags still match; a mark with no readable line never takes it. `-nt` cannot see a removal, so deleting an override or the compaction marker previously read as nothing having moved and left the stale zone in place until an unrelated snapshot write. Both process budgets are unchanged.

## [0.7.64]

### Changed
Expand Down
38 changes: 38 additions & 0 deletions plugins/context-guard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,44 @@ the older command-position budget, so a redirection moved back inside a substitu
rather than quietly doubling a call site. Where `strace` is unavailable that assertion skips and
the command-position budget still runs.

#### Skipping the resolve when nothing moved

Three files outside the hook decide everything it does: the per-session snapshot, the optional
`zones.json`, and the compaction marker. When none is newer than the `.seen` mark the last
completed resolve left, and the two optional ones still exist or are still absent exactly as that
mark's own line records them, the fire cannot reach a different answer, and the hook exits through
builtins alone. The existence line is what an mtime comparison cannot supply: a removed file is
never newer than anything, so without it, deleting `zones.json` or the compaction marker read as
nothing having moved. A mark carrying no readable line never takes the skip. The envelope parse had
to become free for any of this to mean anything, so a payload
within `hook::jq_fields`' proof ceiling is parsed by the library's builtin JSON parser, and one
above it keeps the single here-string `jq` described below.

Measured as process creations under a Windows job object, which counts every descendant; 5 reps per
cell, identical across reps. The subject is the hooks.json row run through `usr/bin/bash.exe -c`,
whose own floor is 3: the `-c` shell, `env`, and the shell the script's shebang starts.

| Fire | Payload | Creations before | After |
|---|---|---|---|
| First, resolves | small envelope | 11 | 9 |
| Repeat, nothing moved | small envelope | 9 | **3** |
| Snapshot rewritten | small envelope | 9 | 7 |
| First, resolves | 150 KB batch | 11 | 11 |
| Repeat, nothing moved | 150 KB batch | 9 | **5** |
| Snapshot rewritten | 150 KB batch | 9 | 9 |

No cell is worse than before, which is what the size test on the envelope parse buys: the helper's
fallback reads through a process substitution and costs four creations on an oversize payload
against two for the here-string `jq`, so only the small arm goes through the helper. Wall clock on
this host is bimodal and is reported only for the row it dominates: the small repeat fire's median
fell from 1,448 ms to 237 ms.

The one failure mode is a snapshot written DURING a resolve. The mark is stamped after the resolve
completes, so that write counts as seen and its crossing waits for the next statusline render; the
window is the resolve, not an mtime tick. A missed crossing is therefore late, never lost, and the
converse cannot happen: skipping only ever chooses silence, so no arrangement of timestamps can
manufacture an injection the full path would not have made.

#### The cost this pass added: a temp file on payloads over 64KiB

The saving is not free, and the charge is disk rather than CPU. Two of the five removed process
Expand Down
Loading