Skip to content

Cut the time from launch to a working session - #138

Merged
blooop merged 1 commit into
mainfrom
perf/launch-latency
Aug 8, 2026
Merged

blooop merged 1 commit into
mainfrom
perf/launch-latency

Conversation

@blooop

@blooop blooop commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Three costs sat between dl <spec> and a usable container. Measured on this machine throughout.

A launch asked devpod the same question twice

Every workspace command opened with devpod list --output json purely to test whether devpod knew this workspace, then asked devpod status about that same workspace moments later. One status answers both.

dl owner/repo@branch -- cmd on a running workspace — the shape wayfinder hands every agent launch — is now two devpod spawns rather than three. Measured ~3.4s → ~3.0s per launch, and it no longer grows with the number of workspaces on the machine.

A bare name gets a second opinion before being refused: status consults the provider while list reads devpod's own records, so a workspace whose provider is broken still lists and cannot be described — and that is exactly the workspace somebody is about to dl <ws> rm. Refusing on the status alone would be a wrong diagnosis and a refusal of the command that fixes it.

A cold container downloaded tools the host already had

Provisioning curl-installed pixi and two packages inside every fresh container, and claude-shim then pulled ~285MB from GCS — tens of seconds to minutes, per container, on the critical path. The host almost always has both already and the container is one pipe away on the same disk, so they are streamed in as a tar over the devpod ssh channel dl already holds.

342MB in 5.1s, checksum-verified, both binaries running in the container.

Nothing lands until it has been proved to run there: the tar unpacks into a staging directory, both binaries are run once, and only then are they moved into place, symlinked and put on the login PATH, with a trap removing the staging directory either way. This ordering is load-bearing — the host's claude is dynamically linked, so a musl or older-glibc container fails that check routinely, and unpacking straight into $HOME left a broken symlink and a PATH edit behind when it did. The network fallback then read those through command -v as "already installed", installed nothing, and reported success — leaving a workspace with a claude that could never run, forever. Reproduced, then fixed and re-verified against the same scenario.

Two ups of one workspace raced

dl <spec> up is new: start or create without attaching, so a caller can warm a container before a user arrives. That makes concurrent ups of one workspace ordinary rather than exotic, and two devpod up against one id is not a race devpod promises to survive.

They serialize on a per-workspace lock. A waiter re-checks state and skips an up the sibling already did — but never for a call wanting something the sibling cannot have done (an IDE, a recreate, a reset, a --devcontainer variant), and it still checks the tools, because Running says the sibling's up returned and not that its install did. A lock that cannot be taken is logged and skipped rather than failing a launch.

Also

  • devpod context options is cached against devpod's own config file rather than re-read before every up — so devpod context use <other> invalidates it instead of serving the previous context's dotfiles settings for an hour.
  • The tools probe no longer surfaces its everyday "not installed yet" answer as a red devpod fatal ... Process exited with status 1.
  • .dockerignore excludes .pixi at any depth. A leftover worktree under .claude/worktrees/ put the very symlink that file exists to exclude back into the build context, which was breaking the e2e container build (pre-existing).
  • validate_workspace_spec is gone with the listing it existed to check against.

Verification

  • 1162 unit/integration tests pass; ruff, ty and pylint (10.00/10) clean.
  • Full e2e suite against real containers: 10 passed, 13 skipped (declared opt-outs).
  • Real cold container built end to end, tools lent and running, staging cleaned up, workspace removed after.

🤖 Generated with Claude Code

Summary by Sourcery

Reduce devpod spawn count and serialize workspace launches to cut time from dl <spec> to a usable session, while adding host-based tool provisioning and a non-attaching up command.

New Features:

  • Introduce dl <workspace> up to start or create a workspace without attaching, supporting background prewarming of containers.

Bug Fixes:

  • Ensure failed host-tool transfers leave no broken claude symlink or PATH edits behind and correctly fall back to network installation, avoiding unusable workspaces that appear provisioned.
  • Prevent duplicate or misleading devpod error output from the tools probe by capturing its everyday non-zero exit and only surfacing meaningful install failures.
  • Fix .dockerignore so .pixi directories are excluded at any depth, avoiding accidental inclusion of pixi environments from nested worktrees that broke e2e container builds.
  • Remove obsolete validate_workspace_spec usage and adjust spec rejection paths so truly unknown workspaces are refused without extra devpod spawns while still allowing cleanup of workspaces devpod lists but cannot describe.

