Skip to content

One cold-path setup pass: fold the hostname into the probe's round trip, with per-stage outcomes #168

Closed

Description

@blooop

Question

Collapse the cold path's two independent devpod ssh round trips into one setup pass: the hostname becomes a named stage in front of the shipped probe_script(), in the trip the probe already pays. Decided on #157.

Measured saving: ~1.92s per cold launch (composed one trip 1.64s vs two sequential trips 3.56s, medians of 5, real devpod against a real container — method on #157). A devpod ssh --command trip is ~1.73s and is ~99% connection/process setup, so the saving is a whole trip, near-exactly. Do not quote the map's ~0.45–0.56s here — that is devpod list / devpod status, a different command shape, and it understates an exec-into-container trip by ~3.5x.

probe → transfer is explicitly NOT merged. #157 refused it: merging either always streams the ~342MB payload on the very path the three-state probe exists to keep free, or gives the container back its own copy of the official-layout relation — the two-predicate drift #151 took three review rounds to remove. Re-opening that is a gate failure, not an optimization.

Behaviour

  1. One composed setup pass on the cold path, replacing the separate setup_hostname trip there. Composed on the host from probe_script() verbatim plus independent stage snippets. No probe logic is copied, re-expressed, or moved into the container.
  2. ProbeResult.parse and _is_official_claude are unchanged. parse already ignores every line not carrying PROBE_MARK and reads only the tools/versions/claude keys, so stage output is already inert to it. The container gains stage-outcome lines and gains no opinions.
  3. No set -e spanning stage boundaries. Each stage's failure is contained. (transfer_script's set -eu is correct for what it is — an all-or-nothing sequence — which is why the transfer is not a stage in this pass.)
  4. The pass exits 0 in every state, preserving _probe's existing discrimination: a non-zero devpod ssh means the transport failed, never that a stage did.
  5. Per-stage outcomes, three-valued: ok / failed(rc) / not reached (the absence of a line — what a truncated report or a mid-script death looks like). Model it as a sum type, not a bool or a sentinel; "never ran" and "ran fine" must not collapse. The host logs a warning naming any stage that is not ok — that is the whole legibility claim and the reason this merge is allowed at all.
  6. Not gated on provisioning_disabled(). ensure_tools returns before probing when DEVLAUNCH_NO_TOOLS is set; if the hostname rides inside a pass that inherits that gate, disabling tools silently disables the hostname. Whether the pass runs and whether the tools stages do anything are two questions.
  7. The warm path is untouched. attach_workspace keeps its standalone setup_hostname for a warm attach — there is no pass there to fold into.
  8. The "already named" signal is an explicit value carried from the launch that ran the pass to the attach that would otherwise repeat it. Never module-level mutable state, never a re-probe. failed counts as attempted — retrying standalone costs another ~1.7s to fail again.
  9. Accepted behaviour change: a cold dl <ws> -- cmd and a cold dl <ws> up set no hostname today and will now set one for free, inside a trip already paid. This is an improvement — a container created by a one-shot launch is presently nameless until somebody attaches interactively. Say so in the CHANGELOG.
  10. CHANGELOG [Unreleased] entry. State the saving as measured, with the method, and do not restate the ~0.45–0.56s figure.

Seams to test at

test/test_devpod_spawn_counts.py — the round-trip regression seam. Verified drivable for the cold path already: workspace_up(..., workspace_identity=...) under the existing DevpodSpawns recorder records context options, up, the probe ssh, the transfer ssh, in order. No new fake is needed. The file has no cold pin at all today — every pin in it is warm — which is why this path's trip count can move silently.

  • New cold-launch pin: exact argv sequence, one ssh --command carrying the composed pass, then the transfer, and no separate sudo hostname spawn.
  • test_attaching_to_a_running_workspace stays green untouched — it still expects status / ssh --command sudo hostname / ssh. It is the guard that the warm path was not touched; a diff that edits it has broken the scope line.
  • Mandatory isolation: stub host_payload in any cold pin. Observed: an unstubbed run takes the transfer arm only because that host has a claude and a gh to lend; a host with neither takes the absent arm and spawns provision_script instead, so the pin would assert a different sequence per developer. (test_starting_a_workspace_drops_the_snapshot reaches the real host_payload() today and survives only because it asserts on the workspace list, not on spawns.)

test/unit/test_tools.py — the pass itself, in the kinds already present there:

  • Script-string: the composed script contains probe_script() verbatim (so a future edit cannot fork it); no set -e spans stages.
  • Real-bash against scratch $HOMEs (as TestHostPayload and the probe's own tests do): a failing stage still lets the probe answer correctly in the same run, and the failing stage's marked line appears. This is directly observed to work — a composed run printed devlaunch-probe stage hostname-failed followed by a correct devlaunch-probe tools missing.
  • not reached: a report truncated before a stage's line reads as not-reached, never as ok.
  • Runner fake for trip counts per probe answer — provisioned pays exactly one trip (the pass) and nothing else.
  • DEVLAUNCH_NO_TOOLS set → the hostname is still handled. Constraint 6 is invisible without this test.

Blocked by #167

Whether the hostname trip should exist in any form is being decided there: measured, sudo hostname cannot succeed in an unprivileged container (CAP_SYS_ADMIN is dropped by default; this repo's own devcontainer is privileged, which is why it works here). If #167 deletes or replaces it, this ticket is retired rather than built. Do not start before it settles.

Activity

  1. blooop commented on Aug 14, 2026

    @blooop
    OwnerAuthor

    Spec amended by #167's resolution: (1) the hostname stage's failure reports at info level, not warning — it fails by design on every unprivileged image (CAP_SYS_ADMIN), so warn-by-name would fire on the majority case; other stages keep warn-by-name. (2) This PR also deletes setup_hostname from attach_workspace — both halves land together, since deleting the warm trip before the stage exists regresses cold-path naming on privileged images. Warm interactive attach becomes 2 trips (status, ssh); test_attaching_to_a_running_workspace pins the new sequence, test_a_one_shot_command_skips_the_hostname_round_trip is affirmed unchanged.

  2. self-assigned this
    on Aug 14, 2026
  3. blooop commented on Aug 14, 2026

    @blooop
    OwnerAuthor

    breadcrumb: claimed and building. What lands, restated from the ticket plus #167's amendment:

    One composed setup pass on the cold path, built on the host as stage snippets + the shipped probe script verbatim, sent in the trip the probe already pays. Each stage reports its own marked outcome line; the host reads three values per stage — ok, failed(rc), not reached (the absence of a readable line) — as a sum type, and names any stage that is not ok. The hostname is the pass's first and only stage today, reported at info level per the amendment, and it goes in front of the probe deliberately: the probe exits early when a tool is missing, so a stage behind it would read not reached on the commonest cold path. No set -e spans stages, the pass still exits 0 in every state so a non-zero trip still means the transport, and whether the pass runs is decoupled from whether the tools work runs — the tools opt-out no longer silently disables the hostname. probe → transfer stays two trips; that merge is refused, not deferred.

    The amendment's other half lands with it: the standalone hostname trip is deleted from the warm attach, and with it the best-effort host-side helper that owned it — its job is the stage now, so leaving the function behind would leave a second, unreachable definition of "name this container".

    Seams, as the ticket names them:

    • the round-trip pin suite — a new cold-launch pin (the file has none today; every pin in it is warm): exact argv, one ssh --command carrying the composed pass, then the transfer, and no separate hostname spawn. host_payload stubbed, mandatory — an unstubbed run picks its arm from whatever the developer's host has to lend. The warm interactive pin moves to its new two-trip sequence (status, ssh) rather than staying untouched: the amendment reversed that instruction. The one-shot pin is affirmed unchanged.
    • the tools unit suite — string-level (the composed script contains the probe script verbatim; no set -e spans stages), real bash against scratch $HOMEs (a failing stage still lets the probe answer correctly in the same run, and the failing stage names itself), stage-outcome reading (a report truncated before a stage's line reads not reached, never ok), trip counts per probe answer through the existing runner fake, and the opt-out case: tools disabled, hostname still handled.
    • the launch-timing suite — the fold must not move work between stages. The pass stays charged to tools; attach loses a round trip because that trip is deleted, not because it moved, and the stage's pinned spans say so.

    Trip counts change deliberately, both directions stated: cold launch 3 → 2, warm interactive attach 3 → 2.

  4. blooop commented on Aug 14, 2026

    @blooop
    OwnerAuthor

    breadcrumb: built, and open as PR #206 — CI green on every matrix leg.

    The pass is composed on the host from the shipped probe verbatim plus stage snippets, hostname first (the probe exits early on a missing tool, so a stage behind it would read not reached on the commonest cold path). Outcomes are a three-armed sum type read off marked lines, matched exhaustively, with not-reached as the absence of a readable line — so a garbled line is named rather than passed over. The host names everything that is not ok; the hostname names itself at info, warning stays the default any other stage would get. The tools opt-out no longer reaches the pass. probe → transfer untouched.

    The warm half landed with it: the standalone naming helper is deleted rather than unwired, so there is no second, unreachable definition of "name this container" left behind, and the attach's interactive/one-shot branch is gone with the trip it existed to skip.

    Measured on real containers under a scratch devpod home, before = current main, after = the branch, same host and image: warm interactive attach 4.65s → 2.48s (medians of 5), cold interactive launch 8.07s → 5.99s summed over the trips into the container (4 trips → 3). Both consistent with #157's composed-vs-sequential figure and with a trip being ~99% connection setup. The measurement ran on an unprivileged image, so every run reported the hostname stage as failed 1 — which is the majority case the info level was decided for, seen working.

    Two things the ticket asked for that are deliberately absent, both because #167's amendment removed what they were for: the "already named" signal carried from launch to attach has no reader once the warm trip is deleted, and "the warm path is untouched" is the instruction the amendment reversed. Nothing carries state between a launch and its attach.

    Trip counts moved in both directions and are pinned as counts: cold launch 3 → 2 beyond the up, warm interactive attach 3 → 2. The cold pins are new — the file had none at all, which is why this path could move silently.

  5. blooop commented on Aug 14, 2026

    @blooop
    OwnerAuthor

    gist: wf-review verdict on PR #206: Request changes — code meets every spec line (both axes), but README:793 still documents the deleted round-trip-skip (blocking under the README-in-step rule) plus three same-pass doc drifts. Fix agent dispatched on the same branch.

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

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions