Skip to content

[bug] compaction breaks cross-session human-time dedup in local dira report --all #216

Open

Description

@azlekov

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) formats since_day from local midnight, but rollup rows are keyed by UTC day. This only matters when retention_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, which day >= '<local today>' leaves out, so --today drops up to 3 h. A session that spans the aligned UTC midnight is also counted in both rollup_session_count and the raw session_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 compare report --all totals 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

  1. added
    area:accountingHuman/agent time, tokens, pricing and orchestration rollups
    area:storeLocal SQLite store, its schema and stored time columns
    on Oct 27, 2026
  2. added theissue type on Oct 27, 2026
  3. claude commented on Oct 27, 2026

    @claude

    Claude finished @azlekov's task in 1m 11s —— View job


    The bug is real, but the fix depends on code that isn't on develop yet, so I've marked this Needs info rather than writing a plan.

    Labels stay area:accounting and area:store. I added needs: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) calls total_human_seconds once per (day, session) group. That double-counts overlapping sessions and drops gaps that cross UTC midnight, as the issue says.
    • Agent time has the same kind of mismatch. The rollup uses 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::build uses agent_active_seconds(samples, agent_policy) over every event (report.rs:119). So "build_merged after equals build before" fails for agent seconds too, even for a single session.
    • report_cmd (cli/dirad/src/control.rs:691) formats since_day from local midnight, as described.
    • The unsynced-day guard is not in this repo. origin/develop at 4408d16 has no fix/cli-quick-wins branch or PR. maintenance_sweep (cli/dirad/src/lib.rs:838) still uses a plain now - retention cutoff.

    Questions for the team

    1. Where is 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.
    2. Should the rollup handle sessions whose signals carry more than one project? session_rollup_daily is keyed by (day, session_id) with one project column, but build assigns each gap to its opening signal's project. Per-project totals can only match if project joins the key, which needs a migration. The other option is to only promise matching grand totals.
    3. Is the agent-time mismatch above in scope here, or a separate issue? It decides whether the proptest can compare total_agent_seconds.
    4. Rollups merge into (project, orchestration=false) (report.rs:186), while build splits out orchestration lines. Should the proptest compare only totals and non-orchestrator lines, or should the rollup start storing the orchestration flag?
    5. For the stalled-guard warning, do you want a log line or a dira doctor check? A doctor check adds area:doctor.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:accountingHuman/agent time, tokens, pricing and orchestration rollupsarea:storeLocal SQLite store, its schema and stored time columnsneeds:decisionBlocked on a product or technical decision

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions