MGS3032: an unordered share of one claimable file
magus job fork, the client tool (magus\job.put) and a magusfile's magus\job.put refused a job because
one of its write paths and another live job's write path, held in a different
checkout, name the same file, and nothing orders the two jobs against each other:
[MGS3032] job: wave/b declares "run.go", and wave/a (running, updated
4m3s ago) already holds "run.go". Both are live and neither is the
other's parent, child, or depends_on partner, so run.go has one
owner unless the rows say otherwise. wave/a has touched none yet in it so
far. Claim `run.go#<declaration>` on both rows, add `--depends-on
wave/a` to wave/b, or fold wave/b into wave/a
Nothing was written to the job store.
Why
A whole-file write path claims the whole file, and two whole-file leases on
one file both read "mine wins": neither is ever refused, which is what makes
the whole-file spelling look free. It is not free once the file is genuinely
shared between two agents working at once, and the plan has three ways to say
so: a declaration claim, a depends_on edge, or one job instead of two. This
refusal is the fork-time cost that makes naming one of those the cheaper
choice. It fires only when magus can prove the fact: the two write paths name
one literal file (no glob, no directory, no project root), that file has a
diff driver so a declaration claim on it could be graded, and
neither row's parent chain nor depends_on already sequences the pair.
What is refused
- Two whole-file entries on one file, held live in two different
checkouts, with neither job the other's parent, child, or
depends_onpredecessor. - A whole-file entry against a claim on that file, as with
run.gohere andrun.go#executeStagesthere, still collides, since the whole-file entry covers every declaration.
What is not refused
- A glob, a directory, or a project root. Only a single literal file is judged here; the overlap report and MGS3018 cover the rest.
- A file with no diff driver (
.txt,.json,.tomlunless you have registered one). Nothing can grade a declaration of it, so the pair is recorded, not refused;depends_onis the only move. - Two claims on different declarations of one file (
run.go#Aandrun.go#B): that is exactly what a claim is for. - A pair already ordered, meaning a parent and its child, or two jobs joined by
depends_on, directly or through any number of hops, in either direction. - Two rows held in the same checkout, or a row not yet taken into one
(
CheckoutRootempty). That is what the fork's shared-checkout check andwrite_proof: overlappingalready watch; sharing a file there stays an advisory the orchestrator that declared both rows can act on. - A holder blocked on
depends_on. A blocked row owns none of its write paths yet, so it cannot collide today; the fork that unblocks it runs this same check again, against whatever holds the file then. - A holder whose job has exited. Its holder returned, so its editing is
over; the row stays until
job waitgrades it or its work lands on the base, but it holds no file against a new fork.
The claim pair
When magus can compute one, the refusal prints the claims to use instead of
<declaration>. It reads the declarations the holder has touched in the file so
far, and the symbols the new job's goals name that the file defines once each, and
says whether the two are disjoint:
wave/a has touched func lintRules(ctx int) { in it so far. Claim
`run.go#lintRules` on wave/a and `run.go#lint` on wave/b, which
name different declarations (nothing was written to either row), or add
`--depends-on wave/a` to wave/b, or fold wave/b into wave/a
When both rows reach the same declaration, the refusal says no claim can split
them, and the answer is --depends-on or one job. When only one side is known it
prints that side's claim and asks for a declaration other than it on the other
row; when neither is, it keeps the generic advice above. magus prints the pair and
never writes it: the rows are the orchestrator's, so re-fork both with it.
What to do
Claim a declaration on both rows, so the two jobs partition the file instead
of racing on it. Claims are the default; --depends-on is for two jobs on the
same declaration. Deleting is also a valid route: when removing a code path
reaches the goal, the job that owns that declaration deletes it.
magus job fork wave/a --write-paths run.go#lintRules
magus job fork wave/b --write-paths run.go#lint
Or sequence them:
magus job fork wave/b --write-paths run.go --depends-on wave/a
Or fold the work into the job that already holds the file.
In a project's magusfile or magus.yaml the claims are declarations and key paths
(magusfile.buzz#project.targets.test, magus.yaml#jobs.stale_after, see
MGS3036). A refusal over one of them also opens one attention request
asking the holder to release it, with the claims the refused fork wanted; the holder or
the root closes it with magus session dispose. A dry run opens none.