Repository navigation
Conversation
Treehouse counts a pool slot as in use only while a process runs inside
it, so a task whose agent has exited - paused, crashed, or simply between
turns - leaves its own working copy looking free. The next spawn was then
handed that copy and reset it to a detached HEAD, silently, with no
refusal (observed 2026-08-19; nothing was lost only because that task had
already pushed its branch).
fm-spawn now takes the pool's own durable claim itself - `treehouse get
--lease` - instead of relying on the pane's `treehouse get` subshell to
hold the slot, and moves the pane into the leased copy. Treehouse never
hands a leased slot to a later get and never prunes it, with or without a
process inside, so the claim is enforced by the layer that does the
allocating rather than re-derived from task records after the copy has
already been reset.
The lease holder label names the task record that owns it
("fm:<id>@<state-dir>"), which is what keeps a claim from outliving its
task unnoticed: teardown's `treehouse return` releases it in the ordinary
case, an aborted spawn returns it holder-matched before any record can
name it, and anything left over is self-identifying and releasable with
one command. A stale lease costs a pool slot until then; it can never
wedge a spawn, because treehouse allocates another slot rather than
refusing.
The leased path is now screened for isolation before the pane is touched
at all, so a non-isolated copy is refused earlier and the refusal carries
the reason it was rejected.
The pool cannot see a claim on a copy whose only claim was the processes inside it - the shape every task spawned before leases existed leaves behind - so fm-spawn checks this home's own records too and refuses by name rather than launching into another worker's copy. The claim is deliberately kept on that refusal. Releasing it would offer the same copy to the next spawn, which would refuse again and deadlock the pool on it; holding it quarantines that one slot instead, and the refusal prints the exact command that releases it once the contradictory records are reconciled.
The spawn now runs treehouse itself instead of typing `treehouse get` into the pane, so every fixture that drives a crew or scout spawn has to answer the acquire. tests/lib.sh owns that stub; each suite's own no-op treehouse is replaced by it.
worktree_lease= in the task record says which claim releases that copy, and it is what tells the claim-conflict refusal apart from its target: a record carrying its own claim cannot have had its copy offered by the pool, so only an unclaimed record - the shape a task spawned before leases existed leaves - blocks a spawn. A relaunch carries the original claim forward.
…t did A plain top-level cd would leave the task pane's own shell inside the worktree, where teardown's leaked-process reaper ends it as a stray process instead of letting the backend close its pane. On Herdr that skipped the projection's confirmed close and left its presentation journal quarantined. Entering in a subshell keeps the process shape the pool's interactive get produced: the pane's shell stays in the project, only the foreground shell sits in the copy. The presentation fixture arms its post-create abort through a refused claim (still after the pane exists) and reads the abort interleaving from the ordered CLI call log, which records both close mechanisms.
talktejas
force-pushed
the
fm/fm-worktree-pool-reassigns-live-task
branch
from
September 20, 2026 06:18
b0b184d to
72c6165
Compare
This was referenced Sep 20, 2026
talktejas
added a commit
that referenced
this pull request
Sep 20, 2026
…der a live task Supersedes #4, which cannot be revalidated: its branch was rewritten (the pipeline rebased it onto a moved main, and custody was then returned at that rebased head), and no-mistakes refuses to reconcile its private mirror record afterwards. Same content, clean branch from main. On 2026-08-19 a spawn was handed a pooled working copy that another task's own record still named as its live copy. The pool reset it to a detached HEAD, evicting a paused task from the copy holding its work; nothing was lost only because that task had already pushed. Treehouse counts a slot as in use only while a process runs inside it, so a task whose agent has exited - paused, crashed, or between turns - leaves its copy looking free. bin/fm-spawn.sh now takes the pool's own durable claim itself (treehouse get --lease) instead of relying on the pane's subshell to hold the slot, then moves the pane into the leased copy in a subshell so the pane's own shell does not sit inside the worktree. Release paths: ordinary teardown returns the worktree and the lease with it; an aborted spawn returns it holder-matched in cleanup; otherwise the holder label names the owning record and the task records worktree_lease= so a stale claim is self-identifying and releasable with one command. A named refusal stays as a second line of defence for the one copy a claim cannot protect - a task spawned before claims existed, holding its slot with nothing but its processes. The claim is deliberately kept on that refusal, so the slot is quarantined rather than offered to the next spawn in a refuse-loop. Upstream's slot-owner claim marker is kept as-is and this claim sits above it: the lease stops the pool reassigning a live copy, the marker answers at teardown whether the slot beside a record is still that record's own.
talktejas
added a commit
that referenced
this pull request
Sep 20, 2026
* fix(spawn): claim a task's pool worktree so it is never reassigned under a live task Supersedes #4, which cannot be revalidated: its branch was rewritten (the pipeline rebased it onto a moved main, and custody was then returned at that rebased head), and no-mistakes refuses to reconcile its private mirror record afterwards. Same content, clean branch from main. On 2026-08-19 a spawn was handed a pooled working copy that another task's own record still named as its live copy. The pool reset it to a detached HEAD, evicting a paused task from the copy holding its work; nothing was lost only because that task had already pushed. Treehouse counts a slot as in use only while a process runs inside it, so a task whose agent has exited - paused, crashed, or between turns - leaves its copy looking free. bin/fm-spawn.sh now takes the pool's own durable claim itself (treehouse get --lease) instead of relying on the pane's subshell to hold the slot, then moves the pane into the leased copy in a subshell so the pane's own shell does not sit inside the worktree. Release paths: ordinary teardown returns the worktree and the lease with it; an aborted spawn returns it holder-matched in cleanup; otherwise the holder label names the owning record and the task records worktree_lease= so a stale claim is self-identifying and releasable with one command. A named refusal stays as a second line of defence for the one copy a claim cannot protect - a task spawned before claims existed, holding its slot with nothing but its processes. The claim is deliberately kept on that refusal, so the slot is quarantined rather than offered to the next spawn in a refuse-loop. Upstream's slot-owner claim marker is kept as-is and this claim sits above it: the lease stops the pool reassigning a live copy, the marker answers at teardown whether the slot beside a record is still that record's own. * no-mistakes(review): guard abort lease release on record; refresh claim rationale * no-mistakes(review): gate and verify holder-matched lease release * no-mistakes(review): bound pool allocation and name orphaned claims * no-mistakes(review): assert lockdir release; scope recovery commands to project * no-mistakes(document): document pool-allocation bound and widened treehouse lease gate * no-mistakes(review): install flag-modelling treehouse lease stub in stale suites * no-mistakes(review): pin CI treehouse to v2.1.0 for holder-matched release * no-mistakes(test): teach dispatch-profile timeout stub the GNU -k shape * no-mistakes(document): document worktree_lease meta field and its release command
Owner
Author
|
Superseded by #10, which is merged. That branch could not be reconciled after its run was aborted, so the work was redone on a fresh branch. |
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.
The fault
On 2026-08-19 a spawn was handed
/home/tds/.treehouse/jt2627s-445362/3/jt2627s, which another task's own record still named as its live working copy. The pool reset that directory to a detached HEAD, evicting a paused task from the copy holding its work. Nothing was lost only because that task had already pushed its branch; had the copy held anything uncommitted, the spawn would have destroyed it silently.The cause is what the pool can see. Treehouse counts a slot as in use only while a process is running inside it, so a task whose agent has exited - paused, crashed, or simply between turns - leaves its own working copy looking free, and the next
treehouse gettakes it.The fix
Of the two options in the report - refuse a claimed copy, or stop the pool offering it - this takes the second, because it is the only one that acts before the copy has already been reset.
bin/fm-spawn.shnow takes the pool's own durable claim itself (treehouse get --lease) instead of relying on the pane'streehouse getsubshell to hold the slot, then moves the pane into the leased copy. Treehouse never hands a leased slot to a later get and never prunes it, with or without a process inside.How the claim is released, since a claim nothing can release is the trap this fix could otherwise create:
bin/fm-teardown.shreturns the worktree, andtreehouse returnreleases the lease with it.fm:<task-id>@<state-dir>), and the task record carries the same value asworktree_lease=, so a lease outliving its task is self-identifying and releasable with one command. A stale lease costs one pool slot until then; it can never wedge a spawn, because treehouse allocates another slot rather than refusing.The named refusal the report asked for is kept as a second line of defence, for the one copy a lease cannot protect: a task spawned before leases existed, holding its slot with nothing but its processes. If the pool offers a copy that another record in this home still names and that record carries no claim of its own, the spawn refuses and names the task that holds it. The claim is deliberately kept on that refusal - releasing it would offer the same copy to the next spawn, which would refuse again and deadlock the pool on it; holding it quarantines that one slot, the next spawn is given a different copy, and the refusal prints the command that releases it.
Two consequences worth calling out:
cd. A top-levelcdputs the pane's own shell inside the worktree, where teardown's leaked-process reaper ends it as a stray process instead of letting the backend close its pane - on Herdr that skipped the projection's confirmed close and left its presentation journal quarantined. The subshell keeps the process shape the pool's interactivegetproduced. This was caught by the existing Herdr presentation E2E, not by inspection.Delivery
This ships direct-PR rather than through the full review pipeline, deliberately: the pipeline's cached copy of this repository points at the project firstmate was forked from rather than at ours, so it rebases any change onto months of unrelated history and fails. Fixing that needs the captain's permission to update a tool, which has not been given yet. Rather than wait, this goes direct-PR with the reason stated.
Verified locally
New:
tests/fm-spawn-worktree-lease.test.sh- the claim is taken and labelled by the owning record, an aborted spawn returns it holder-matched, a copy another record claims is refused by name and stays claimed, a record holding its own claim does not block a spawn, and - against the real pool, skipped when treehouse is absent - a leased slot with no process inside it is not handed to a laterget. Dated evidence for that last guarantee is recorded indocs/verification/runtime-backends.md.39 of the 58 suites that exercise
fm-spawnwere run locally; 38 reported, 36 passed, 2 failed. The remaining 19 were stopped rather than queued, and CI covers them. Neither failure is from this change:tests/fm-secondmate-liveness.test.sh- "Herdr pane state unknown should map to unreadable, got 'missing'". Fails identically on the untouched base commit 11217bc (exported to a clean tree and run there);bin/backends/herdr.shis not touched by this branch.tests/fm-secondmate-harness.test.sh- "first config push did not reach pointer delivery", a 2-second polling window in afm-config-push.shconcurrency case. It passed on its own run earlier on this same branch and failed only under a loaded parallel sweep;bin/fm-config-push.shis not touched.Two others seen during the sweep were also confirmed against the untouched base:
tests/fm-backend-tmux-smoke.test.sh("the tmux task shell did not become ready", raw tmux, before any firstmate code runs) andtests/fm-remote-secondmate-lifecycle-e2e.test.sh("remote spawn never reached its blocked inheritance write"). Both fail the same way on 11217bc here.bin/fm-lint.shandbin/fm-doc-audience-check.share clean.