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
- Every session collects phantom time on its first in-window prompt — up to
break_minutes, taken from whichever chat you were in before.
- 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.
session-report and engagement sort by active time, so a trivial session outranks real work.
- Total active time is inflated by roughly (number of session switches x average switch gap). A day of heavy chat-switching accumulates real error.
- 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:
- 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.
- Credit nothing across a boundary. Simplest and safest, but slightly under-counts a real switch.
- 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
Summary
--mode engagement/--mode session-reportcredit 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 afternoon040ce31c— partner-update-email chat, opened only to run/estack-better-titleReports:
What actually happened in
040ce31cin that window (raw transcript, UTC converted to EDT):/estack-better-titleinvoked, failed with a shell-permission error"No response requested."/estack-better-titleinvoked againAskUserQuestionasked, answered 5:34:23pmBashrename, session endsOne 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_windowsbuilds (scripts/read_transcript.py:1595-1614):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:_f0— the session the gap started in — is discarded. The docstring at:1556states 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 != f1you were inf0for most of the gap, then switched. The chat you just opened did not consume the preceding minutes.Why it matters
break_minutes, taken from whichever chat you were in before.session-reportandengagementsort by active time, so a trivial session outranks real work.akiflow-backfillsizes 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 <= brkbranch, treat a cross-session gap differently from a same-session gap:Three options for the cross-session case, in order of preference:
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.f0andf1. Rejected — it invents attention forf0past the point the user left it.The same
f0 != f1distinction should be applied to the waiting-on-Claude branch at:1607-1612, which has the same shape.Whichever is chosen, the docstring at
:1553-1562needs 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'sintent:line shows the session's first ever prompt, not the first prompt inside the requested window: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
skills/estack-read-agent-history/scripts/read_transcript.py(identical to the installed copy at~/.claude/skills/)engagement,session-report, and anything consuming_engagement_windows--break)akiflow-backfillrun over 2026-08-22