Skip to content

fix(telemetry): a record can predate its journal, and must say so - #759

Merged
blafourcade merged 2 commits into
nextfrom
fix/a-record-can-predate-its-journal
Sep 4, 2026
Merged

blafourcade merged 2 commits into
nextfrom
fix/a-record-can-predate-its-journal

Conversation

@blafourcade

@blafourcade blafourcade commented Sep 4, 2026 •

Copy link
Copy Markdown
Contributor

What it says today

by_task tells a person their flow declares its task late in 96.2% of a real period.

Measured on this project's own sink, 29,207 requests over 2026-08-01..09-30: of the 28,101
requests reading "before the next task this session declares", 22 actually did.

Why

The rest are older than anything their session's journal witnessed. Reading a resumed
transcript stores the turns it inherited under the session that read them, dated when each
was billed — days before that session ever opened a journal. There was no journal to
declare anything in, so nothing was declared late.

Verified on both of this repository's run journals that min(at) over every line equals the
session_start line, so the journal's own witnessed.fromMs — which already exists, and
which the "inferred" bound already reads — is the key, not a second notion of when a
session began.

The change

A fifth TaskUnattributedReason, "precedes-journal". Decided before "no-declaration", so
a journal that declared nothing and did not cover the record is named by the coverage
fact — the one that explains why no declaration could have covered it.

The same separation "no-journal" made from "no-declaration" at version 11, one level
down: a fact about what the journal could cover is not a fact about how the work behaved.

Proven end to end

Built binary, the real sink copied read-only into a sandbox HOME and
AIDD_USER_CONFIG_DIR, same command on both builds:

Row origin/next this branch
older than anything this session's journal witnessed — 28,079
before the next task this session declares 28,101 22
no usable run journal for this session 33 33
named tasks (5 rows) 1,073 1,073

by_task and by_backlog both sum to 29,207 — the period total — on both builds.

Guards, each with the mutation that kills it

Guard Mutation
a record older than everything its journal witnessed reads "precedes-journal" drop the coverage check
a record inside the span, before the first declaration, still reads "precedes-declaration" widen the check to every unattributed record
a journal that declared nothing and did not cover the record is named by the coverage fact order the coverage check after the empty-intervals check
a journal carrying no readable moment never claims coverage default an absent span to Infinity
the report passes the journal's span, not only the function drop the third argument at the call site
every consumer names the fifth reason remove one key from TASK_UNATTRIBUTED_LABELS

Rejected as an equivalent mutant: defaulting an absent span to 0. Every moment this
function is ever asked about is after 1970, so the mutant behaves identically.

Found while making the change

Nothing checked the cost_report_version the skill names against the one the CLI emits.
The contract document has had that guard since version 10; the skill, which refuses any
object whose version it does not know, had none — so a bump could ship leaving it set to
refuse the object it was built to read. Added, and it failed before 03-report.md was
updated.

Envelope

cost_report_version 13 → 14. A consumer switching exhaustively on the four previous
reasons must add this one; every row's totals still reconciles.

Rebased onto #757 once it merged, so this takes 14. Found in that rebase: #757 bumped the number and never wrote its entry in the contract's version history, which jumped from currently 13 straight to "Bumped from 11". Both entries are written here.
Whichever lands second renumbers to 14 — the constant, the contract heading and its
version history, the skill, and the fixture envelope.

Gates

pnpm build + vitest run 3392/3392 · tsc --noEmit · biome ci · knip --production ·
jscpd · bundle 588.6/590 KB · repo node --test scripts/__tests__ 366/366 · markdown
links 0 broken · catalogs and README counts regenerate to no change.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp

@blafourcade
blafourcade requested a review from a team as a code owner September 4, 2026 17:52
@blafourcade
blafourcade force-pushed the fix/a-record-can-predate-its-journal branch from 23aafe0 to d8f9bf7 Compare September 4, 2026 17:55
`by_task` told a person their flow declares its task late in 96.2% of a real
period. Measured on this project's own sink: of the 28,101 requests reading
"before the next task this session declares", 22 actually did.

The rest are older than anything their session's journal witnessed. Reading a
resumed transcript stores the turns it inherited under the session that read
them, dated when each was billed - days before that session opened a journal.
No journal existed to declare anything in.

A fifth `TaskUnattributedReason`, `"precedes-journal"`, names that fact,
keyed on the journal's own earliest witnessed moment - the field the
`"inferred"` bound already reads, never a second notion of when a session
began. Decided before `"no-declaration"`, so a journal that declared nothing
and did not cover the record is named by the coverage fact, which is the one
that explains why no declaration could have covered it. The same separation
`"no-journal"` made from `"no-declaration"`, one level down.

`cost_report_version` 12 -> 13: an existing field gains a value a consumer
switching exhaustively on the four previous reasons does not know.

Also guards what nothing guarded: the version the cost skill names is now
checked against the one the CLI emits. The contract document has had that
guard since version 10, and the skill refuses any object whose version it
does not know - so a bump could ship leaving it set to refuse the object it
was built to read.

Proven on the built binary against the real sink, sandboxed: 28,079 named for
their age, 22 for declaring late, both breakdowns still summing to the 29,207
requests of the period.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
@blafourcade
blafourcade force-pushed the fix/a-record-can-predate-its-journal branch from 7037f8a to 7d07846 Compare September 4, 2026 20:03
The comment pinned the constant to a plugin script's own copy, "held to it by
the byte-compare e2e test". That script and that test went when the CLI took
the read path, so the sentence pointed a reader at a file that is not in the
tree - and at a guard that no longer exists.

The dead name is dropped rather than allowlisted in `NAMED_AS_HISTORY`:
`comments-name-files-that-exist.test.js` is the guard that caught the first
attempt at this fix, which kept the name while saying it was gone. A reader
can act on "the limit lives only here"; a deleted filename gives them
nothing.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
@blafourcade
blafourcade force-pushed the fix/a-record-can-predate-its-journal branch from 7d07846 to a58e6d9 Compare September 4, 2026 20:11
@blafourcade
blafourcade merged commit 39a4e84 into next Sep 4, 2026
17 checks passed
@aidd-bot aidd-bot Bot mentioned this pull request Sep 9, 2026
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.

1 participant