Repository navigation
[bug] compaction breaks cross-session human-time dedup in local dira report --all #216
Description
What happened
Store::compact rolls human time up per (day, session) group. It calls accounting::total_human_seconds(&acc.signals, idle) once per group (cli/core/src/store.rs, the rollup upsert in compact). The raw report dedups human time across sessions. The rollup does not. So after compaction, local dira report --all counts overlapping sessions twice. Gaps that cross UTC midnight are lost, so long sessions are undercounted too.
Example: one project, 20 days old. Session s1 prompts at 10:00 and 10:02. Session s2 prompts at 10:01 and 10:03. Before compaction, report --all says 180 s. After compaction the rollup holds 120 s + 120 s = 240 s, and report::build_merged adds it to the report.
Two smaller problems sit in the same path:
report_cmd(cli/dirad/src/control.rs) formatssince_dayfrom local midnight, but rollup rows are keyed by UTC day. This only matters whenretention_days < 8(the setting accepts 0). Example: retention 0, UTC+3. Today's local 00:00 to 03:00 sits in yesterday's UTC rollup row, whichday >= '<local today>'leaves out, so--todaydrops up to 3 h. A session that spans the aligned UTC midnight is also counted in bothrollup_session_countand the rawsession_count.- Compaction now stops at the midnight of the oldest unsynced event's day, so a day is never part rollup and part raw (fix/cli-quick-wins). If sync stops for good (logged out, a relink needed after a 401, a persistent 413) and the user has a back-dated
dira log, compaction of every day from that date onward stops for good, and the database grows. This costs storage, not billing.
What you expected
report::build_merged after compaction returns the same totals as report::build over the raw events before it, for concurrent multi-session streams too.
Repro
dira --version: 0.11.1-develop.2 (fix/cli-quick-wins)- Install method: built from source
- OS + arch: macOS arm64
- Harness and version: any
- Steps to reproduce: found in code review. The fastest repro is a store test. Append two interleaved sessions on one project dated past retention, set the sync cursor past them, run
compact, and comparereport --alltotals before and after.
Output
Proposed fix: compute counted_gaps once over all signals in the pass. Attribute each gap to its opening signal's session, project and UTC day, then upsert those. Add a proptest asserting build_merged after compaction equals build before it. Key since_day by UTC day, or compare instants. Log, or add a doctor check, when the unsynced-day guard holds back more than a couple of days.
The cloud is not affected: it receives intervals at sync time, before compaction.
Does this involve captured data?
No.
Is this a security issue?
No.
Activity
claude commentedon Oct 27, 2026
Claude finished @azlekov's task in 1m 11s —— View job
The bug is real, but the fix depends on code that isn't on
developyet, so I've marked this Needs info rather than writing a plan.Labels stay
area:accountingandarea:store. I addedneeds:decision, set the type to Bug and set Triage state to Needs info. Risk is High because a wrong rollup is written once and the raw rows are deleted. Priority is Medium because the cloud is unaffected. Source is Internal.What I confirmed in the code
Store::compact(cli/core/src/store.rs:1073) callstotal_human_secondsonce per(day, session)group. That double-counts overlapping sessions and drops gaps that cross UTC midnight, as the issue says.accounting::active_seconds(&acc.activity, idle)(store.rs:1074), which trims gaps at the human idle threshold and only looks at agent-activity events.report::buildusesagent_active_seconds(samples, agent_policy)over every event (report.rs:119). So "build_mergedafter equalsbuildbefore" fails for agent seconds too, even for a single session.report_cmd(cli/dirad/src/control.rs:691) formatssince_dayfrom local midnight, as described.origin/developat4408d16has nofix/cli-quick-winsbranch or PR.maintenance_sweep(cli/dirad/src/lib.rs:838) still uses a plainnow - retentioncutoff.Questions for the team
fix/cli-quick-wins, and will it land before this? Without the guard, the compacted set isn't a time prefix. Unsynced old rows stay raw between compacted ones, so a gap can't be cleanly given to either the rollup or the raw report. The plan for gaps at pass and cutoff boundaries depends on that guard.session_rollup_dailyis keyed by(day, session_id)with oneprojectcolumn, butbuildassigns each gap to its opening signal's project. Per-project totals can only match ifprojectjoins the key, which needs a migration. The other option is to only promise matching grand totals.total_agent_seconds.(project, orchestration=false)(report.rs:186), whilebuildsplits out orchestration lines. Should the proptest compare only totals and non-orchestrator lines, or should the rollup start storing the orchestration flag?dira doctorcheck? A doctor check addsarea:doctor.fix(cli): sum token usage into daily rollups and compact only whole u…
chore(release): v0.12.0-develop.1 [skip ci]