Skip to content

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

Merged
blooop merged 15 commits into
mainfrom
feat/recreate-keeps-agent
Oct 16, 2026
Merged

blooop merged 15 commits into
mainfrom
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 absent. It keeps a done pane, because herdr's done is an idle agent that nobody has looked at.
    • 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

Summary by Sourcery

Restore agents ended by a running workspace recreate in their original panes when possible, while providing clear recovery guidance for sessions that cannot be restarted automatically.

New Features:

  • Automatically restore agent sessions in their original herdr panes after a running workspace is recreated.

Bug Fixes:

  • Prevent recreates from silently leaving agents ended by the container replacement when herdr has enough information to restart them.

Enhancements:

  • Add herdr agent-status parsing and resilient pane-session discovery, retrying pane restarts and reporting manual recovery commands when automatic recovery is unavailable.

Documentation:

  • Document recreate behavior, recovery limitations, and the differences from reset and code operations.

Tests:

  • Add coverage for session collection, agent status handling, workspace filtering, pane retries, quoting, failure paths, and launch integration.

Chores:

  • Expose the new launch notices and core session-management API required by the feature.

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.52%. Comparing base (86c70a8) to head (6e8f127).

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.74% <98.31%> (+0.07%) ⬆️

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

Components Coverage Δ
shipped code (rust) 94.74% <98.31%> (+0.07%) ⬆️
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

@blooop blooop left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was generated by AI during review.

wf-review of #675 at 6b158582. Fixed point: merge-base with feat/stale-image-ls, e851e750. Range: git diff e851e750...6b158582, which has 10 files, +1804/−7. Two separate subagents ran the two axes in fresh context. Neither saw the code being written.

Standards

Repo rules checked: dash rule, test citations, public-api widening stated in the PR body, no second copy of a fact without a diff test. None of them is broken. Two defects in this code are fixed higher in the stack in #676: Crockford pane ids in saved_argv, and the saved line that a restarted agent lost.

Findings (all non-blocking):

  1. clients/herdr.rs:1810: the doc is wrong. The doc on AgentStatus::Done says "The agent has ended". agent_sessions.rs:162 and docs/cli.md say that herdr's done means idle and not yet looked at. A maintainer who trusts the enum doc could make collect drop done panes. Fix it before merge.
  2. agent_sessions.rs:64-77 against clients/herdr.rs:2009-2031: Duplicated Code. Manager::resolve copies ResumeReport::resolve: the provisioning_disabled gate, the socket parent + session.json, and the binary fallback. That is two copies of "one rule for which herdr to ask".
  3. agent_sessions.rs:159-174: Primitive Obsession. collect folds AgentReading into Option<Option<String>>. Match on AgentReading directly.
  4. clients/herdr.rs:1803-1858: Speculative Generality. Production code never reads AgentStatus or PaneAgent::status; only a parse test does, and both add pub rows.
  5. launch.rs:5220, agent_sessions.rs:246: Duplicated Code. Two call sites build the same shell line from HeldSession::line. A HeldSession::shell_line() method can own that.
  6. launch.rs:5214: bool flag. restart_agent_sessions(.., up: bool) has two behaviours.
  7. dl/src/render.rs:3299: string compare. The renderer branches on agent == "claude", but herdr::agent_named already models the known agents. The Unseen and Unanswered notices say claude --resume in a flow that is generic.
  8. agent_sessions.rs:46: Mysterious Name. Manager sits beside flows::session_manager. A name like HerdrPanes says what it does.
  9. agent_sessions.rs:182: accidental pub. agent_reading lands in public-api.rest.txt, and only collect calls it.

Axis verdict: Comment. Nothing is blocking.

Spec

Spec: #673 item 2 and its second "Done when" line, and the body of #675.

Met:

  • "starts each session again with claude --resume <id> --remote-control=<name> in the pane it held". restart types herdr's saved line back into the same pane (agent_sessions.rs:236), after the up and before the attach (launch.rs:5139-5152). A launch test checks the order.
  • "this belongs on the host side". All of the work runs in host dl.

Findings (all non-blocking):

  1. "records the session id and remote-control name of each Claude process in the container". The PR reads herdr's session.json instead (agent_sessions.rs:131), and outside herdr it only warns (launch.rs:5176). The PR body gives the reason: argv goes stale after /clear. The deviation is stated.
  2. "dl <ws> recreate on a workspace with an idle agent leaves the same session resumed, with its transcript, on the new image." The PR tests only with fakes. Note from the run owner: the host run on 2026-10-15 checked this with the whole stack (#676). Two recreates in a row with an idle agent kept the same --resume id. After the second recreate, the agent answered a question about the conversation from before the first. The evidence is in the "Found on a real host" section of #676.
  3. The PR body says "herdr agent get removes a pane whose agent is done or absent". The code keeps done (agent_sessions.rs:164), and docs/cli.md agrees with the code. Fix the PR body.
  4. "Saved lines for codex and gemini panes are typed back too" is more than the spec names. It does no harm.

#677 findings 2, 3 and 5 hit this code, and #676 fixes them. Finding 4 (argv[0] is dl) stays unfixed on purpose.

Axis verdict: Comment. Nothing is blocking.

Verdict

Comment. Neither axis has a blocking finding. Before merge, fix Standards 1 (the AgentStatus::Done doc) and Spec 3 (the PR body).

blooop added a commit that referenced this pull request Oct 15, 2026
Found in the wf-review of #675 (Standards 1). agent_sessions and
docs/cli.md treat done as an idle agent nobody has looked at; the enum
doc said the agent had ended, which invites dropping done panes.
Base automatically changed from feat/stale-image-ls to main October 16, 2026 15:06
@blooop
blooop merged commit 0ce5095 into main Oct 16, 2026
15 checks passed
@blooop
blooop deleted the feat/recreate-keeps-agent branch October 16, 2026 15:07
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