Enhancements:

  • Replace broad devpod list usage with targeted devpod status calls during CLI spec resolution and fast-attach, lowering launch overhead and making behavior independent of workspace count.
  • Cache devpod context options on disk with TTL and config-file invalidation to avoid repeated devpod round trips before every up.
  • Serialize concurrent workspace_up calls per workspace using a lock, including contention-aware behavior that skips redundant up when a sibling already started the workspace while still ensuring tools installation.
  • Update tool provisioning to prefer streaming the host’s own claude and gh binaries into the container over devpod ssh, with staged verification and a fallback to the existing network-based pixi/pixi-global installs.
  • Refine workspace CLI error handling and state resolution to use get_workspace_state and a second-opinion listing only for bare names that may represent broken-provider workspaces.
  • Extend bash completion and README documentation to cover the new up subcommand and refreshed workspace lifecycle semantics.

Documentation:

  • Update README and CHANGELOG to document the dl <workspace> up command, the reduced devpod spawn strategy, and host-based tool lending behavior, including performance characteristics and edge cases.

Tests:

  • Add unit tests for launch serialization and lock behavior around workspace_up, including contention scenarios and failure to take locks.
  • Expand tool provisioning tests to cover host payload resolution, tar streaming over ssh, transfer scripts, and probe/transfer/network fallback semantics.
  • Update spawn-count, CLI, completion, and state tests to reflect the new devpod status-based spec resolution, up subcommand, and changed workspace state handling.
  • Adjust existing tests to drop validate_workspace_spec and get_workspace_ids assumptions, pinning behavior on get_workspace_state and new error paths instead.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sorry @blooop, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@sourcery-ai

sourcery-ai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR reduces launch-to-session latency by replacing global workspace listing with targeted status checks, introducing host-side tooling transfer, adding a non-attaching dl <ws> up command with per-workspace launch serialization, and tightening various caching, error-handling, and build behaviors.

Sequence diagram for ensure_tools host lending and fallback flow

sequenceDiagram
    participant Caller as workspace_up
    participant Tools as tools.ensure_tools
    participant Devpod as run_devpod

    Caller->>Tools: ensure_tools(workspace, run_devpod)
    Tools->>Devpod: _probe via run_devpod("ssh", --command "bash -lc checks", capture=True)
    alt tools already present
        Devpod-->>Tools: returncode == 0
        Tools-->>Caller: True
    else tools missing
        Devpod-->>Tools: returncode != 0
        Tools->>Tools: host_payload()
        alt host_payload() returns payload
            Tools->>Devpod: _transfer via run_devpod("ssh", --command transfer_script, stdin_file=tar)
            alt transfer succeeded
                Devpod-->>Tools: returncode == 0
                Tools-->>Caller: True
            else transfer failed
                Devpod-->>Tools: returncode != 0
                Tools->>Tools: provision_script(tools)
                Tools->>Devpod: run_devpod("ssh", --command "bash -lc provision_script")
                Devpod-->>Tools: result
                Tools-->>Caller: result.returncode == 0
            end
        else host_payload() is None
            Tools->>Tools: provision_script(tools)
            Tools->>Devpod: run_devpod("ssh", --command "bash -lc provision_script")
            Devpod-->>Tools: result
            Tools-->>Caller: result.returncode == 0
        end
    end
Loading

Sequence diagram for serialized workspace_up with per-workspace lock

sequenceDiagram
    actor User
    participant CLI as _run_cli
    participant Up as workspace_up
    participant Lock as hold_lock
    participant Devpod as run_devpod
    participant Tools as tools.ensure_tools

    User->>CLI: dl <workspace> up
    CLI->>CLI: get_workspace_state(workspace_id) -> known_state
    alt known_state == "Running" and custom_id is None
        CLI->>Tools: ensure_tools(workspace_id, run_devpod)
        Tools-->>CLI: True/False
        CLI-->>User: return 0 (already running)
    else need up/create
        CLI->>Up: workspace_up(workspace_spec, workspace_identity=workspace_id)
        Up->>Lock: hold_lock(_launch_lock_path(identity), waiting_note)
        alt lock contended (waited == True)
            Lock-->>Up: waited = True
            Up->>CLI: get_workspace_state(identity)
            alt state == "Running" and not (ide or recreate or reset or devcontainer)
                Up->>CLI: invalidate_workspace_list_cache()
                Up->>Tools: ensure_tools(identity, run_devpod)
                Tools-->>Up: True/False
                Up-->>CLI: CompletedProcess(returncode=0)
                CLI-->>User: return 0
            else needs devpod up
                Up->>Devpod: run_devpod(["up", workspace_spec, ...])
                Devpod-->>Up: result
            end
        else lock not contended
            Lock-->>Up: waited = False
            Up->>Devpod: run_devpod(["up", workspace_spec, ...])
            Devpod-->>Up: result
        end
        Up->>CLI: invalidate_workspace_list_cache()
        alt result.returncode == 0 and identity
            Up->>Tools: ensure_tools(identity, run_devpod)
            Tools-->>Up: True/False
        end
        Up-->>CLI: result
        CLI->>CLI: update_cache_background(force=True)
        CLI-->>User: return result.returncode
    end
