feat(loop-lane): sample per-cycle usage into lane telemetry (measure-only) - #1719
Conversation
Both loop lanes now record a `usage_sample` in the durable-state block they already upsert every cycle, holding the two subscription-window percentages the rate-limit guard step already reads plus the rise since the previous sample. The field is deliberately inert: no lane reads it back and no pacing, backoff, adaptive cap, merge rung, or pause derives from it. Measuring first is the point; whether the data supports acting on it is a separately decided question. Caveats are recorded beside the sample because they bound what the data can support: the readings are approximate, machine-local, account-scope, and a percentage of a subscription window rather than a token count. Closes #1651
…nt (#1651) Adds the loop-lane convention changelog entry for the measure-only per-cycle usage sample and reconciles the README's caveat count with the lane bodies, which already record three bounding properties. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…am (#1651) The absolute claim that no in-session signal exposes cumulative token spend overreached: it is readability at a cycle boundary that is missing, not the existence of any display. Narrows the claim in the convention and both lane changelogs to what was checked. Records the §Versioning trigger-2 re-verification against the status-line stdin schema and refreshes the stamp to 2026-07-28 (no drift). Makes `at` unconditional in the sample so an unobservable cycle stays distinct from an unsampled one, and states why only the five-hour window carries a delta. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
) The bumps skipped a minor (0.33.2 -> 0.35.0, 0.25.4 -> 0.27.0), which reads as coordinating around other in-flight PRs' numbering. Loop-lane changelog entries are numbered against their own merge base only; the next minor from this branch's merge base is 0.34.0 (source-control) and 0.26.0 (work-items). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JifsqDc7vY8NfwBB1fi8sE
…source (#1651) Independent verification refuted three claims: The assertion that every surface reporting cumulative spend is an interactive display is false. `cost.total_cost_usd` is machine-readable on the same status-line payload the guard's tee already receives, and is session-scoped, so it would attribute to a lane. It is recorded as the deferred candidate; the reason a lane cannot read it today is that the tee does not forward it, which is a guard-side change this item does not make. "Approximate" was imported from `/usage` and never held for `rate_limits.used_percentage`, which the status-line schema documents without qualification. The real bound is the guard's staleness rule, so the caveat now names that. "Machine-local" described the tee file, not the reading, while the next clause correctly called the reading account-scope; the two are now attributed separately. Discloses the gap the sample's framing hid: the tee refreshes only while an interactive session renders a status line, and a lane is by construction a background session, so an unattended lane samples null every cycle. An empty sample means unobserved, never zero. Also drops a no-progress-detector precedent that exists nowhere in this changelog, and restores the established bump-ambiguity paragraph. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude encountered an error after 1s —— View job I'll analyze this and get back to you. |
|
Claude encountered an error after 1s —— View job I'll analyze this and get back to you. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 43ff0aac46
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…sline source (#1651) An independent pre-merge audit read "both null when the guard is not proactive" as implying that a proactive guard guarantees a reading. It does not: a window is independently null when the tee file is absent or stale, when the window is absent upstream, and when the guard's reader contract rejects the window as unknown. The last case mattered most — "the readings as taken" could be read as writing a value the guard had already rejected. Both lane bodies now state all three. The README paragraph asserting current-context token occupancy and a session-scoped cumulative cost field carried no inline citation, while the neighbouring paragraphs stamp exactly such claims inline. It now matches that pattern. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude encountered an error after 0s —— View job I'll analyze this and get back to you. |
|
Claude encountered an error after 0s —— View job I'll analyze this and get back to you. |
…its delta attribution (#1651) Review found two contradictions in the invariant as staged. The invariant required `five_hour_delta_pct` to be derived from the previous `usage_sample` while the same text said no lane reads the field back. Both cannot hold: the telemetry block is the durable source across compaction, so a lane obeying the prohibition literally could not obtain the previous percentage and would have to emit null or fabricate the delta. The boundary is now stated as a closed operation rather than a blanket prohibition — the previous sample is read back for exactly one thing, subtracting its `five_hour_pct` to compute the new sample's delta, and that derivation is the field's only permitted consumer. The value still never reaches a decision: not pacing, backoff, an adaptive or item cap, a merge rung, admission, escalation, a warning, or a pause, at any threshold, in a lane or in any gate a lane runs. Enumerating the forbidden consumers is what keeps the carve-out from reading as permission to act on the value. The guard reading is taken during Re-anchor, before intake and execution, but the field was documented as "this cycle's" sample. The delta recorded for cycle N therefore measured the interval since cycle N-1's start and excluded cycle N's own execution. Of the two remedies, re-attribution is taken rather than a post-execution reading: a second reading would be an observation of the guard's tee that no lane's cycle shape performs, requiring the reader contract's staleness and per-window capability logic to run again, which contradicts the invariant's own justification that the reading is already in hand and costs only a write. The field's documented meaning now says so — `at` is the cycle-start observation time, the delta covers the interval preceding the cycle whose report carries it, and that cycle's own consumption lands in the next cycle's sample. Both clarifications are stated standalone in the README and in both lane bodies so the three cannot drift, and the existing 4.0.0 changelog entry is amended rather than superseded. "Inert" is retired wherever it described the field: it is now false, since the field is read once, narrowly. No enforcement, threshold, or consumer is introduced; measure-only still holds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude encountered an error after 0s —— View job I'll analyze this and get back to you. |
|
Claude encountered an error after 0s —— View job I'll analyze this and get back to you. |
|
Claude finished @kyle-sexton's task in 2m 14s —— View job PR Review: feat(loop-lane): sample per-cycle usage into lane telemetry (measure-only)
This is a well-structured, carefully documented PR. The changes are purely specification/documentation (SKILL.md files are natural-language instructions for LLM lane sessions, not executable code), so the review focuses on internal consistency, correctness of the documented contract, and alignment with the repo's conventions. Codex P2 findings — already addressedBoth automated findings are handled in the PR text: "Permit readback when deriving the delta" — The PR explicitly names the one permitted readback in all three sites. Convention README lines 238–243: "The one permitted readback. The previous cycle's sample is read back for exactly one operation: subtracting its "Sample after execution before attributing usage to this cycle" — The PR explicitly chose the cycle-start reading and documented the tradeoff. The CHANGELOG (lines 38–40) records the rejection of post-execution sampling: "The alternative — a post-execution reading — was rejected because it would be a second observation of the guard's tee that no lane's cycle shape performs, contradicting the invariant's own justification that the reading is already in hand." The lagging nature is disclosed as an explicit invariant ("Read the series as a lagging one") in both the convention and lane bodies. Convention-to-lane consistencyThe convention (§4) and both lane body implementations are consistent. Differences between the two lane bodies are appropriate to each lane's decision surface:
Both lists correctly substitute the lane-specific decision surfaces while preserving the measure-only invariant. JSON schema examples are byte-identical across lanes. Null conditions and delta semanticsThe delta null conditions are sound:
One observation worth noting: when a five-hour window resets, the cycle immediately after the reset will produce a Versioning rationaleThe major bump (3.0.0 → 4.0.0 for the convention, minor bumps for the two plugins) is correct under the convention's own rule: "a change to... any loop-layer invariant is a major bump." Adding a new §4 invariant every loop lane must implement is a new obligation, which the "Bump ambiguity" section in the changelog correctly distinguishes from a purely additive field. The attend-queue carve-out (§4 binds loop lanes, and that lane holds no durable-state block) is accurate. The version collision table is appropriately disclosed. This is a process issue, not a correctness one. Minor observations
SummaryNo correctness issues found. Both Codex P2 findings are already resolved in the PR's documentation. The convention, both lane bodies, and both changelogs are internally consistent. The major version bump rationale is sound under the convention's own rules. The measure-only constraint is stated unambiguously in all three implementation sites with the correct decision surface list per lane. Ready to merge pending version collision resolution with sibling PRs. |
|
Claude finished @kyle-sexton's task in 3m 9s —— View job Security Review
No security issues found. This PR is exclusively documentation — CHANGELOG.md entries, README.md additions, SKILL.md convention prose, and plugin.json version bumps. No new scripts, hooks, or GitHub Actions are introduced. All security-relevant infrastructure ( The data-flow security posture was reviewed end to end: Tee file as data source. Injection surfaces. Secret / credential exposure. One-permitted-readback authorization. The convention's "measure-only" constraint prohibits any decision — pacing, backoff, merge rung, pause — from deriving from the sampled value. The only permitted readback ( Reader contract classification. The reader contract correctly distinguishes |
main landed #1650 and #1648, taking convention 4.0.0/5.0.0, source-control 0.36.0 and work-items 0.28.0 — the renumber this PR's body predicted. This branch's entries compose above them (convention 6.0.0, source-control 0.37.0, work-items 0.29.0), and the convention entry moves back to the top of its changelog. The step 6 and state-block conflicts compose all three features: the no-progress streak update, the escalation record write, and the usage sample now sit in one step and one state block. babysit-loop's SKILL.md crossed the 500-line hard cap with the third feature in it. The usage-sample prose in both lane bodies now keeps the field contract and cites §4 for the readback rule, the delta semantics, and the three bounding properties the convention already states in full; the telemetry upsert mechanics move to a spoke beside the existing two. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TkLLDcB8Ht2tm3j688F5ZP
main landed source-control 0.37.0 while this merge was in flight, which collided with the number this branch had just taken. The branch's entry moves to 0.38.0 in both the changelog and the manifest, and the two entries the union had collapsed under one heading are split back apart. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TkLLDcB8Ht2tm3j688F5ZP
|
Claude finished @kyle-sexton's task in 4m 37s —— View job PR Review: feat(loop-lane): sample per-cycle usage into lane telemetry (measure-only)
Findings1. Undisclosed scope: upsert script moved to
|
|
Claude finished @kyle-sexton's task in 2m 51s —— View job Security Review — PR #1719
No security issues found. This PR is almost entirely documentation and convention prose. The only change with any executable surface is the relocation of an existing bash script from inline in Scope of reviewNine changed files: two convention docs, one new reference file ( Script relocation — unchanged attack surface
|
main took source-control 0.38.0; this branch's entry moves to 0.39.0 in both the changelog and the manifest, with the two entries kept separate under their own headings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TkLLDcB8Ht2tm3j688F5ZP
|
Claude finished @kyle-sexton's task in 5m 34s —— View job PR Review: feat(loop-lane): sample per-cycle usage into lane telemetry (measure-only)
New Finding: CHANGELOG ordering incorrect (medium)
The Prior finding confirmed: step 6 doesn't reference
|
|
Claude finished @kyle-sexton's task in 2m 5s —— View job Security Review — PR #1719
No security issues found. This PR is nine changed files: two convention documents (changelog + README), two SKILL.md instruction bodies, one new reference file, two changelogs, and two Script relocation — zero change to attack surface
|
…hangelog order (#1758) ## Why The per-cycle usage-sample invariant (loop-lane 6.0.0, shipped in #1719) is self-contradictory as written. §4 permits reading the previous sample back **"for exactly one operation: subtracting its `five_hour_pct`"**, then states **"No other read is permitted"** — but the same invariant withholds a delta when the window rolled over, and deciding that requires a *second* comparison against that previous reading. No lane can satisfy both clauses. This is not theoretical. Both `babysit-loop` and `work-loop` were rewritten to hold the readback contract **by citation** ("Everything else — the single permitted readback … — is the convention's") while independently mandating the rollover comparison. The contradiction therefore has exactly one authoritative site, and every consuming lane inherits it. `chatgpt-codex-connector` raised this on #1719. The fix was drafted but never committed — the authoring agent was interrupted mid-edit and #1719 merged without it. ## What changed - **The permission is scoped by *purpose*, not by *operation*.** It now covers deriving `five_hour_delta_pct` — the subtraction **and** the rollover comparison — as one derivation. **The measure-only guarantee is unchanged**: the value still reaches no decision, at any threshold, in a lane or in any gate a lane runs. - **`at` disambiguated.** It is when the lane read the tee, not the snapshot's own `captured_at`, which the staleness rule permits to lag it. - **The delta's `null` condition widened.** "Either sample is missing" excluded a present sample carrying a `null` `five_hour_pct`; it is now `null` whenever either side's `five_hour_pct` is unavailable. ### Changelog version regression (separate defect, same file) `docs/conventions/loop-lane/CHANGELOG.md` on `main` read `6.0.0 → 3.1.1 → 5.0.0 → 4.0.0 → 3.1.0`. The `#1638` entry was authored against `3.1.0` and merged (#1686, 17:46:59Z) after `4.0.0` had already landed (17:44:23Z) — a stale-branch renumber miss, in a file with no CI gate for version order. Renumbered **`4.0.1`** and repositioned below `5.0.0`, which preserves both descending version order and the order entries actually shipped in. **Its wording is unchanged.** Verified by script — all three touched changelogs are now strictly descending with no duplicates. ## Deliberately not done The `source-control` `0.39.0` and `work-items` `0.29.0` entries describe the field as *"deliberately inert: no lane behavior reads it back"*, which the shipped contract contradicts. Those versions have already been published, so they are **left as shipped** and superseded by the new `0.40.2` / `0.30.2` entries rather than rewritten in place. ## Verification - `node scripts/validate-plugin-contracts.mjs` — 43 setup skills, 2150 files, pass - `bash scripts/check-changed-skills.sh origin/main` — 2 skills, 0 failures (`babysit-loop` 495/500, `work-loop` 434/500) - `npx markdownlint-cli2` over all 6 changed markdown files — 0 errors. Run standalone because `check-changed-skills.sh:67` sets `CHECK_SKILL_SKIP_MARKDOWNLINT=1` by design (documented at line 19; markdown is gated by the hygiene lane). - Changelog ordering verified by script against `sort -rV`. ## Related No linked issue — this corrects defects in already-merged work; both originating issues are closed. Refs #1651 (the usage-sample invariant this corrects, shipped via #1719) Refs #1638 (the changelog entry renumbered here, shipped via #1686) Refs #1720 (its post-merge review findings are tracked separately, not in this PR) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Phase 1 of #1651: turn each loop lane's spend from a blind spot into a measured one, by recording a
per-cycle
usage_samplein the lane telemetry state block that already exists. Nothing reads thefield back. Phase 2 — a budget config key — is deliberately not designed here, and no threshold,
cap, warning, pause, backoff, pacing change, or merge-rung change derives from the sampled value.
The signal, and why it is not a token count
The issue title says "token usage". This ships window-percentage usage instead, because no token
count is readable at a lane's cycle boundary. What each lane actually records is the reading its
rate-limit guard step already took that cycle:
rate_limits.five_hour.used_percentage/rate_limits.seven_day.used_percentageplugins/rate-limit-guard/scripts/statusline-tee.sh, which selectsrate_limitsout of the status-line stdin payload and atomically rewrites
~/.claude/rate-limit-guard/rate-limits.jsonused_percentageis 0–100,resets_atisUnix epoch seconds, and
rate_limitsappears only for Claude.ai subscribers after the session'sfirst API response, with each window independently absent. Re-verified 2026-07-28; no drift.
Because the guard step already holds this value, the invariant costs a write, not an observation.
Why not tokens.
context_window.total_input_tokens/total_output_tokensare documented astoken counts currently in the context window, from the most recent API response — cumulative
session totals only before Claude Code v2.1.132. The
/usage,/goal, and/workflowsfigures theissue cites are interactive displays, not values a skill body can parse at a cycle step.
One signal does exist, and is recorded as deferred rather than denied.
cost.total_cost_usdisdocumented as the session's client-side accumulated cost, resetting on
/clear— machine-readableand session-scoped, so it would attribute to a single lane. The tee does not forward it. Widening
the tee is a
rate-limit-guardchange (its own bump plus a reader-contract amendment) and is out ofscope for a measure-only item, so this PR records it as the deferred candidate with its trigger
rather than claiming no signal exists.
Caveats, re-derived rather than inherited
The issue's acceptance criterion names "approximate, machine-local". Both are recorded, but
attributed to what they actually describe:
/usagecaveat and does not hold forused_percentage, which thestatus-line schema documents without qualification. The real bound is the guard's staleness rule,
so the sample is described as a snapshot no fresher than that rule allows.
reading, which is account-scope. The two are now stated separately.
a lane is by construction a
claude --bgsession. An unattended lane with no interactive windowopen samples
nullevery cycle. An empty sample therefore means unobserved, never zero. This isstated rather than glossed, because the framing "the reading is already in hand" otherwise hides
it.
Scope
#502durable-state block in both loop lanes — no second telemetrymechanism.
plugins/claude-ops/skills/lanes/scripts/telemetry-upsert.shis body-opaque andvalidates no schema, so it needs no change; nothing else in the repo parses the state block.
@1: the added key is additive-optional and no reader validates the shape.attend-queuelane is unaffected — §4 binds loop lanes, and that lane holds nodurable-state block.
plugins/autonomy/reference/is untouched. That tree holds the OTLP telemetry contract, not thelane state-block shape; the shape lives in the loop-lane convention and the two lane bodies.
Versioning — renumbered as predicted
The competing numbers this PR's table anticipated all landed first, so every version here was
re-derived against
origin/mainat merge time rather than against this branch's original mergebase:
docs/conventions/loop-lane/CHANGELOG.md6.0.04.0.0, #1710 took5.0.0plugins/source-control0.39.00.35.0, #17100.36.0, plus0.37.0and0.38.0landing mid-mergeplugins/work-items0.29.00.27.0, #17100.28.0The loop-lane bump stays major on the convention's own discriminator — §4 states loop-layer
invariants and this adds one every loop-lane body must carry. The competing minor reading (a field
nothing reads back changes no behavior) is recorded in the entry per the established pattern. The
convention entry was also moved back to the top of its changelog, where the merge had left it below
the entries that landed ahead of it.
§Versioningre-derivation trigger 2 is satisfied: the status-line claim was re-verified against itscited page and the stamp refreshed, recorded as a changelog entry.
rate-limit-guard's readercontract carries its own 2026-07-23 stamp on the same page; it is unchanged by this PR and its
refresh belongs to that plugin's own bump.
Composition with the two features that landed first
Both lane bodies now carry three loop-layer features in one cycle step and one state block: the
no-progress streak (#1648), the escalation record write (#1650), and this usage sample. Composing
them pushed
babysit-loop'sSKILL.mdpast the 500-line hard cap, so two things were tightenedwithout changing any contract:
atsemantics, everynullrule, the delta's roll-over and first-cycle cases) and cites §4 for the single permittedreadback, the preceding-interval delta, and the three bounding properties — all of which §4
already states in full. §4's own wording changed from "recorded alongside the sample in each lane
body" to "held by citation from each lane body" to match.
babysit-loop's telemetry upsert mechanics — the singleton lookup and the creation-race reconcile— moved to
reference/telemetry-upsert.md, beside thepre-escalation-dispatch.mdandno-progress-detector.mdspokes already there. Still inlined in this plugin rather than invokedfrom
claude-ops, for the same reason as before.Verification
Gates green after the merge:
check-changed-skills.sh(babysit-loop 486/500 and work-loop 451/500against the hard 500-line cap, both re-measured with all three features composed),
check-changelog-parity.sh --checkand--check-bump origin/main, and markdownlint over everychanged markdown file. No
evals.jsonis touched.A fresh-context verifier audited the diff with the rationale withheld and traced the tee's producer
and the live status-line doc independently. Its findings drove the cumulative-cost correction, the
caveat re-attribution, the background-session disclosure, and the removal of a cited precedent that
did not exist. Its remaining version-collision finding is the table above.
Related
4.0.0and both plugin bumps, and writes the escalationrecord into the same cycle step this PR extends
5.0.0and both plugin bumps, and adds the no-progressstreak to the same state block and cycle step
3.1.0work-itemsbump; still open*_session_token_budget) stays deferred until this sampling produces data, per theissue's own staging.
Closes #1651
🤖 Generated with Claude Code