Concurrency
You can start several runs at once. Most of what a run does needs no coordination. Only four things are shared, and each has its own rule.
| Shared thing | Rule | Code |
|---|---|---|
| The Flare VM | A numbered ticket and a lock file, first come first served. | factory2/lib/VmQueue.psm1 |
| The files in the plugin repository | A claim per run, and a later run waits for an earlier one that plans to change the same files. | Set-RunClaim, Wait-ForOverlap |
| The code graph | A mutex around updates, and a private copy per run. | scripts/graph.ps1 -Snapshot |
| Main | Replay the change on the latest main when it matters. | Sync-WithMain |
Per-run work folders
Section titled “Per-run work folders”Every run gets its own folder, .work/issue-N-<time>. It holds the clone of the plugin, the agent files, the patch, the log, the event files and the run’s copy of the code graph. Two runs never write to the same folder.
The VM ticket queue
Section titled “The VM ticket queue”Only one acceptance session can use Flare at a time. Enter-VmSlot works like this.
- The run writes a ticket named
<time in ticks>-<process id>.ticketinto.work/vm-queue. Tickets sort by name, so they sort by time. - Every poll (5 seconds),
Get-VmTicketlists the tickets. A ticket whose process is no longer alive is deleted by whoever sees it. - The run whose ticket is first tries to create
slot.lockwith create-new semantics and writes its process id into it. - If creating the lock fails, the run reads the process id in it. A lock whose process has died is deleted and the run tries again. A live lock means wait.
- A run that is not at the front calls the wait callback with its place and the length of the line, but only when its place changes. The callback’s output is discarded, so it can never become part of the slot the function returns.
Exit-VmSlotreleases the lock and deletes the ticket, in afinally, so a failed acceptance still frees the line.
A crashed run therefore never blocks the line. Everything else a run does needs no ticket.
accept.ps1 takes the slot itself when it is run alone. When the factory calls it, the factory already holds the slot and passes -HoldsSlot, so the lock is not taken twice.
File claims and the overlap check
Section titled “File claims and the overlap check”After planning, the run writes .work/claims/issue-N.json holding the issue number, its process id, a start time in ticks and the planned files. The start time is kept if the file already exists, so a rerun keeps its place.
Wait-ForOverlap then looks at every other claim.
- A claim whose process is dead is deleted and ignored.
- A claim that started later than this run is ignored. Only earlier runs can block.
- If an earlier live claim shares any path with this run, the run logs
waiting to start, writes aclaim.waitevent and sleeps 30 seconds before looking again.
Two runs that touch different files never wait for each other. Two that touch the same files go one after the other.
Replay on main
Section titled “Replay on main”A change is built on the main that existed when the run started. Other runs merge in the meantime. Sync-WithMain brings the branch up to date and returns one of four states.
| State | Meaning |
|---|---|
same |
Main has not moved. Nothing to do. |
moved |
Main moved, but nothing it changed meets this change. Nothing was done. Only with -OnlyIfOverlap. |
rebased |
The change was replayed on the new main and pushed. |
conflict |
The replay did not apply cleanly. |
It runs at two points.
- At the front of the VM line, before acceptance. Any movement of main triggers a replay, so Flare always sees the change on top of the latest main.
- After the required checks pass, with
-OnlyIfOverlap. The run asks the code graph what the change can reach, to depth 2, and adds the changed files. If any file main changed meets that set, the change is replayed. If nothing meets it, the run merges without redoing anything.
A replay writes the change as a binary patch, resets the branch hard to the new main, applies the patch with --3way --index, commits with the old message and pushes with --force-with-lease against the commit this run last pushed. After a rebase that needs acceptance, the run goes back through the VM line.
On a conflict the branch is reset to the new main and the failure text goes to the next implement pass. It says that changes merged to main while this one waited conflict with it, shows the conflict and the removed patch, and asks for the same change to be made again on top of main. Conflicts skip the diagnosis agent because the cause is already known.
Cleaning up after a stopped run
Section titled “Cleaning up after a stopped run”A run that stops leaves a pull request and a branch open. When you run the same issue again, the new run
- refuses to start if the claim file belongs to a live process, which keeps two runs from building one issue
- closes every open pull request whose branch starts with
factory/N-, with a comment, and deletes the branch - picks a fresh branch name, adding
-r2,-r3and so on if the remote already has one
Merged pull requests delete their branch as part of the merge. A stopped run removes its own claim, but only if the claim holds its own process id, so a rerun that found another live run never removes that run’s claim.