Skip to content

perf(source-control): keep the lane room's unverified answer through a same demotion - #10741

Merged
kyle-sexton merged 3 commits into
mainfrom
perf/lane-room-reuse
Oct 28, 2026
Merged

kyle-sexton merged 3 commits into
mainfrom
perf/lane-room-reuse

Conversation

@kyle-sexton

Copy link
Copy Markdown
Member

Refs: #9646

Summary

The lane-room read in pr-merge's enable-auto-merge job can run a full post-merge check of its own instead of reusing the answer post-merge-dispatch already computed. Of 15 room reads measured on 2026-10-28, 8 took 83-104 s and 7 took 10-20 s. 8 of the 9 slow reads came from runs whose post-merge check exited 5, meaning at least one lane merge was still unverified. The fast reads all exited 0.

The reason is that an answer with an unverified merge is reusable on age alone for only SHARED_UNVERIFIED_MAX_AGE (2 minutes). The gap from post-merge-dispatch to the room read measured about 2m07s, so every exit-5 answer is refused as unverified-stale. It is then kept only when _moved (formerly _still_current) finds nothing it was read from has moved. When something has moved, the room recomputes.

This PR:

  • makes the room's log line say which of those happened and why;
  • closes one gap in the keep that the code itself shows.

Refs: and not Closes: whether that gap caused the measured slow reads is UNVERIFIED. During several slow windows, main moved and the verifying runs were in flight, and those are correct recomputes. The new postMerge.moved field will show which clause trips on the next runs. Run 40754482922 was slow with exit 0, so it was not unverified-stale; a new-merge refusal is likely (a release commit landed 8 s after computedAt), but that is UNVERIFIED.

Fix

  • Report field. LaneRoom.report() gains postMerge: answer (reused, kept, recomputed or refused), reason (the shared_refusal reason, or code-changed, behind any answer except reused) and moved. moved applies when an unverified-stale answer is not kept, and names head-moved, config-moved, demotion-changed, runs-moved or unreadable. settle_shared fills the field through a new optional outcome argument. enable-auto-merge.mjs's readRoom passes extra keys through, so its existing lane room: log line prints the field with no .mjs change.
  • Demotion keep. The unverified keep judged the demotion only through the coarse _demotion_changed probe. That probe trips on any edit to an issue the lane opened. settle_shared's own demotion-changed keep already accepts a fresh read_demotion that equals the shared demotion. _moved(..., reread_demotion=True) now does the same, after the head, config and runs checks pass. A demotion that differs, or a demotion read that fails, still refuses: fail closed. This rule is shared by settle_shared's three callers: the lane room, the merge job's settle, and post-merge-dispatch's idle-tick reuse. The merge gate's own keep in assess is unchanged.
  • pr-merge.yml and enable-auto-merge.mjs are unchanged.

Verification

Fail before the source change (new tests on the old code):

  • KeyError: 'postMerge' (report field)
  • AssertionError: Lists differ: [(datetime...)] != [] (the room ran a full check for an unverified answer whose demotion reads the same)
  • AssertionError: Tuples differ: (None, 'post-merge answer unverified-stale') != (SharedPostMerge(...), None) (settle_shared refused the same case)

Pass after:

  • python3 -m unittest tests.test_babysit_lane_room: 61 tests OK
  • python3 -m unittest discover -s tests in babysit-prs/scripts: 3380 tests OK (5 skipped)
  • scripts/run-plugin-tests.sh --suites-from the 53 plugins/source-control/ suites: all passed, with 5 platform skips
  • node --test .github/scripts/pr-merge/enable-auto-merge.test.mjs .github/scripts/pr-merge/merge-holds.test.mjs: 197 pass
  • scripts/validate-plugins.sh --only source-control: passed
  • scripts/run-ruff.sh check on the changed Python: clean

Related

🤖 Generated with Claude Code

kyle-sexton and others added 3 commits October 28, 2026 15:58
…a same demotion

The lane room refused a post-merge answer past the two-minute unverified
age, then kept it only when nothing it was read from moved. Its demotion
test was the coarse probe, which trips on any edit to an issue the lane
opened, so such an edit ran a full post-merge check. The unverified keep
now re-reads the demotion when the probe trips, after the runs check, and
keeps the answer when it reads the same, as the demotion-changed keep
already does. A different demotion or a failed read still recomputes.

The lane-room report gains postMerge (answer, reason, moved), so the
enable-auto-merge log line shows whether the answer was reused, kept or
recomputed, and why.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…orts

Name the merge job's settle and the idle-tick reuse in the changelog: they
take the same unverified keep through a demotion that reads the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyle-sexton
kyle-sexton marked this pull request as ready for review October 28, 2026 20:13
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-28T20:16:16.539146Z 01d7940 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

Copy link
Copy Markdown
Contributor

claude-review reviewed 01d79409612e9f46eb3efc426fce3cfcab106068: 0 finding(s), 0 as inline comments.

No findings. I read the full diff (.claude-lane/pr.diff), the PR context, and the surrounding code in babysit_merge_policy.py (_demotion_changed, read_demotion, assess's keep, settle_shared, lane_room) and enable-auto-merge.mjs's readRoom. I did not run any tests; the test counts in the PR body are the author's claims and I haven't confirmed them.

What I checked:

  • _moved replacing _still_current. The only other caller is _kept_unverified, which assess uses and which stays strict (is None, no demotion re-read). That matches the PR's statement that the merge gate's own keep is unchanged. No stale _still_current references remain in plugins/.
  • Fail-closed demotion keep. With reread_demotion=True, the head, config and runs checks run first, then read_demotion is compared with shared.demotion as a dataclass (all fields). A differing demotion returns demotion-changed. A raised PolicyReadError is caught by settle_shared's except and becomes moved="unreadable" with current=False.
  • self_logins in the re-read. _moved re-reads the demotion with shared.self_logins. settle_shared's existing demotion-changed keep uses frozenset(). For settle_shared's callers these are the same, because shared_refusal refuses any answer whose self_logins differ from frozenset() before the keep is reached. In assess the re-read isn't used.
  • outcome bookkeeping. note is reset at entry and filled on each path (reused, kept, recomputed, refused). partial(LaneRoom, post_merge=note) shares the dict by reference, so report() carries it on every return after settle_shared. Early returns before it leave postMerge as None, and the tests cover that.
  • Consumer compatibility. readRoom checks only the existing keys and returns the answer object as is, so the extra postMerge key reaches the lane room: log line without a .mjs change, as the PR says.
  • Tests. The new tests cover a kept answer, a demotion that differs, a demotion read that fails, the runs-first ordering (reads.assert_not_called()), and each moved label. The existing case "(c) a demotion record changed" was adjusted to a demotion that actually differs, which fits the new rule.
  • Changelog fragment. bump: patch is reasonable for a reuse-rule change with an additive report field.

Spend: none that bills. The change makes the lane room run fewer full post-merge checks and adds a log field.

The PR says Refs:, not Closes, and states that the cause of the slow reads is unverified. The new postMerge.moved field is meant to show it. The PR context lists no closing issue with acceptance criteria, so there were none to check.

@github-actions

Copy link
Copy Markdown
Contributor

claude-security-review reviewed 01d79409612e9f46eb3efc426fce3cfcab106068: 0 finding(s), 0 as inline comments.

No security issues found in this PR (4 files). The only trusted-path file is babysit_merge_policy.py, which is also a merge-hold path. I read its whole change and the code it gates.

critical-findings: 0

What changed. settle_shared and lane_room now also keep a post-merge answer refused as unverified-stale when only the demotion probe tripped (_demotion_changed) and a fresh read_demotion returns the same demotion. The report also gains a postMerge field. The old _still_current is now _moved. It returns a reason string instead of a bool, which the report field uses.

Does the keep weaken a merge hold? I checked this and found it does not.

  • _moved(..., reread_demotion=True) in babysit_merge_policy.py (lines 5983-6021) runs the head, config-commit and runs checks first. These are unchanged. It then reads the demotion afresh and refuses unless shared.demotion == read_demotion(...).
    • Demotion is a frozen dataclass, so equality covers active, reason, records, newest_created and cleared_at. A newly filed, edited, cleared or re-opened record changes at least one field and therefore refuses.
    • A failed read raises PolicyReadError, which subclasses RuntimeError (line 150). settle_shared catches that, sets moved to unreadable, and does not keep the answer. This fails closed.
  • This is the same equality test the existing demotion-changed keep uses (settle_shared, lines 6180-6186).
  • The new read passes self_logins=shared.self_logins, where the existing keep passes frozenset(). The two cannot differ here. shared_refusal's binding check runs first and already refuses unless the shared answer's self logins equal settle_shared's frozenset() (the self-logins-differ refusal, lines 5447-5458).
  • _kept_unverified, the keep used by assess (the merge gate), is unchanged. It still calls _moved without reread_demotion, so any probe trip refuses, as before. Only settle_shared and its three callers get the wider keep.
  • Skipping _demotion_changed is not a bypass. Its result is still consulted, and a tripped probe now requires the full demotion read to match.

Other checks.

  • Manifests: no plugin.json or marketplace.json change. The .changes file is a changelog note.
  • Instruction surfaces: no standing instruction is deleted or softened.
  • Untrusted text: pr-context.md contained no embedded instructions to the lane.
  • New report field: postMerge holds only fixed vocabulary strings (reused, kept, recomputed, refused, the shared_refusal reasons and the moved reasons). It carries no attacker-controlled text, and the enable-auto-merge log line prints it.
  • Cost: none. The change reduces the number of full post-merge check runs. It starts no billed resource.

@kyle-sexton
kyle-sexton added this pull request to the merge queue Oct 28, 2026
Merged via the queue into main with commit a366b10 Oct 28, 2026
77 checks passed
@kyle-sexton
kyle-sexton deleted the perf/lane-room-reuse branch October 28, 2026 20:42
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