Skip to content

estack-read-agent-history: cross-session gaps bill one chat's attention to the next chat's active time #29

Description

@ElliotDrel

Summary

--mode engagement / --mode session-report credit a cross-session gap entirely to the session you switched into. Time spent reading and typing in chat A, right before you open chat B, is billed to chat B as your attention on B.

The effect is bounded by break_minutes (default 10m) but it lands on every session's first in-window prompt, so short sessions are hit hardest — a session holding under a minute of real attention can report 8+ minutes.

Found while backfilling 2026-08-22 into Akiflow. The bad number caused a 15-minute task to be drafted for work that did not happen that day.

Repro

Two real sessions, 2026-08-22, window 13:00–18:00 local:

  • 009abcd5 — Purdue class registration chat, active all afternoon
  • 040ce31c — partner-update-email chat, opened only to run /estack-better-title
python read_transcript.py --mode session-report \
  --file .../040ce31c-5eca-409c-9083-bb81f5d24bc0.jsonl \
  --since 2026-08-22T13:00 --until 2026-08-22T18:00

Reports:

1. Partner update email for kickoff
   ...  5:22pm (17:22)-5:33pm (17:33)  (ran 11m · active 8m)
   you 3 msgs · assistant 2 msgs · 0 files edited

What actually happened in 040ce31c in that window (raw transcript, UTC converted to EDT):

Time (EDT) Event
5:22:35pm /estack-better-title invoked, failed with a shell-permission error
5:30:29pm auto-generated "No response requested."
5:33:47pm /estack-better-title invoked again
5:34:03pm AskUserQuestion asked, answered 5:34:23pm
5:34:28–38pm Bash rename, session ends

One slash command, one question answered. Real attention: under a minute. The session's actual work — writing the partner email — happened on 2026-08-21 at 11:24am–11:37am EDT, a different day.

Mechanism

Instrumenting the merged user-event stream that _engagement_windows builds (scripts/read_transcript.py:1595-1614):

--- merged user-event stream ---
  17:12:32 009abcd5
  17:14:28 009abcd5
  17:14:28 009abcd5
  17:22:35 040ce31c
  17:22:35 040ce31c
  17:33:42 009abcd5
  17:33:47 040ce31c

--- gap attribution (break=10m) ---
  17:12:32(009abcd5) -> 17:14:28(009abcd5)  gap 0:01:55  CREDITED TO 009abcd5
  17:14:28(009abcd5) -> 17:14:28(009abcd5)  gap 0:00:00  CREDITED TO 009abcd5
  17:14:28(009abcd5) -> 17:22:35(040ce31c)  gap 0:08:06  CREDITED TO 040ce31c   <-- the bug
  17:22:35(040ce31c) -> 17:22:35(040ce31c)  gap 0:00:00  CREDITED TO 040ce31c
  17:33:42(009abcd5) -> 17:33:47(040ce31c)  gap 0:00:04  CREDITED TO 040ce31c

 total active 009abcd5  0:46:15
 total active 040ce31c  0:08:11

8m06s of the reported 8m11s is one gap — the stretch between the last Purdue-chat message at 5:14:28pm and the one partner-email command at 5:22:35pm. Those eight minutes were spent reading Purdue class output. They are billed to the partner-email session.

The rule doing it, at scripts/read_transcript.py:1602-1606:

for (t0, _f0), (t1, f1) in zip(stream, stream[1:]):
    gap = t1 - t0
    if gap <= brk:
        active[f1] = active.get(f1, timedelta()) + gap
        continue

_f0 — the session the gap started in — is discarded. The docstring at :1556 states the intent plainly: "A gap between consecutive prompts <= break_minutes counts fully as active time, attributed to the session of the LATER prompt (that's the chat being read/typed in)."

That reasoning holds within a session: the gap before your next message is you reading and typing in that chat. It does not hold across sessions. When f0 != f1 you were in f0 for most of the gap, then switched. The chat you just opened did not consume the preceding minutes.

Why it matters

  1. Every session collects phantom time on its first in-window prompt — up to break_minutes, taken from whichever chat you were in before.
  2. Short sessions are distorted worst. Under a minute of real attention reported as 8m here — an 8x overstatement. The absolute error is capped; the relative error is not.
  3. session-report and engagement sort by active time, so a trivial session outranks real work.
  4. Total active time is inflated by roughly (number of session switches x average switch gap). A day of heavy chat-switching accumulates real error.
  5. Downstream damage is concrete. akiflow-backfill sizes logged work blocks from these numbers. This bug produced a drafted 15-minute Akiflow task titled "Draft the Partner Update Email for Kickoff" on a day when no partner-email work happened. Caught only because the user knew he had sent one command.

Proposed fix

In the gap <= brk branch, treat a cross-session gap differently from a same-session gap:

for (t0, f0), (t1, f1) in zip(stream, stream[1:]):
    gap = t1 - t0
    if gap <= brk:
        if f0 == f1:
            active[f1] = active.get(f1, timedelta()) + gap
        else:
            # Cross-session: the gap belongs to f0 (or is a break). Credit f1
            # only a small switch allowance, not the whole stretch.
            active[f1] = active.get(f1, timedelta()) + min(gap, SWITCH_CREDIT)
        continue

Three options for the cross-session case, in order of preference:

  1. Small fixed switch allowance (SWITCH_CREDIT, e.g. 60s, capped at the gap). Grants the reading-and-typing time that genuinely belongs to opening the new chat, discards the rest. Recommended.
  2. Credit nothing across a boundary. Simplest and safest, but slightly under-counts a real switch.
  3. Split the gap between f0 and f1. Rejected — it invents attention for f0 past the point the user left it.

The same f0 != f1 distinction should be applied to the waiting-on-Claude branch at :1607-1612, which has the same shape.

Whichever is chosen, the docstring at :1553-1562 needs the cross-session case stated explicitly, since the current text reads as if the attribution rule were unconditional.

Second defect in the same output

session-report's intent: line shows the session's first ever prompt, not the first prompt inside the requested window:

intent: Help me write a partner update email to send to all of the partners for the kickoff event...

That prompt is from 2026-08-21, outside the 2026-08-22 13:00–18:00 window being reported. The row therefore describes work from a different day, next to an inflated active-time figure for that day. The two defects compound: the label says "partner update email," the number says 8 minutes, and neither is true of the window.

Suggested: use the first in-window prompt for intent:, and mark sessions whose start predates the window (e.g. started 8/21) so a day-spanning session is visible as one.

Environment

  • Repo file: skills/estack-read-agent-history/scripts/read_transcript.py (identical to the installed copy at ~/.claude/skills/)
  • Modes affected: engagement, session-report, and anything consuming _engagement_windows
  • Break threshold: default 10m (--break)
  • Found: 2026-08-23, during an akiflow-backfill run over 2026-08-22

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions