Skip to content

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

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.

Open the diagram full size

Only one acceptance session can use Flare at a time. Enter-VmSlot works like this.

  1. The run writes a ticket named <time in ticks>-<process id>.ticket into .work/vm-queue. Tickets sort by name, so they sort by time.
  2. Every poll (5 seconds), Get-VmTicket lists the tickets. A ticket whose process is no longer alive is deleted by whoever sees it.
  3. The run whose ticket is first tries to create slot.lock with create-new semantics and writes its process id into it.
  4. 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.
  5. 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.
  6. Exit-VmSlot releases the lock and deletes the ticket, in a finally, 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.

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 a claim.wait event 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.

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.

  1. 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.
  2. 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.

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, -r3 and 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.