Skip to content

fix: keep reading git while work is still landing - #18

Merged
iamnotstatic merged 5 commits into
iamnotstatic:mainfrom
joshuanwankwo:fix/rescore-late-commits
Aug 6, 2026
Merged

fix: keep reading git while work is still landing#18
iamnotstatic merged 5 commits into
iamnotstatic:mainfrom
joshuanwankwo:fix/rescore-late-commits

Conversation

@joshuanwankwo

@joshuanwankwo joshuanwankwo commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Stacked on #16 and #17 — the diff to review here is the last commit.

Problem

Two ways a session loses work it genuinely did.

1. You step away instead of closing down. The reaper finalizes anything idle for 30 minutes, but it only writes a duration — stats stay frozen at whatever the last hook event happened to see. Commit, then shut the laptop without ending the session, and that commit is never read. The session is submitted with zero.

# commit 80 lines, walk away, never return to the session
05 aug  app  38m  idle         ← before  (the commit is right there in git)
05 aug  app  38m  interrupted  ← after, locally — the reaper's exitCode still
                                  reads as interrupted; the leaderboard, which
                                  scores from raw stats and ignores exitCode,
                                  sees it as shipped

2. You commit a minute after closing the tab. The work was already there — it was in the diff when the session ended — but as uncommitted lines, so commits: 0 and the session scores tinkering instead of shipped.

Approach

Sessions are re-read from git while their stats can still move: while open, and for 30 minutes after they close (GRACE_MS, the same inactivity constant used everywhere else).

The refresh runs before the reaper, so a session the reaper is about to close carries the work it actually did rather than whatever the last event saw. Corrected stats clear submittedAt and resubmit on the next flush — the server upserts on id, the same route #14's reaper-recovery fix relies on.

Wired into the existing reapOrphanedSessions() call sites via refreshAndReap(), so vibe status, vibe log, vibe share, a new session opening, and the wrapper all pick it up. No new commands, no new timers.

Notes / decisions

  • A commit is credited to exactly one session. If another session watching the same repo is open right now, or started after this one closed, that session owns anything landing from here on and this one leaves it alone (ownedByAnotherSession) — covers both a strictly later session and two sessions genuinely overlapping in time.
  • The window is deliberately short. Past 30 minutes a closed session is final. Work that lands hours later belongs to whichever session was open at the time — crediting it backwards would be inventing history, and the daily shipped cap makes that worth being careful about.
  • interrupted is left alone. A reaped session keeps exitCode: 1 and reads as interrupted locally; only its stats are corrected. The server scores from raw stats and ignores the tier, so the leaderboard sees the work either way, and wasReapedInterrupted keeps working unchanged.
  • Sessions from an older CLI have no repo baselines and are skipped — nothing to measure against.
  • Storage stays git-free: the new pass lives in src/rescore.ts, db.ts keeps its single responsibility.
  • No new dependencies, no server changes, privacy model unchanged.

Testing

Against a throwaway $HOME with real repos:

  • commit one minute after SessionEnd: credited, rescored shipped, submittedAt cleared for resubmission
  • commit 45 minutes after: not credited, session left final
  • a strictly later session on the same repo: it claims the commit, the earlier one doesn't
  • genuine overlap — session A opens, session B opens, B closes while A is still open, then a commit lands: A claims it, B (in its own grace window) doesn't
  • laptop-shut simulation (open session, commit, idle past the timeout): reaper closes it with the commit intact
  • legacy row with startSha and no repos: untouched
  • fix: score sessions started from a directory of repos #16 and fix: count commits made in a linked worktree #17 suites re-run green
  • tsc clean

Update: addressed both review comments — a closed session no longer double-credits a commit that a still-open session on the same repo will also count (ownedByAnotherSession now checks for that, not just later start times), and the description above no longer claims the walk-away example reads shipped locally; it reads interrupted locally and shipped only on the leaderboard, which was the actual, correct behavior all along. Rebased onto #17's fix.

A session is only measured against the repo its cwd sits in. Plenty of
sessions are started a level up instead — from a parent holding `api/` and
`web/` side by side, or a plain `~/dev`. That directory has no HEAD, so the
session records `startSha: ''` and can never score above idle no matter how
much shipped inside it.

Sessions now watch a set of repos rather than one: the repo cwd is in, or,
when cwd is not itself a repo, the repos directly inside it. Stats are summed
across them, so work spread over several repos in one session is counted
once, in full. Discovery is a filesystem check one level deep, bounded at 100
entries and 10 repos, so it costs no git spawn per candidate.

The baseline moves from a single `startSha` to a `repos` array. `startSha` is
still written and still read as a fallback, so sessions opened by an older
CLI keep scoring across the upgrade.

