Cloud runs' work is adopted onto the run's record (fix #1601) - #1603
Cloud runs' work is adopted onto the run's record (fix #1601)#1603suleimansh wants to merge 2 commits into
Conversation
|
🤖 agent comment SPEC walkthrough. Per modified SPEC file: where it sits, the diff, and why. One sentence covers all of it: the run's record now learns the branch its cloud session actually worked on, by exact ancestry from a commit unique to the hand-off — and everything that only existed to cope with not knowing gets simpler. 1. The test SPEC lists what the tests prove: exact/zero/ambiguous matching, the armed draft PR and its nothing-beyond-the-hand-off guard, unarmed runs, the window, no-remote never throwing, and the service's announce-only-changes rule. 2. -- Before the hand-off, HEAD is pushed to origin under the agent's own id: the CLI's default revision pin is the current local branch — which an agent workspace's local-only branch fails — and a slash-carrying ref never resolves on the cloud side even when pushed (anthropics/claude-code#87235), so the ref is minted slash-free and handed over explicitly. A push that fails degrades to the old behavior and says so, naming `--teleport` as the recovery path.
+- Before the hand-off, the anchor is pushed to origin under the agent's own id: an empty commit on top of HEAD, unique to this run and minted without moving any branch. The session clones at it, so the branch it does its work on — a name of the cloud's own choosing — descends from it, and that ancestry is how the daemon later recognizes which branch is this run's. The ref is explicit and slash-free because the CLI's default revision pin is the current local branch — which an agent workspace's local-only branch fails — and a slash-carrying ref never resolves on the cloud side even when pushed (anthropics/claude-code#87235). A push that fails degrades to the old behavior and says so, naming `--teleport` as the recovery path; a repo where the anchor cannot be minted hands off plain HEAD, and the run is simply never matched to its branch.This is the mechanism the whole PR rests on: the pushed commit is unique to the run, so the session's branch is recognizable later by plain ancestry — no name heuristics, no time windows deciding matches. The two degradations (push failed, anchor unmintable) keep exactly the old behavior. 3. 4. - An hourly sweep deletes the dead refs Claude-web hand-offs leave on origin, once they are old enough and provably hold no work.
+- Settled web runs are matched to the `claude/*` branch that grew out of their hand-off, and the branch and its PR are adopted onto the run's record — with the armed draft PR opened when the session never opened one.New background job, so the daemon's service inventory has to say so. 5. +- A web run's checkout is the one carve-out from the push: the hand-off already pushed everything the cloud session clones at, and the work lands on the session's own remote branch — so pushing the empty local run branch would only put a dead ref on origin per web run. It goes without a push once it provably holds nothing (a clean tree whose tip is inside what the hand-off pushed); any doubt falls back to the ordinary rule.This is what stops the debris at the source: the one-empty- 6. 7. -- A web run pushes a `cloud-*` ref for the cloud session to clone at, and its run branch reaches origin when the worktree is reclaimed. The session then works on its own branch and opens its PR from there, so nothing ever consumes either ref again.
+- A web run pushes a `cloud-*` ref for the cloud session to clone at; the session then works on its own branch and opens its PR from there, so nothing ever consumes the ref again. Run branches used to reach origin too, pushed empty when a web run's worktree was reclaimed — teardown no longer pushes them, and the sweep clears the ones already there.+- The hand-off anchor is the one tip the default branch never absorbs — an empty commit no merge ever lands — so it clears the work gate its own way: a tip that changes nothing against its parent, on a parent that landed, holds no work.First: the run-branch half of the sweep's diet dries up (teardown stops pushing), but the sweep keeps clearing what is already on origins. Second is a consequence the anchor forces: the sweep's work gate was "tip reachable from the default branch", and an anchor tip never is (a squash merge rewrites the session's history without it) — without this rule every 8. 9. +- A branch that exists only on the remote — a cloud session's own, pushed from a VM this machine never sees — can still get its draft PR opened: there is nothing to push first, the PR request itself is the whole action.The existing PR opener pushes first, which fails for a branch with no local copy; the adoption pass needs this push-free variant to open the armed draft. 10. 11. -- An archived snapshot can be patched afterwards with a fact discovered once the agent's process is gone, such as the pull request opened for its work.
+- An archived snapshot can be patched afterwards with a fact discovered once the agent's process is gone, such as the pull request opened for its work or the branch a cloud session's work landed on.Same patch-in-place mechanism, second fact. 12. 13. | 124 | Web runs trust the project for Claude Code automatically — no manual trust step |
+| 125 | A cloud run's row follows the session's real branch and PR, with its armed draft PR opened when the session opens none |User-facing behavior: the row that said "Nothing committed" about finished cloud work now shows the branch and the PR. |
A web run's cloud session works on a claude/* branch of its own naming, never the designated run branch, so every surface keyed to the recorded branch said "nothing committed" while the work sat on origin — and the armed draft PR never opened. The hand-off now pushes an anchor (an empty commit unique to the run, minted without moving any branch) as the ref the session clones at, so the session's branch is recognizable later by plain ancestry. A daemon pass matches each settled web run to the one claude/* head descending from its anchor and patches the run's archive with the branch and its PR — opening the armed draft PR itself when the session never did. Web-run teardown stops pushing the empty tf-agent-* branch to origin; the scratch-ref sweep clears the ones already there and learns that an anchor tip (an empty commit on a landed parent) holds no work.
cceef13 to
2dcb2a9
Compare
…ile the next sync wipes Seen live: the adoption pass patched the run's archived record straight into the tf-data checkout; a minute later the data sync's rebase refused the dirty tree and the funnel hard-reset it, so the run flipped back to its birth branch every pass. One `patchArchivedAgent` replaces `recordAgentPr` + `adoptAgentBranch`, and `patchArchivedAgentOnDataBranch` is the funneled form (sync, patch, commit, push) the pass and the Open PR button now use. The pass also treats a failed `gh pr list` as "could not tell" rather than "none" (it opened a second draft PR otherwise), leaves alone a run whose record names some other branch, and writes one commit per adoption. The driver mints and pushes the anchor in one step — the anchor-or-HEAD fallback was a second hand-off path that could not occur. SPECs that still described the pre-#1601 pair of dead refs, and the test SPECs missing the new cases, are brought current. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
🤖 agent comment Dogfooded live twice today, plus a code review. The mechanism held; one real bug came out of it and is fixed in 084b140. Evidence first, then the SPEC changes of this round (intro + TLDR, diff, meaning — per file). What the two live runs showedRun 1 (
Run 2 ( What changed in 084b140
SPEC changes in this round1. The test SPEC: a patch lands as a pushed commit with the checkout clean and survives the next sync; a run with no archive is reported as not patched and commits nothing. 2. -- What gets recorded: the branch, and the pull request the session opened for it. A run that was armed for a PR the session never opened gets its draft PR opened by the daemon — the armed handoff finally resolving against the facts — unless the branch carries nothing beyond the hand-off itself.
+- What gets recorded, as one commit on the data branch so every machine learns it: the branch, and the pull request the session opened for it. A run that was armed for a PR the session never opened gets its draft PR opened by the daemon — the armed handoff finally resolving against the facts — unless the branch carries nothing beyond the hand-off itself, or the session's pull requests could not be listed that pass: not knowing is never read as none, since the cost would be a second pull request.Meaning: the third line is where the record is written — the data branch (file 1) — and gains the second case in which the armed PR is not opened this pass: the listing failed. The "exact, never guessed" rule of the line above now also covers the PR side. 3. -Deletes the two dead refs every Claude-web hand-off leaves on origin — the pre-hand-off `cloud-*` ref and the run branch — once it is provably safe, so they stop accumulating one pair per web run.
+Deletes the dead refs web runs leave on origin — the `cloud-*` ref a hand-off pushes for the session to clone at, and any run branch that holds no work — once it is provably safe, so they stop accumulating.-- ... Run branches used to reach origin too, pushed empty when a web run's worktree was reclaimed — teardown no longer pushes them, and the sweep clears the ones already there.
+- ... A web run's own branch never reaches origin — its checkout is reclaimed without a push once the cloud session has what it needs — while a local run's branch does, and is swept only once its work has landed.Meaning: the sweep's two candidate classes are the 4. -... A push that fails degrades to the old behavior and says so, naming `--teleport` as the recovery path; a repo where the anchor cannot be minted hands off plain HEAD, and the run is simply never matched to its branch.
+... A hand-off whose anchor cannot be minted or pushed goes ahead with no ref and says so, naming `--teleport` as the recovery path; such a run is simply never matched to its branch.Meaning: one failure mode instead of two — mint-and-push is one step, and its failure is the existing "no ref, notice, 5. Test SPECs — |
|
@brillout ready for your read. Dogfooded live twice today: run 1 caught a real bug (the adoption was written into the tf-data checkout and wiped by the next sync — |
|
Saving the |
🤖 agent PR
Fixes #1601, along the direction proposed in the issue comment.
The problem
A web run's cloud session works on a
claude/*branch of the cloud's own naming, never the designatedtf-agent-*branch. Nothing ever told the run's record, so every surface keyed to the recorded branch — the session row, PR resolution, CI watch, merge — said "Nothing committed" while the work sat on origin; the armed draft PR never opened; and teardown pushed one emptytf-agent-*ref to origin per web run just to satisfy the retention rule.The fix, in three moves
The hand-off anchor (
driver/cloud.ts). The pre-hand-off push no longer pushes plain HEAD: it pushes an anchor — an empty commit on top of HEAD, unique to the run, minted withcommit-treeso no local branch moves. The session clones at it, so the branch it works on descends from it — and that ancestry is an exact match, not a guess. The anchor rides the driver's result into a newcloud-anchorevent and onto the meta (AgentMeta.cloudAnchor). A repo where the anchor can't be minted hands off plain HEAD, exactly as before.The adoption pass (
cloud-work.ts, new; wired as a daemon background job every ~10 min). For each settled web run still owed an answer (within a 48h window), onels-remotelists origin'sclaude/*heads; the head descending from the run's anchor is adopted: the branch is patched onto the run's archive (the same patch-in-placerecordAgentPruses), the PR the session opened is recorded — and when the run was armed for a PR the session never opened, the daemon opens the draft PR itself via the new push-freeopenRemoteBranchPullRequest(the branch exists only on the remote;gh pr create --headis the whole action). Zero or two matching heads → retried next pass; conservative everywhere.Teardown stops the debris (
worktrees.ts). A web run's checkout goes without pushing its empty run branch, once it provably holds nothing: a clean tree whose tip is inside what the hand-off pushed (an ancestor of the recorded anchor). Any doubt falls back to the ordinary commit-and-push rule. The scratch-ref sweep (cloud-scratch-refs.ts) learns the anchor's one quirk: its tip is never reachable from main (no merge ever lands an empty commit), so a tip that changes nothing against a landed parent now clears the work gate — otherwisecloud-*refs would be kept forever.Everything downstream lights up through the existing single lookup (
agentBranchForprefers the recorded branch;AgentMeta.prfeedsresolveAgentPr): no per-surface changes were needed.Not in this PR
Verified
SPEC walkthrough follows as a comment.