The unparseable-body arm takes its bytes off spool_bytes (#905 finding 3) - #913
The unparseable-body arm takes its bytes off spool_bytes (#905 finding 3)#913philcunliffe wants to merge 1 commit into
Conversation
…g 3) `loadSpooledBodies` deletes a body file it cannot parse right there in the read, but only the files it CONSUMED were reported back to the caller in bytes, so the deleted-unparseable bytes stayed on `state.spoolBytes`. The published `spool_bytes` then over-reported content that was already off the disk until the next sweep restated the gauge, up to a minute later, and `hyp status` read it in between. The read now returns `unparseableBytes` (counted only when the unlink actually succeeded, so a read-only spool does not under-report instead), and the listener subtracts it at the call site, next to where it already records `bodiesUnparseable`. The count and byte total also ride the existing `body_unparseable` warning and span. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Duplicate fix: this PR and #912 close the same defectWhat neutral was doing. The issue-fix reconciler dispatched one worker per open Why it cannot proceed. Both PRs edit the same two files with the same intent, so merging both would double-apply the change and the second will conflict:
Neither is wrong. They differ in emphasis:
What neutral needs from you. Which one should land?
Neutral is not choosing between two maintainer-equivalent implementations on its own. It has left #912 riding the normal ladder purely because it was opened first, not because it judged it better. Also worth knowing: both branches touch How to unstick. Reply with a comment on this PR (or push to the branch); neutral monitors this thread and will re-engage with your guidance on its next tick. |
Issue #905 records five deferred findings from the review of PR #851 (branch
fix/issue-843, head1378a82c). PR #851 is still open, so four of the five describe code that exists only on that unmerged branch and cannot be reproduced, tested, or fixed frommaster. This PR fixes the one finding that is present onmastertoday, and states plainly why each of the others stays deferred.Fixed: finding 3, the unparseable-body arm leaves
spool_bytesdrifted highWhat was wrong.
loadSpooledBodiesdeletes a body file it cannot parse immediately (LLP 0252 #project-then-delete: an undeleted body is a raw prompt sitting on disk) and counts it inunparseable. The bytes it removed were reported nowhere, so nothing ever subtracted them fromstate.spoolBytes.Root cause. The read reports bytes for one arm only:
consumedBytes, the files it hands back for the caller to delete after the batch's writes land. The unparseable arm deletes inside the read and returns no byte figure, so the call site insource.jshad nothing to subtract. The gauge then over-reported until the next spool sweep restated it, and the sweep runs on a 60s timer (SPOOL_SWEEP_INTERVAL_MS), sohyp statuscan read aspool_bytesthat counts content already off the disk for most of a minute. LLP 0257 S16 says the published number is the spool's current byte size, so this is a violation of the spec clause, not just noise.Fix.
loadSpooledBodiesnow returnsunparseableBytes, counted only when the unlink actually succeeded (an unremovable file is still occupying the cap, and subtracting it would under-report in the other direction, which is the same reasoning PR #851 applied todeleteSpooledBodies). The listener subtracts it at the call site, immediately beside where it already accumulatesbodiesUnparseable, and the byte total also rides the existingclaude.telemetry.body_unparseablewarning and the batch span.Regression test (
test/plugins/claude-telemetry-spool-accounting.test.js), two cases, the first driven through the real transport (a listener on an ephemeral port, OTLP/JSON over the wire, a fake gateway behind it):an unparseable body takes its bytes off spool_bytes when it is deleted- seeds the spool with one truncated and one valid body before start so the start-time sweep primes the gauge, POSTs a batch referencing both, then asserts the spool directory is empty and the publishedspool_bytesequals the bytes actually on disk.loadSpooledBodies reports the bytes its unparseable arm removed- pins the reporting itself, including that the parseable file's bytes stay inconsumedBytesand its file stays on disk until the writes land.Before/after (ground-truth gate). With the two source files stashed and the test file kept:
With the fix applied:
# tests 2 # pass 2 # fail 0.Full suite:
npm test-># tests 4486 # pass 4485 # fail 0 # skipped 1.npm run typecheck-> clean. Both@refanchors used here (LLP 0257#status-and-health) resolve to a real section, and S16 is what the fix restores rather than a broader claim the spec does not make.Still deferred, with reasons
Finding 1 (the catch-block comment that claims a retry "writes the same assistant rows with no
attributes.usage"). That comment was introduced by PR #851 and does not exist onmaster; there is nothing here to correct. It has to be fixed onfix/issue-843or after #851 merges, and the issue itself ties it to #879 landing.Finding 2 (snapshot/restore widens a cross-request race on the shared usage index).
restoreUnclaimedUsageand theusageBeforeProjectionsnapshot exist only onfix/issue-843. Onmasterthere is no restore, so the described failure (a failed request's restore resurrecting usage another concurrent request already spent) cannot be reproduced from here and no test could fail-then-pass. Fixing it also means choosing between serializing the OTLP handler and scoping the index per batch, which is a design call on top of an unmerged change.Finding 3's sibling arms. The drop arm's identical drift (
suppressSessiondeleting bodies without subtracting) is already fixed onfix/issue-843; touching it here would only manufacture a conflict, so this PR changes the unparseable arm only.Finding 4 (restored usage entries land at the tail, inverting eviction age). The issue records this as a deliberate tradeoff inside a correct fix, bounded by
USAGE_INDEX_LIMIT, to "revisit only if an age-aware structure is introduced". Nothing to fix; it also lives only on the unmerged branch.Finding 5 (LLP 0257 has no clause for retry-input survival). LLP 0257 is
Accepted. Per CLAUDE.md, changing what an Accepted doc settled requires minting a new request that extends it, and the issue conditions that on a maintainer agreeing the requirement belongs in the spec. That is a human decision, not an automatable one, and the two@refs it would restore are on the unmerged branch. Left for a maintainer.Because four of the five findings cannot be reproduced on
master, #905 should stay open after this merges, to be re-triaged againstfix/issue-843once PR #851 lands.Residual risk
source.jsis touched three lines above a hunk PR #851 also edits, so the second of the two to merge may need a trivial conflict resolution. The behaviour change is confined to a gauge: no row, no dataset, and no deletion decision moves.Fixes #905