Hook sessions from a directory with no repos in or under it are now skipped
rather than opened as permanently-idle rows — a quick question from the home
folder leaves no trace. The shell wrapper still tracks a non-repo directory by
wall clock, as before; you invoke it deliberately.
A session is measured from the HEAD of the checkout it started in. Commits
made in a linked worktree never move that HEAD, so an agent that works in
`.claude/worktrees/<name>` — the default for Claude Code Desktop — leaves the
session reading zero commits and a clean tree. It scores idle however much
shipped.

Committed work is now the set of commits reachable from any current checkout
tip but from no baseline, asked for in one rev-list per repo. A worktree's
commits are reachable from its own tip and from no baseline, so they count.
Merging that worktree back mid-session doesn't double count: the commits sit
in the same reachability set whether one tip or two can see them.

Each worktree carries its own baseline, taken when the session opens, so a
worktree parked on a branch that diverged last month reads as zero — only what
moved during the session counts. A worktree created mid-session has no
baseline of its own and is measured from the repo's, since its commits are new
by definition.

Committed lines are now summed per commit rather than diffed net across a
range: a range diff needs a single tip, and running one per tip would count
shared history twice. Merge commits report no numstat, so merged work is
counted once, where it was written. Uncommitted work is still read per
checkout — only one working tree can hold it.
@iamnotstatic

Copy link
Copy Markdown
Owner

Checked the parts this leans on and they all hold up: the submittedAt clear works, wasReapedInterrupted still matches, and the server cap query excludes the session's own id so resubmits don't bounce. Refresh cost is basically zero.

Two small things:

  1. If two sessions overlap in the same repo and a commit lands in the older one's grace window after it closes, both credit it. Maybe also skip the refresh when another session on the repo is still open. Not blocking.

  2. The example in the description shows the walk-away session as shipped in vibe status, but it'll actually show interrupted locally (only the leaderboard sees shipped). Worth fixing so nobody files a bug about it.

Needs a rebase once #17 is fixed since the refresh reads through the same counting code.

Two counting bugs from review.

A repo cloned next to a linked worktree of itself has a `.git` on both sides,
so discoverRepos surfaced both as separate top-level repos. Each one's
worktree list then enumerates the same pair of checkouts, so every commit and
line landed in both entries and was counted twice.

Candidates are now deduped by git-common-dir, keeping the main checkout — its
worktree list is the one that enumerates every linked checkout; a linked
worktree's list of itself does not. Resolving through realpath before
comparing matters here: a linked worktree's --git-common-dir comes back
already resolved (e.g. through /private/var on macOS), while the main
checkout's relative ".git" resolves through the unresolved cwd — same
directory, two strings, no match without it.

Separately, filesTouched summed the committed file set and the uncommitted
one, so a file both committed and then re-edited in the same session counted
twice. committedStats now returns the file set instead of a count, and the
two sets are unioned before counting.
Two ways a session loses the work it did.

The reaper closes anything idle for 30 minutes, but it only writes a duration
— stats stay frozen at whatever the last hook event saw. Commit, then shut the
laptop without ending the session, and the commit is never read: the session
is submitted with zero. This is the case for anyone who steps away mid-task
rather than closing things down cleanly.

The other is narrower but more common: you close the session and commit a
minute later from the terminal. The work was there — it was sitting in the
diff when the session ended — but only as uncommitted lines, so the session
scores tinkering instead of shipped.

Sessions are now re-read from git while their stats can still move: while
open, and for 30 minutes after they close. The refresh runs before the reaper,
so a session it is about to close carries the work it actually did. Corrected
stats clear submittedAt and resubmit; the server upserts on id.

A commit is only credited to one session. If a later session watching the same
repo has already started, its baseline covers anything committed from then on,
so the earlier one leaves it alone. Past the 30 minute window a closed session
is final — work that lands hours later belongs to whatever session was open at
the time, not to this one.
…y owns

Review found the grace-window fix could double count. Two sessions watching
the same repo don't have to be sequential — one can open while the other is
still running. If the earlier one closes first, it was still eligible for a
refresh during its grace window, and would pick up the same commit the
still-open session was already going to count on its own.

ownedByAnotherSession (renamed from supersededBy) now also skips a closed
session's refresh when another session on the same repo is open right now,
regardless of which one started first — an open session always claims new
work on its own, so a closed one never needs to.
@joshuanwankwo
joshuanwankwo force-pushed the fix/rescore-late-commits branch from f5719ed to 61051c2 Compare August 5, 2026 22:10
@iamnotstatic
iamnotstatic merged commit b2d1598 into iamnotstatic:main Aug 6, 2026
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.

2 participants