Skip to content

state-dir contract v2: cooperating writers + robust model context - #15

Merged
lroolle merged 16 commits into
mainfrom
feat/shared-store-contract-v2
Aug 10, 2026
Merged

state-dir contract v2: cooperating writers + robust model context#15
lroolle merged 16 commits into
mainfrom
feat/shared-store-contract-v2

Conversation

@lroolle

@lroolle lroolle commented Aug 6, 2026

Copy link
Copy Markdown
Member

Implements the shared-store proposal from #14, now that ccpace v0.1.1 ships its side of it.

What changed

docs/api/state-dir.md — contract v2. v1 said "statusline.sh is the ONLY writer". v2 opens the dir to cooperating writers under explicit rules: record shapes are law (additive-only, foreign writers tag source: "<tool>/<version>"), readers tolerate unknown types/fields, the 32MiB/.1/mkdir-lock rotation is shared, usage.cache/profile.cache act as a cross-tool fetch pool (fresh fetched_at = a fetch already made; publish back atomically), and aggregating readers partition by user.uuid.

statusline.sh — last_logged_model(). The two tail -1 usage.jsonl | jq .model sites blanked the advisor's model context whenever the newest line lacked .model — which happens today with our own session_start/session_end markers, and with any cooperating writer's samples (ccpace logs model:null by contract). The helper scans a bounded tail (200 records) for the newest record that actually carries a model. This is a standalone bug fix even without ccpace in the picture.

t/ — four bats cases: newest-model-wins, markers-don't-blank, foreign-null-skipped, empty-log-silent. Full suite: 346 tests, 0 failures.

What this buys

ccpace v0.1.1 already reads a fresh usage.cache instead of fetching, skips re-logging pooled samples, and publishes its fetches back — with this contract in place, a statusline render and a ccpace watch cycle on the same account share one API fetch stream and one usage history (richer ledger + forecasts for both).

Closes #14

🤖 Generated with Claude Code

lroolle and others added 16 commits July 28, 2026 22:53
…input

jq's `catch .` binds the error MESSAGE as the value, not the original
input. Snapshots with an empty or unparseable five_hour.resets_at all
normalized to the same error string — one shared fake window identity —
so the pct_per_window pair walk could pair samples across real windows.
Observed on real data: phantom window transitions and a skewed ratio
(9.91 -> 10.06 after the fix on the same log).

Normalize via a named def that treats empty as empty and falls back to
the raw string on parse failure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`statusline.sh report [--days N]` replays usage.jsonl and ledgers every
closed window: 7d closes as used%/expired% (converted to 5h-windows-worth
via the learned pct_per_window ratio), 5h closes as count/avg/capped,
plus the week in progress projected with the same learned walk the
advisor uses — the surfaces cannot disagree. A window closes when
consecutive samples disagree on resets_at, normalized to the minute
(the ratio learner's identity rule). The advisor prevents waste
prospectively; this proves it retroactively.

Every usage snapshot now also logs limits[] (scoped weekly caps,
verbatim), model (id active in the logging session), and predicted_end —
the learned walk's projection at sample time, the calibration seed that
lets closed windows score the forecast later. Learning lags logging by
weeks; a field absent today is a pattern that can't be learned next
month.

Subcommands dispatch after all function definitions, take no stdin, and
are read-only against the state dir.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CHANGELOG entry, README section with a real ledger example, state-dir
contract gains the widened usage.jsonl fields (limits, model,
predicted_end) and names report as the reference consumer, counts
synced to 327 tests across README / llms.txt / site.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stacked PRs (base = a feature branch) got no checks under the
branches:[main] filter; judgment should not depend on where a PR
points. Push triggers stay main-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The statusline never runs when you're away — exactly when expiring
capacity needs a voice. Instead of a daemon, `check` exposes the
advisor's judgment as an exit code (0 calm / 1 opportunity / 2 pressure
/ 3 unknown-or-stale) with the plain verdict on stdout, for tmux
segments, cron notifiers, and scripts. Model context for the scoped
clauses comes from the last logged snapshot — the first consumer of the
widened `model` field.

`session-summary` gives one-line session retrospectives from the usage
log, designed as a SessionEnd hook (hook JSON on stdin, only session_id
read; bare runs fall back to the last logged session). Window deltas
are positive-delta sums — the profile builder's rule — so a session
straddling a 5h reset still reports what it consumed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
README scripting section (cron + tmux + SessionEnd hook recipes),
CHANGELOG entry, counts synced to 334 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three layers, one source of truth: line 1 shows the numbers, the
advisor row says the one sentence, and this skill carries the full
conversation — should I start a heavy task, which account has headroom,
what did I waste this week. It encodes the state-dir contract, the
learned-forecast semantics, and the advisor's judgment rules
(feasibility before advice, facts only while unlearned, staleness said
out loud), and runs the same report/check subcommands instead of
re-mining — so the layers cannot disagree.

Install: cp -r skills/usage-insight ~/.claude/skills/

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two taste fixes for the two-line render.