Loading

File-Level Changes

Change Details Files
Launch path now uses devpod status instead of devpod list, with improved bare-name validation and fast-attach behavior.
  • Remove validate_workspace_spec and its tests; specs are validated via status, path/git checks, and list only for bare names.
  • Resolve owner/repo@branch specs by deriving WorkspaceId, checking devpod status for known_state, and only using clone manager when status reports unknown.
  • For bare workspace names, consult get_workspace_state and fall back to get_workspace_ids to distinguish missing workspaces from provider failures, ensuring rm/stop can act on list-only workspaces.
  • Thread known_state from spec resolution into fast-attach instead of re-calling get_workspace_state, so launches pay a single status call per workspace.
  • Update tests to mock get_workspace_state instead of get_workspace_ids and to assert new error messages and spawn patterns.
devlaunch/dl.py
test/test_dl.py
test/test_devpod_spawn_counts.py
test/test_workspace_state.py
example/example.py
test/test_interactive_command.py
test/test_bash_completion.py
test/test_cli_error_messages.py
Add host-first tool provisioning via devpod ssh tar stream, with robust staging, gating, and fallback to the existing pixi-based network install.
  • Extend run_devpod to accept stdin_file and pass it to subprocess.run for both captured and non-captured calls.
  • Introduce HostPayload dataclass, host_payload() discovery, and helpers to locate official claude and gh binaries (including pixi trampolines) under the user’s home.
  • Implement transfer_script() that unpacks a tar payload into a staging dir, gates on running both binaries, then moves them into ~/.local, creates the claude symlink, and edits PATH guardedly.
  • Implement _write_payload_tar(), _probe(), and _transfer() utilities, and refactor ensure_tools into a three-step flow: probe, host transfer, then pixi-based network provision, with adjusted logging and failure handling.
  • Add extensive unit tests for tools provisioning, host payload discovery, transfer script behavior, and end-to-end tar stream correctness; add fixtures to control host payload resolution in tests.
devlaunch/tools.py
devlaunch/dl.py
test/unit/test_tools.py
Introduce dl <workspace> up (non-attaching start/create) and serialize concurrent ups of a workspace using per-workspace locks keyed in the cache.
  • Add up subcommand handling in _run_cli that uses workspace_up with workspace_identity, respects known_state, and ensures tools even when skipping an actual devpod up on already-running workspaces.
  • Extend workspace_up to use a per-workspace lock via hold_lock, track contention, re-check state for contended launches, and skip redundant ups while still running ensure_tools under the lock.
  • Add _launch_lock_path helper to compute lock paths outside the repo cache and update the lock context manager hold_lock to yield a boolean indicating contention.
  • Ensure lock acquisition failures (e.g., PermissionError) log debug and do not block workspace_up from proceeding.
  • Add CLI help, README, and bash completion entries for the new up subcommand; add a dedicated unit test suite for launch serialization scenarios and lock behavior.
devlaunch/dl.py
devlaunch/worktree/locks.py
devlaunch/completions/dl.bash
README.md
CHANGELOG.md
test/unit/test_launch_serialization.py
test/test_bash_completion.py
Cache devpod context options on disk with TTL and invalidation tied to devpod config changes, reducing repeated context option round-trips.
  • Add CONTEXT_OPTIONS_TTL_SECONDS constant, cache path helper, and devpod config path helper that honor DEVPOD_HOME.
  • Refactor get_context_options to read from cache when fresh and devpod config hasn’t changed, otherwise fetch via devpod context options, persist atomically to cache, and handle errors gracefully.
  • Adjust tests that expect context options fetch behavior to align with new caching, including mocking run_devpod in launch serialization tests.
  • Ensure cache directory resolution uses runtime _get_cache_dir so tests that override XDG_CACHE_HOME are respected.
