Repository navigation
Description
Problem
Refine writes the result of an item with two requests. store.write_issue_body(...) comes first. Then set_field or set_marks follows (refine.py, _apply_refined). The job record is a block inside the issue body, so each call is one PATCH.
A stop between the two leaves the new body with the old marks. The next round reads this as a person's edit (WaitingItem(EDIT)) and refines the item again, with model spend.
apply_alternatives has the same order: write_issue_body, then set_marks, then comment. A stop after the body leaves an item with a new block and no checked_at. The item looks unchecked and is checked again.
A simple fix is not enough. Suppose refine wrote the body and the marks in one PATCH, and then the Status in a second request. A stop between them would leave refine.next_step=ready and a new refine.body_hash on an item whose Status is still New.
In _still_waiting (refine.py), NextStep.READY on a New item means that the stage job or a person returned it. The item would get a new look with model spend. Then RETURNED_BODY_HASH would make the second pass wait on a person. That moves the loss window and does not close it.
Expected
Put the body, the labels and the marks of one result in one PATCH.
GitHubRoadmapStore.set_fieldalready writes the labels and the marks in onePATCHthrough_edit_issue.- Give
_edit_issue,set_fieldandset_marksan optional argumentbody. When it is given, the request carrieswith_record(body, encode_job_record(held, changes)). write_issue_bodystays forrepair.py, which has no marks.- Change the abstract methods (stores/store.py), the GitHub store, the dry-run request plan (request_plan.py) and the fake store (tests/support/fakes/roadmap_store.py).
Use it in _apply_refined for all three paths:
- an item set Ready:
set_field(..., body=new_body, marks=marks); - a Ready item set back to New:
set_field(..., body=new_body, marks=marks); - an item that keeps its Status:
set_marks(..., body=new_body).
Use it in apply_alternatives: set_marks(check.item, marks, body=check.new_body). A check changes no Status.
The comment of refine and of alternatives stays a separate request after the write. A stop before it loses a comment, not the state.
The request count of the normal path goes down by two for each refined item and each check: one read and one PATCH of write_issue_body.
Acceptance criteria
-
uv run pytest tests/roadmap/refine/test_roadmap_refine_one_write.py -k stop_after_first_write- the only write of an item is one request. A stop after it leaves the new body, the new marks and the new Status together. The item is not aWaitingItem(EDIT), and it is not a spurious stage return. -
uv run pytest tests/roadmap/refine/test_roadmap_refine_one_write.py -k no_status_change- an item that keeps its Status is written with one request that holds the body and the marks. -
uv run pytest tests/roadmap/test_roadmap_alternatives_one_write.py -k stop_after_write- a stop after the write leaves the new block andchecked_attogether. -
uv run pytest tests/roadmap/test_roadmap_dry_runs.py -k body_with_marks- the dry-run plan lists onePATCHfor the body with the marks. -
uv run pytest tests/roadmap/stores/test_roadmap_store_body_with_marks.py -k set_marks_body-set_markswithbodysends onePATCHwith the new body and the record block.
Every command above fails on the unchanged code. The pull request cites the fail-to-pass run (devops ci fail-to-pass --base <ref>).
Feasibility
I read these files at origin/main:
- refine.py:
_apply_refined,_refine_marks,_still_waiting,_returned_wait. - alternatives.py:
apply_alternatives. - stores/github_store.py:
write_issue_body,set_field,set_marks,_edit_issue,_send_edit. - stores/store.py (line 917) and request_plan.py.
_edit_issue builds the body from issue.split() and the record block from encode_job_record(held, changes). A new body argument replaces the first part only. set_field reads the issue before the write and refuses a field that someone changed. That check stays.
I did not run the code or the tests. A first plan wrote the body and the marks in one PATCH and the Status in a second request. A check showed the fault described above. This plan replaces it.
Existing tool considered
with_record and encode_job_record of the store. No library is needed.
Cost
Build: about 60k tokens and 3 h. Keep: about 2k tokens per quarter. Fix: about 12k tokens and 1 h.
Related
Part of #3027. Order: after the issue #4044 (alternatives.py) and the issues that change store.py before it.
Activity
dan-petty commentedon Nov 7, 2026
Intake placed this item to the backlog: intake puts every new item in the backlog. A person plans the milestones.
fixprefix of the title gives the type.(roadmap)scope of the title gives the scope.A service stop between two writes leaves inconsistent state and triggers wasted model calls, but only during a stop event.It prevents spurious re-refine cycles and wasted model spend after a service stop.It changes the abstract store, GitHub store, dry-run plan, fake store, refine, and alternatives, with new tests.Both touch the write path for alternatives and stage, but #4034 writes items one at a time as done while the candidate merges body and marks into one PATCH per item.