Skip to content

feat: dl <ws> recreate starts the agents it ends again - #675

Open
blooop wants to merge 14 commits into
feat/stale-image-lsfrom
feat/recreate-keeps-agent
Open

blooop wants to merge 14 commits into
feat/stale-image-lsfrom
feat/recreate-keeps-agent

Conversation

@blooop

@blooop blooop commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner

Part of #673 — A workspace keeps its old image after a pull, and a recreate ends its agent. This PR is part 2 of 3: dl <ws> recreate starts the agents that it ends again, in the panes that they held.

What this changes

  • A new module, flows/agent_sessions.rs, has two steps:
    • collect asks herdr for every pane. It keeps a pane when the pane's foreground is a dl session into this workspace, and herdr's session.json holds a saved line for the pane that starts an agent. The word after dl in the saved line must be this workspace id. herdr agent get removes a pane whose agent is done or absent.
    • restart types each saved line, shell-quoted, into its pane with herdr pane run. herdr refuses a pane until the old session has exited, so restart tries again every 500 ms, up to 20 times for each pane. It never types into the pane that dl itself runs in. It returns that pane's line for dl to print.
  • run_rebuild in flows/launch.rs uses these steps for LaunchVerb::Recreate on a running workspace only. reset and code do not change.
  • Seven new LaunchNotice variants carry each outcome. Their words are in rust/dl/src/render.rs. No outcome refuses the recreate.
  • The herdr agent get parser is now in core (herdr::agent_reading_in, AgentStatus, PaneAgent), so part 3 can read whether an agent is idle.
  • docs/cli.md has a new section, "A recreate starts the agents it ends again". CHANGELOG.md has an entry.

Where this fits

 run_rebuild(verb = Recreate)
+  sessions = agent_sessions::collect(ws)       # herdr pane list, process-info, session.json, agent get
   bring_up(--recreate)                         # every process in the container ends
+  if the up succeeded:
+    agent_sessions::restart(sessions)          # herdr pane run <pane> <saved line>, with retries
+  else:
+    print each saved line, to run by hand
   attach

A saved line looks like dl <ws> -- ... claude ... --remote-control=<ws> --resume <uuid>. The container's SessionStart hook reports a new id after /clear, so the line holds the session that the pane is in now. The transcripts are in ~/.claude, which outlives the container, so --resume finds the conversation.

How I checked it

All tests run on FakeRunner. This devcontainer has no herdr and no docker daemon.

  • Before: a recreate on a workspace with an agent pane typed nothing. After: the launch tests show collect before the up, restart after it, and then the attach.
  • The review fixes each have a test that failed first:
    • A pane that once ran dl other -- claude no longer gets that line on a recreate of myws.
    • A herdr answer that this build cannot read keeps the saved line.
    • A recreate whose devpod up never started tells the user nothing about the agents, because they still run.
    • A pane that runs codex and has no saved line is not told to run claude --resume.
  • Other tests cover: a busy pane that takes its line on a later try, quoting of spaces and quotes, a pane that herdr will not describe, a missing session.json, a stopped workspace, and pane list failing.

Commands that I ran in the devcontainer:

  • cargo test --workspace: 3044 passed.
  • cargo clippy --locked --all-targets -- -D warnings and cargo fmt --check: clean.
  • pytest test/ against the debug build: 846 passed.
  • prek run --from-ref origin/main --to-ref HEAD: passed.

I did not run:

  • A real recreate with herdr. These facts are not tested against a live herdr: that herdr pane run takes the line as one string and presses Enter, the shape of agent get for a pane with no agent, and that a pane falls back to its shell when the dl session dies.
  • scripts/public-api-snapshots.sh. The tools are not in the container. I copied the diff from the public-api CI job into the snapshot files.

Merge danger

Door: two-way. A revert gives back the old recreate. The promised API (public-api.api.txt) gains seven LaunchNotice variants. LaunchNotice is not #[non_exhaustive], so a caller with an exhaustive match needs an arm for each, as for each notice added before.

Blast radius: medium. If collect picks the wrong pane, dl types a dl <ws> -- claude ... line into a shell prompt where it does not belong. The workspace id check limits this to panes whose saved line names this workspace. If a pane stays busy, the attach waits up to about 10 s for that pane, and up to about 50 s if herdr hangs. The panes run one after another.

Review notes

  • Outside herdr, a recreate starts no agent again. It prints that the recreate ends the agents and that claude --resume picks a conversation up again. Reading the Claude processes in the container would cover this case, but the session id in a process's arguments is wrong after /clear. Part 3 skips such workspaces, so dl --refresh-stale never ends an agent that herdr cannot see.
  • When devpod disappears after an up (DevpodNotRun(NotInstalled)), dl cannot tell this from a recreate that removed the container. It then prints the lines to run by hand although the agents may still run. A fix needs a new error variant in workspace_up.
  • Saved lines for codex and gemini panes are typed back too. The issue names only Claude.

