Skip to content

fix: reconcile Hermes attribution per bucket without global equality - #197

Merged
pitimon merged 2 commits into
mainfrom
fix/hermes-per-bucket-reconciliation
Aug 24, 2026
Merged

fix: reconcile Hermes attribution per bucket without global equality#197
pitimon merged 2 commits into
mainfrom
fix/hermes-per-bucket-reconciliation

Conversation

@pitimon

@pitimon pitimon commented Aug 24, 2026

Copy link
Copy Markdown
Owner

Per-bucket Hermes reconciliation (option B) — replaces the exact-equality grand-total gate

Follow-up to the findings in this comment. Closes #169's historical-reconciliation blocker.

Problem

reconcileHermesMixedModelUsage() required sameHermesTokenTotals(existingTotals, authoritativeTotals) exactly before rebuilding. On any real long-running install this precondition never holds:

  • Retention gapstate.db deletes old sessions after ingestion, leaving existing buckets with tokens the authoritative view can never see again (verified orphan: hermes|gpt-5.5|2026-06-23T15:30Z, 15,787 tokens; state.db retains only sessions from 2026-08-05).
  • Live growth — unfinished sessions write to state.db between the two reads, so the totals drift on every attempt (4 production attempts all returned blocked_total_mismatch; delta oscillated 1.06M–1.32M).

Change

Replaces the all-or-nothing gate with per-(model, half-hour)-bucket comparison:

  1. Rebuild canonical buckets from state.db as today.
  2. Canonical bucket differs from the existing accumulator → append a latest-wins correction row after the consumed upload offset (unchanged safe-append/rollback machinery in appendHermesReconciliationRows).
  3. Existing key with no canonical counterpart and total_tokens > 0 → zero-out latest-wins row:
    • outside retained coverage (hour < MIN(started_at) across sources) → provable orphan;
    • inside coverage → stale observation placement from incremental syncs of long-running sessions (canonical places everything at the session's current last_seen bucket). Unfinished sessions re-emit growth at their new bucket on the next incremental sync.
  4. Migration receipt records bucketsCorrected / bucketsSkipped / orphansZeroed / stalePlacementsZeroed; status: applied short-circuits reruns (idempotent).
  5. Queue history behind the offset stays byte-identical; state-write failure still truncates back.

Verification

  • New tests: per-bucket correction without global equality, matched-bucket skip under live growth, stale-placement zero-out, updated mismatch-corrected assertion. Full suite: 1061 passed, 0 failed.
  • Live-state dry-run (isolated copy of cursors/queue/queue-state + state.db snapshot): receipt applied, 275 corrected, 78 skipped, 1 orphan zeroed (gpt-5.5), 348 stale placements zeroed. After persisting cursors: hourly state == canonical DB view == queue latest-wins exactly (7,611,980,055 tokens, diff 0); fable per-model diff 0. Second run appended 0 bytes.
  • Independent read-only review of commit 65a6b242: sound implementation, unfinished-session trade-off verified safe.

@pitimon
pitimon merged commit 85ca586 into main Aug 24, 2026
2 checks passed
@pitimon
pitimon deleted the fix/hermes-per-bucket-reconciliation branch August 24, 2026 03:59
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.

[bug]: Hermes mixed-model sessions are attributed entirely to sessions.model

1 participant