devlaunch/dl.py
test/unit/test_launch_serialization.py
Tighten devpod spawn expectations, error messages, and .dockerignore behavior around pixi directories and tooling.
  • Update devpod spawn count tests to reflect removal of devpod list from common paths and the new status-based resolution, including scenarios with git specs and undescribable workspaces.
  • Ensure error messages for invalid specs and clone/branch/ensure_workspace failures remain single-line and non-duplicated under new resolution logic.
  • Adjust .dockerignore (and its tests indirectly) to exclude .pixi directories at any depth to avoid including unwanted pixi environments from nested worktrees that break container builds.
  • Update README and CHANGELOG to describe new behavior, performance characteristics, and correctness fixes around tools and workspace handling.
test/test_devpod_spawn_counts.py
.dockerignore
README.md
CHANGELOG.md
test/test_workspace_state.py

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

Three costs sat between `dl <spec>` and a usable container, and none of
them were doing anything.

**A launch asked devpod the same question twice.** Every workspace
command opened with `devpod list --output json` purely to test whether
devpod knew this workspace, then asked `devpod status` about that same
workspace moments later. One `status` answers both, so the listing is
gone from the path: `dl owner/repo@branch -- cmd` on a running workspace
is two devpod spawns rather than three. Measured on the reference
machine, ~0.4-0.5s off every launch, and it no longer grows with the
number of workspaces the machine has.

A bare name gets a second opinion before being refused. `status`
consults the provider while `list` reads devpod's own records, so a
workspace whose provider is broken still lists and cannot be described
-- and that is the workspace somebody is about to `dl <ws> rm`.

**A cold container downloaded tools the host already had.** Provisioning
curl-installed pixi and two packages inside every fresh container, and
`claude-shim` then pulled ~285MB from GCS. The host almost always has
both already and the container is one pipe away on the same disk, so
they are streamed in as a tar over the `devpod ssh` channel dl already
holds: 342MB in 5.1s here, checksum-verified, both binaries running in
the container.

Nothing lands until it has been proved to run there. The tar unpacks
into a staging directory, both binaries are run once, and only then are
they moved into place, symlinked and put on the login PATH. The host's
claude is dynamically linked, so a musl or older-glibc container fails
that check routinely -- and unpacking straight into $HOME left a broken
symlink and a PATH edit behind when it did, which the network fallback
then read through `command -v` as "already installed". The workspace
ended up with a claude that could never run, reported as success
forever.

**Two `up`s of one workspace raced.** `dl <spec> up` is new -- start
without attaching, so a caller can warm a container before a user
arrives -- and that makes concurrent `up`s of one workspace ordinary
rather than exotic. They serialize on a per-workspace lock; a waiter
re-checks state and skips an `up` the sibling already did, but never for
a call wanting something the sibling cannot have done (an IDE, a
recreate, a reset, a --devcontainer variant), and it still checks the
tools, because "Running" says the sibling's `up` returned and not that
its install did. A lock that cannot be taken is not worth failing a
launch over.

Also: `devpod context options` is cached against devpod's own config
file rather than re-read before every `up`; the tools probe no longer
surfaces its everyday "not installed yet" answer as a red devpod
`fatal`; `.dockerignore` excludes `.pixi` at any depth, which is what
was breaking the e2e container build; and `validate_workspace_spec` is
gone with the listing it existed to check against.
@codecov

codecov Bot commented Aug 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.00000% with 27 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.03%. Comparing base (d04a6f6) to head (6abc36f).

Files with missing lines Patch % Lines
devlaunch/dl.py 72.60% 20 Missing ⚠️
devlaunch/tools.py 91.78% 6 Missing ⚠️
devlaunch/worktree/locks.py 75.00% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #138      +/-   ##
==========================================
- Coverage   93.79%   93.03%   -0.76%     
==========================================
  Files          20       20              
  Lines        2239     2354     +115     
==========================================
+ Hits         2100     2190      +90     
- Misses        139      164      +25     
Files with missing lines Coverage Δ
devlaunch/worktree/locks.py 76.19% <75.00%> (-2.76%) ⬇️
devlaunch/tools.py 94.82% <91.78%> (-5.18%) ⬇️
devlaunch/dl.py 92.90% <72.60%> (-1.58%) ⬇️

Impacted file tree graph

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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