Stack

  1. #674 — feat: dl --ls says which workspaces run an older image
  2. #675 — feat: dl recreate starts the agents it ends again ← this PR
  3. #676 — feat: dl --refresh-stale recreates the stale workspaces that are idle

🤖 Generated with Claude Code

blooop added 12 commits October 7, 2026 14:50
dl <ws> recreate ends every agent in the container. herdr already saves,
per pane, the line that starts the pane's agent again. agent_sessions::collect
reads it for each pane attached to the workspace, and herdr agent get decides
whether the agent in that pane is still live.

Claude-Session: https://claude.ai/code/session_01Ro7QyyAtHmrh9oxyyK6h9m
agent_sessions::restart runs herdr pane run with the saved line, and asks
again for up to ten seconds while the old transport exits. The pane this dl
runs in is handed its line instead of typed into.

Claude-Session: https://claude.ai/code/session_01Ro7QyyAtHmrh9oxyyK6h9m
Before the up, the recreate reads which herdr panes hold an agent in the
workspace. After the up, it types each saved line back into its pane, and
then attaches. Outside herdr, or when herdr does not answer, or when the up
fails, it says so, and hands over any line it did not type. None of this
refuses the recreate.

Claude-Session: https://claude.ai/code/session_01Ro7QyyAtHmrh9oxyyK6h9m
A pane attached to the workspace being recreated can still hold the
resume line an earlier `dl other -- claude` saved in it, because a plain
attach saves nothing. collect took that line as the pane's session, so
the restart started an agent in the other workspace and the notice said
the agent here was kept.

A saved line now counts only when its workspace word is the id of the
workspace being recreated, which is the word dl writes there from the
launch and the container hook. A live agent with a line for another
workspace is reported as unresumable.

Claude-Session: https://claude.ai/code/session_01Ro7QyyAtHmrh9oxyyK6h9m
…uild could not read

herdr's agent get answer with /result/agent present but no string name
read as NoAgent, so collect dropped the pane's saved line. Such an answer
now reads as Unanswered, which keeps the line; NoAgent stays for an agent
that is absent or null.

Claude-Session: https://claude.ai/code/session_01Ro7QyyAtHmrh9oxyyK6h9m
…ive agent again

When the OS refused to start `devpod up` for a recreate, the old container
and its agents were untouched, yet every held pane got a line to run
`claude --resume` by hand: a second claude on a session that still ran.
A spawn refusal (`NotRun::Blocked`) now says nothing about the held agents.
`NotInstalled` and a devpod refusal keep the hand-back, since both can
follow a container the recreate already removed.

Claude-Session: https://claude.ai/code/session_01Ro7QyyAtHmrh9oxyyK6h9m
…panes

The CHANGELOG said a recreate outside herdr prints the line to run by
hand; there is no line there, and dl says to run 'claude --resume'.
docs/cli.md said a pane counts unless herdr reports its agent done; collect
also drops a pane where herdr reports no agent. The module doc made the
same no-line promise, and a render.rs comment gave the agent notices
severities that nothing maps.

Docs and comments only: no red test is possible beyond the existing prose guards.

Claude-Session: https://claude.ai/code/session_01Ro7QyyAtHmrh9oxyyK6h9m
…create's agent restart

Pins a pane run refused twice then taken (two ticks of waiting), a saved
word with a space or a quote typed back as the same word, a pane herdr
will not describe skipped without losing the rest, a live agent with no
session file reported unresumable, a stopped workspace's recreate asking
herdr nothing, a pane list that fails saying AgentSessionsUnanswered, and
the four agent-session notices that had no word test.

Claude-Session: https://claude.ai/code/session_01Ro7QyyAtHmrh9oxyyK6h9m
…ane gained its agent

e0fca22 wrote a word-for-word test of the AgentSessionUnresumable line and a
collect test of a live agent with no session file, both against the shape
before b47d65d gave the notice an `agent` field and made
`HeldSessions.unresumable` a list of `UnresumablePane`. Cherry-picked together,
neither test compiled.

Name the agent in both. The claude sentence keeps its exact words, and the word
test now also pins the sentence a codex pane gets, so the test still says every
unresumable line word for word.

Claude-Session: https://claude.ai/code/session_01Ro7QyyAtHmrh9oxyyK6h9m
@sourcery-ai

sourcery-ai Bot commented Oct 7, 2026

Copy link
Copy Markdown

Sorry @blooop, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 2 hours and 41 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

@sourcery-ai

sourcery-ai Bot commented Oct 7, 2026

Copy link
Copy Markdown

Reviewer's Guide

Running-workspace recreate now snapshots live herdr agent sessions before replacing the container and restores their saved dl commands in the original panes afterward, using bounded retries and shell-safe quoting; all discovery and restart failures become notices or manual recovery instructions rather than blocking the recreate. Core herdr parsing is generalized for this flow and future agent-state consumers, with comprehensive tests and CLI documentation.

Sequence diagram for recreating a workspace and restoring agent sessions

sequenceDiagram
    participant User
    participant Launch
    participant Herdr
    participant Devpod
    participant Pane

    User->>Launch: run_rebuild(Recreate)
    Launch->>Herdr: pane list
    Herdr-->>Launch: panes
    loop each pane
        Launch->>Herdr: process-info(pane)
        Herdr-->>Launch: foreground process
        Launch->>Herdr: agent get(pane)
        Herdr-->>Launch: agent status
        Launch->>Launch: read saved_argv(session.json)
    end
    Launch->>Devpod: bring_up(--recreate)
    Devpod-->>Launch: up result
    alt up succeeded
        loop each held session
            Launch->>Herdr: pane run(pane, shell-quoted line)
            Herdr-->>Launch: accepted or pane busy
            opt pane busy
                Launch->>Herdr: pane run retry after 500 ms
            end
        end
    else up failed after starting
        Launch-->>User: print saved lines for manual recovery
    end
    Launch->>User: attach
Loading

File-Level Changes

Change Details Files
Adds agent-session discovery and restart orchestration around running-workspace recreates.
  • Collects matching herdr panes by workspace, foreground process, saved session command, and agent status.
  • Restarts saved commands after a successful recreate, with shell quoting, bounded retries, and protection for dl’s own pane.
  • Reports unresumable, skipped, failed, and manually actionable outcomes without blocking the recreate.
rust/devlaunch-core/src/flows/agent_sessions.rs
rust/devlaunch-core/src/flows/launch.rs
rust/devlaunch-core/src/flows/mod.rs
rust/devlaunch-core/src/flows/session_manager.rs
Moves herdr agent-status parsing and command construction into reusable core client functionality.
  • Parses agent get responses into known statuses and safe unknown/unanswered states.
  • Exposes pane-run, agent-get, saved-command, process/workspace, and timeout helpers for the new flow.
rust/devlaunch-core/src/clients/herdr.rs
Adds user-facing launch notices and documentation for recreate behavior and recovery paths.
  • Defines and renders notices for discovery, restart, failure, own-pane, and unresumable-agent outcomes.
  • Documents herdr and non-herdr behavior, retry timing, manual recovery, and unchanged reset/code semantics.
  • Adds changelog coverage.
rust/dl/src/render.rs
docs/cli.md
CHANGELOG.md
Expands automated coverage for collection, parsing, launch ordering, restart retries, quoting, failure handling, and unaffected verbs.
  • Tests workspace filtering, stale or missing saved lines, agent states, herdr failures, pane ownership, retry timing, and shell-safe command reconstruction.
  • Verifies collection occurs before up, restart after successful up, no restart after an untouched failed up, and reset/code remain unchanged.
rust/devlaunch-core/src/flows/agent_sessions.rs
rust/devlaunch-core/src/flows/launch.rs
rust/dl/src/render.rs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@codecov

codecov Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.31683% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.51%. Comparing base (e851e75) to head (6b15858).

Files with missing lines Patch % Lines
rust/devlaunch-core/src/flows/launch.rs 96.04% 13 Missing ⚠️
rust/devlaunch-core/src/flows/agent_sessions.rs 99.63% 2 Missing ⚠️
rust/devlaunch-core/src/clients/herdr.rs 97.05% 1 Missing ⚠️
rust/dl/src/render.rs 98.90% 1 Missing ⚠️
Additional details and impacted files
Flag Coverage Δ
python 42.98% <ø> (ø)
rust 94.72% <98.31%> (+0.05%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
shipped code (rust) 94.72% <98.31%> (+0.05%) ⬆️
harness and tooling (python) 42.98% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

blooop added 2 commits October 7, 2026 15:29
Copied from the public-api CI job's diff. public-api.api.txt gains the
seven LaunchNotice variants, which is a change to the promised API: a
caller that matches LaunchNotice exhaustively needs an arm for each, as
for every notice added before them.

Claude-Session: https://claude.ai/code/session_01Ro7QyyAtHmrh9oxyyK6h9m
…rt it again

herdr's own docs say idle and done both mean the agent is ready for
input. done only says nobody has looked at the pane since the turn
ended. collect read done as an agent the user had quit, so it dropped
the pane's saved line, and the recreate ended that agent for good.

Claude-Session: https://claude.ai/code/session_01Ro7QyyAtHmrh9oxyyK6h9m

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant