Skip to content

Let a merged PR stop hiding a session's next task (XERK-224) - #403

Merged
xerhab merged 1 commit into
mainfrom
XERK-224
Aug 7, 2026
Merged

Let a merged PR stop hiding a session's next task (XERK-224)#403
xerhab merged 1 commit into
mainfrom
XERK-224

Conversation

@xerhab

@xerhab xerhab commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #396. Fixes a real hole in the Ready-for-review rule that shipped there.

The bug

XERK-224 demoted a session out of Ready-for-review once every PR it opened reached MERGED/CLOSED — merging is the review, so it drops to Idle to be parked until the build is verified. But that demotion was absolute, and a session is a conversation, not a pull request.

Give the same session a new task after the merge, and it finishes work with no new PR to show for it. The landed PR hid that result permanently — the rule never even reached the finished-turn check:

if (prs.length) return prs.some(p => !prLanded(p));   // early return: pinned to Idle forever

The fix

The demotion is now scoped in time.

_poll_prs_landed runs beside _poll_pr_conflicts, off the status the PR sweep just cached (no extra network call), and stamps prsLandedTs — the session's own last-activity timestamp at the moment every PR it owns is seen landed. _new_work_since_prs compares that against the session's current last activity and reports newWorkSincePrs; readyForReview falls through to the finished-turn signal once the conversation has moved past the landing.

if (prs.some(p => !prLanded(p))) return true;   // an unlanded PR is a diff to read
if (prs.length && !s.newWorkSincePrs) return false;   // merged, and nothing said since
return sig.lastRole === "assistant" && !sig.lastHasToolUse;

Three details worth keeping:

  • Both sides are transcript ENTRY timestamps, so the comparison runs on the conversation's own clock — never wall time, and never file mtime, which a synced ~/.claude or a backup restore inflates to copy-time (the trap XERK-73 already documents).
  • A newly opened PR clears the stamp, so the next landing is measured fresh rather than against the previous round.
  • An unfetched PR state stamps nothing — "not looked at" is not "landed", and stamping early would measure new work against the wrong moment.

The answer is false whenever it can't be known (no stamp, undated transcript, an agent too old to report it), which is exactly the behaviour that shipped before this expiry — so a lagging host degrades rather than flips.

Mirrored in all four places the rule lives (sessions.html, server.js, core/Sessions.kt, glasses/src/sessions.ts), so the hub's one ready-for-review alert fires for the follow-up too — with a body that says "nothing to merge" rather than naming the PR that already landed.

Verification

suite result
agent (Python) 1005 — 5 new in TestPrsLanded
turma 14/14 files — 278 server, 68 sessions
glasses 463 + typecheck
Android 226 compiled and run in the agent image (JDK 17 / Gradle 8.11.1 / SDK 35)
browser E2E 23/23 against a real hub

The last three browser checks walk the reported lifecycle end to end:

PASS  merged + nothing since: parked in Idle            {"review":[],"idle":["Merged Task"]}
PASS  a new task after the merge surfaces under Ready for review
                                                        {"review":["Merged Task"],"idle":[]}
PASS  and reads as a finished turn, not as the old PR   ["finished · awaiting review"]

The hub alert timeline was exercised through the real heartbeatAlerts with a captured FCM sender: PR announced → merged → alert retracted → new task → announced again, without naming the merged PR.

Two things worth flagging

  • A test in Add a "Ready for review" session group, with one alert (XERK-224) #396 lied. "a merged PR still comes back once the session runs again" asserted the session stayed in Idle, and its comment described behaviour the code didn't have. Rewritten to test what its name claims. Compiling the Android side also caught a genuine error in my new Kotlin test (JUnit's assertEquals takes the message first) — worth noting since android-ci never ran on Add a "Ready for review" session group, with one alert (XERK-224) #396 due to the Actions outage; I ran it locally in the agent image.
  • CLAUDE.md remains jammed at its cap. This change needed ~850 characters of new documentation, so it took another consolidation pass to land at 149,991 of 150,000. That's now the fourth PR in a row paying this tax.

XERK-224 demoted a session out of Ready-for-review once every PR it opened
reached MERGED/CLOSED — merging IS the review, so it drops to Idle to be parked
until the build is verified. But that demotion was ABSOLUTE, and a session is a
conversation, not a pull request: give the same session a new task after the
merge and it finishes work with no new PR to show for it, and the landed one
hid that result for good. The rule never even reached the finished-turn check:

    if (prs.length) return prs.some(p => !prLanded(p));   // early return

So the demotion is now scoped in TIME. `_poll_prs_landed` runs beside
`_poll_pr_conflicts`, off the status the PR sweep just cached (no extra network
call), and stamps `prsLandedTs` — the session's own last-activity timestamp at
the moment every PR it owns is seen landed. `_new_work_since_prs` compares that
against the session's current last activity and reports `newWorkSincePrs`;
`readyForReview` falls through to the finished-turn signal once the
conversation has moved past the landing. A newly opened PR clears the stamp so
the next landing is measured fresh, and an unfetched PR state stamps nothing —
"not looked at" is not "landed", and stamping early would measure new work
against the wrong moment.

Both sides of that comparison are transcript ENTRY timestamps, so it runs on
the conversation's own clock: never wall time, and never file mtime, which a
synced ~/.claude or a backup restore inflates to copy-time (the trap XERK-73
already documents). The answer is False whenever it can't be known — no stamp,
an undated transcript, an agent too old to report it — which is exactly the
behaviour that shipped before this expiry, so a lagging host degrades rather
than flips.

Mirrored in all four places the rule lives (sessions.html, server.js,
core/Sessions.kt, glasses/src/sessions.ts), so the hub's one ready-for-review
alert fires for the follow-up too — and its body says "nothing to merge" rather
than naming the PR that already landed.

Verified: 1005 agent tests (5 new, TestPrsLanded), turma 14/14 files (278
server, 68 sessions), glasses 463 + typecheck, Android 226 tests compiled and
run in the agent image, and the browser E2E 23/23 against a real hub — the last
three checks walk the reported lifecycle: merged and quiet parks in Idle, a new
task surfaces under Ready for review, and it reads as a finished turn rather
than as the old PR.

Also corrects a test whose NAME claimed this behaviour ("a merged PR still
comes back once the session runs again") while asserting the opposite.
@xerhab
xerhab merged commit 3080189 into main Aug 7, 2026
8 checks passed
@xerhab
xerhab deleted the XERK-224 branch August 7, 2026 18:28
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.

1 participant