A session traced by cctrace (deva --trace, or cctrace directly) now
wears a dim [cctrace:PORT] chip on the LEFT, next to path and branch:
being recorded is session identity, so it lives in the structure lane —
red stays reserved for pressure. Detection takes the strongest signal
available: the trace env cctrace exports into its child
(CCTRACE_SERVER_PORT), the capture's CA plumbing (NODE_EXTRA_CA_CERTS
under a cctrace dir), or deva's DEVA_TRACE=1. Plumbing-only captures
(older cctrace) resolve the port through the live-instance registry —
session id first, then project path, then only-live-capture; tombstones
never lend their port. No resolvable port still shows a bare [cctrace].

The advisor row anchored on term_width - 5, but under a pipe `tput
cols` answers a flat 80 whatever the terminal is — line 1 overflowed
the phantom edge while the advice dangled mid-line beneath it. The row
now anchors on line 1's ACTUAL rendered width (line1_cols, recorded by
format_output), so the second line's right edge meets the first's even
when the width guess is wrong. Width detection itself got honest too:
the controlling tty (stty size </dev/tty) and an inherited COLUMNS both
beat tput's default.

Tests: 12 new (8 chip units, 2 chip integrations, 2 edge-alignment
integrations); helpers now scrub the trace env the way they scrub
account identity — this repo's own dev loop runs under deva --trace,
which is exactly how the leak would happen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Live testing against this repo's own traced dev session caught two
registry realities the first cut missed. cctrace REDACTS session ids
before they land on disk ("c3a6e0f3-****-…"), so an exact-id match
never fires against a real registry — match on the sid8 prefix, the
same join key cctrace's own UI uses. And crashed captures leave
non-tombstoned "live" entries behind for up to a day, so the project-
path and only-live fallbacks now trust heartbeat-fresh files only
(<2min against a 30s heartbeat); the sid8 match keeps trusting any
non-tombstone entry — if OUR capture died, this session's proxy died
with it.

Tests 346 -> 348: the sid test now stores a redacted id, plus
stale-entry coverage both ways (sid8 claims its own stale entry;
fallbacks refuse a stranger's).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 7d window becomes a `week` subcommand: a 56-cell timeline (8
cells/day) where fill is budget consumed on a time axis, `│` marks
now, `▒` is headroom to the clock, and `▓` is usage running ahead of
it. The day ruler anchors to the account's own reset weekday — no
pretending every week is Mon–Sun — and shares the reset formatters'
zone, so the labels can't drift a weekday from the boundaries they
describe. The advisor renders underneath in always-mode; the strip is
the prospective glance beside `report`'s retrospective ledger, and the
same one claudex's watch mode draws. State dir only; stale data
renders but says so; no cache or no active window exits 3 like
`check`.

The calm advisor line now speaks the shared budget frame: `- budget
~19x5h left · even 1.1%/win · heading ~52%`. "budget" names the
frame, "pace" stops doing double duty (it means usage/elapsed
everywhere else), and in the last window — where per-window math just
restates the headroom — it degrades to `- budget last window · 61%
left · heading ~40%`.

Tests 348 -> 352: strip geometry against a fixed clock, ruler
anchoring, last-window degradation, and the exit-3 contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 56-cell time axis told you when the week ends; it could not tell
you where the week went. Redrawn: the 7d period as its own 5h windows,
one cell each (34, the last a 3h stub). ▁▂▃▄▅▆▇█ a window that ran,
height = the 7d points it burned; · ran but under 1%; ░ unknown, no
samples on record; ▮ the window you're in; ▫ still ahead; × a window
the pool won't cover at the current pace — the same wall claude.py
draws, learned forecast when trained, linear projection otherwise.

Past cells are reconstructed from usage.jsonl: a window instance is
keyed by its 5h resets_at rounded to 5min (the API jitters it, and
05:59:59/06:00:00 are one window), costed by the 7d movement observed
inside it. ░ and · stay different glyphs because a gap in the record
is not an idle session, and drawing it as one is the lie this row must
not tell. Count ▮ and what follows and you get the budget line's own
~Nx5h left, so the picture and the sentence under it are one number.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… the log

The dir opens to cooperating writers (first: ccpace) under explicit
rules: record shapes are law, readers tolerate unknown types/fields,
rotation lock is shared, usage.cache/profile.cache act as a fetch pool,
aggregating readers partition by user.uuid.

last_logged_model() replaces the two tail -1 sites: the newest log line
may be a session_start/end marker (no .model) or a cooperating writer's
sample (model:null) — either blanked the advisor's model context. Take
the newest record that actually carries a model. Bats coverage for all
four shapes.

Refs #14

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lroolle
lroolle merged commit c7e7733 into main Aug 10, 2026
1 check passed
@lroolle
lroolle deleted the feat/shared-store-contract-v2 branch August 10, 2026 05:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Proposal: shared usage store contract for thevibeworks Claude tools (ccpace interop)

1 participant