Feat/messages - #36
Merged
Merged
Conversation
The overlap surface could grade a collision and show both sides, but every exit from it was lossy: promote one worker and the other is left building on a base that moved, or send it back to redo work it had already finished. Both workers branched from one base, which worker-overlap.ts already relies on to compare pre-image ranges. Two change sets over a shared base are a three-way merge, and git computes it exactly — so for every line the two did not both touch there is a machine answer, and only the lines they did touch need a human. Each side's post-image is built in a scratch index (read-tree, apply --cached, write-tree), committed as a child of the shared base, and merged with merge-tree --write-tree under diff3. No worktree, no checkout, no working-copy write; every object produced is unreferenced. The base is chosen by proof rather than by ancestry: each candidate head is accepted only if both patches apply to it, because a patch that applies is proof the tree is a correct pre-image for it. merge-tree labels conflicts with the object id it was handed, which tells a human nothing. The roles are fixed by the marker glyph, so relabelling is positional and pure, and every label carries a worker and its task. Outcomes are tri-state, as everything else here is: clean, conflicted with the regions counted, or a stated reason. "Could not combine" is never allowed to read as "they are apart".
A promotion has always had one author, so a worker reading the journal could only ever learn that the ground moved under it. That is the right message when someone else's change landed on top of yours. It is the wrong message when your own work is inside what landed, and reconciliation makes that case real. A promoted entry now carries workerIds. Staleness skips every contributor, not just the one worker the entry came from, so nobody derives base moved from edits that are already in — and the entry says so in words, because "your base moved" reads as "start again" and this is the opposite of that. workerId is still written alongside it, so an entry appended today reads correctly to a Docket that predates this. The peer's deliverable is closed out with a new terminal verb, reconcile: its row settles and its generation is safe to prune, and isDeliverableApproved stays false. Approval has to keep meaning that a human judged one deliverable on its own, which nobody did here. Guardrails gain the matching sentence, so a worker credited by a promotion does not read the standard stale-base paragraph as a reason to stop and ask.
The overlap view's question was "who yields", and the promote confirmation's was "promote anyway". Both accept as given that one worker's work is about to be thrown away. Combine replaces that premise: the two change sets are merged over the base they share, and what the human is asked for is the residue. The row states its own outcome before anything is committed to — "merges clean, 2 files" or "2 conflicts to resolve in 1 file" — so neither is discovered halfway through an editor session. Most overlaps turn out to have no decision in them at all, and stillApplies was already telling us that. Contested files open in the human's own editor with the relabelled markers. Native form: git's conflict markers are a vocabulary every developer has, and a bespoke merge pane would be a new visual language for a job the editor already does. The set is transactional, and a file handed back with a marker still in it is refused by name — the gate re-reads what is actually about to be applied, not what the human was handed. Reconciliation lives on the promote path rather than beside it. A second door that also applied patches would be two notions of what promoting means, and this codebase already paid once for having two implementations of one idea. The overlap view reaches that path; it does not reimplement it. So a reconciled change set passes apply --check, the parent-moved confirmation, and the journal unchanged. The third exit hands a worker both diffs, the merge residue, the directive, and the provenance that the other change is under review and not in the project. The escape hatch for a conflict that is semantic rather than textual, and deliberately not the default: it costs a worker turn and returns an unreviewed deliverable, where a human edit is seconds and is reviewed by construction. A merge that could not be opened says which peer and why. Showing the older exits without a word would let the human conclude combining was never possible for this collision.
…demo P5 recorded that the overlap view is "a reading surface, not a merge tool", and the reason given was mechanical: two sections for one path would be applied twice by git. That is an argument about the composed patch, which really is unapplicable, and it does not generalise into "Docket must never produce a merged file" — a three-way merge is a different construction with none of that problem. What has to hold is about authority, not mechanism: Docket computes the mechanical part of a reconciliation and hands the human the residue, and never decides one. A merge is an observation about which lines both sides changed, the same kind of fact as apply --check, which P5 already accepted. P6 written with that restatement, its scope, its non-goals, twelve passing criteria, and what was decided along the way. CONTEXT gains Reconcile and Conflict residue as vocabulary and three invariants. Architecture, the full reference, and the README follow. The README demo becomes two takes over one fixture. Take A is unchanged except where the promote confirmation's wording moved. Take B is the new lane, and it wants both workers ready rather than one running, because a reconciliation needs both sides frozen.
The take said "delete the markers, keep w3's signature and w2's body" and left the recorder to invent the resolution on camera, against markers that were written from memory rather than from a run. Both are now the fixture's real bytes: the base setup.sh writes, what each worker produced, the conflict merge-tree actually leaves, and the resolved file to have on the clipboard before recording. That last file is worth reading on its own — w3's signature, w2's table, and w2's lookup rewritten against w3's variable name. Neither worker wrote it and neither could have, which is the claim the beat exists to make. Also corrects two things the take was wrong about before reconciliation existed. Take A's w2 must publish and *then* be woken, not merely be left running: without a frozen change set there is nothing to grade its side against, so the card would read `same file` and `o` would have one set of hunks rather than two. And task labels are the first six words of the spawn task, so w2's ends on "to" and w3's is clipped — the take now quotes them as they render instead of as they read well.
docs/readme-demo.md is production notes for the README clips — spawn order, which worker to leave running, what to have on the clipboard before recording. It is written for whoever is holding the screen recorder, not for anyone installing the package, and it belongs beside the other demo guides that are already local-only. Untracked and gitignored, same as manual-demo.md and manual-demo-messaging.md. The file stays on disk; nothing about recording a clip changes. Two things came out of doing it: npm pack was shipping the demo guides regardless of .gitignore, because the `files` field in package.json wins over it. Every published version so far has carried 36kB of manual-demo-messaging.md, which is a recording script for a fixture that does not exist in the tarball. `files` now negates all three guides explicitly. The real docs are unaffected: 87 files, adr/, architecture, configuration, full-reference, releases. README linked to docs/manual-demo.md under More docs, which has been ignored and untracked for some time — so that link was already dead in the published package. Removed rather than repointed: the manual guides are local tooling and the README's list is for people reading the shipped docs.
CI has been failing with eight cancelled tests and zero failures, all in messaging-surface, all reporting "Promise resolution is still pending but the event loop has already resolved". It reproduces on Node 22 and not on 24 or 26, which is why it never showed locally. The cause is in the seam, not the test. collectBroadcastSuggestions races each advisor against a timeout, and that timer was unref'd. The timer is the entire isolation guarantee — an advisor that never resolves is precisely what it exists to survive — so unref'ing it means that in the one case it is there for, it is the only handle left, the loop drains, and the await never settles. Inside pi this never surfaced because a live session always has other handles keeping the loop alive. Under `node --test` the file is the only thing running, so the process exits and every test after it in the file dies as cancelledByParent. The timer is now ref'd and always cleared, so it holds the loop for at most the window and a well-behaved advisor costs nothing. The hung promise stays pending forever, because a promise cannot be cancelled, but nothing awaits it and it holds nothing open. The existing test could not have caught this: it registered a fast advisor alongside the hung one, so the race always had something else to settle on. Added the case where every advisor hangs, which is the only configuration where the timeout is what resolves it. Checked the other unref'd timers while here — the dock watcher, the peek and pulse timers, Hunk's comment harvester. All of them fire callbacks or poll beside something else that keeps the process alive, and none gates a promise anyone awaits. Unref is right for those.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Reconcile: settle a collision without either worker redoing anything
Two workers touch the same lines. Today the overlap view asks who yields, and
every answer to that question throws work away. (forced to promote one and the other is
building on a base that moved, or send it back to redo something it already
finished). The premise was wrong. Both workers branched from one base, which
worker-overlap.tsalready relies on to compare pre-image ranges. Two changesets over a shared base are a three-way merge, and git computes it exactly. So
the question a collision actually poses is not which worker wins, it's what the
file should be.
Combine is that merge. The picker row states its own outcome first:
Most overlaps come back
merges clean. there was never a decision in them, andstillApplieshad been quietly saying so for a while. Where the edits genuinelymeet, the contested file opens in your own editor with git's markers relabelled
by worker and task, and the base between them. You write the resolved file.
One that comes back with a marker still in it is refused by name. Then both workers' work lands in one promotion, and neither is told its base moved the journal entry credits both and says neither needs redoing.
On the principle this revises
P5 recorded that the overlap view is "a reading surface, not a merge tool". The
reason given was mechanical: two sections for one path would be applied twice by
git. That's an argument about the composed patch, which really is
unapplicable. It doesn't generalise to "Docket must never produce a merged
file", and a a three-way merge is a different construction with none of that problem.
What has to hold is about authority: Docket computes the mechanical part of a
reconciliation and hands the human the residue. It never decides one. A merge
is an observation about which lines both sides changed, same kind of fact as
git apply --check, which P5 already accepted. Auto-merging and promoting wouldbreak the rule. Showing three versions and letting the human write the fourth
doesn't.
How it works
Each side's post-image is built in a scratch index —
read-tree,apply --cached,write-treecommitted as a child of the shared base, thenmerge-tree --write-treeunderdiff3. No worktree, no checkout, noworking-copy write; every object produced is unreferenced. The base is picked by
proof rather than ancestry: a candidate head is accepted only if both patches
apply to it.
Reconciliation lives on the promote path rather than beside it. A second door
that also applied patches would be two notions of what promoting means, and this
codebase already paid once for having two implementations of one idea. So a
reconciled change set passes
apply --check, the parent-moved confirmation, andthe journal unchanged.
The peer's deliverable closes with a new terminal verb,
reconcile. Its rowsettles;
isDeliverableApprovedstays false, because approval has to keepmeaning a human judged one deliverable on its own.
Third exit hands a worker both diffs, the residue, the directive, and the
provenance that the other change is under review and not in the project. Escape
hatch for a conflict that's semantic rather than textual.