Repository navigation
Collapse the three sequential devpod ssh round trips on the cold path #157
Description
Question
Cold interactive launch issues three sequential devpod ssh exec round trips into the same container — tools probe, tools transfer, hostname — before the attach. At the map's working number of ~0.45–0.56s per devpod round trip, that is roughly 1.4s of the cold path spent on process spawn and connection setup rather than work. Decide whether these collapse, and into what.
This fog was parked pending the tools-transfer decision, which is now settled on #141: the probe becomes three-state (provisioned/lendable/absent), the payload stays all-or-nothing, and ensure_tools never runs on fast-attach — so this is a cold-path-only question and the warm path must not be touched.
Decide against the principles (maintainability > simplicity):
- Collapse or overlap? Options include: one
devpod sshinvocation running a single composed remote script that probes, receives, and reports the hostname in one round trip; keeping them separate but overlapping the independent ones concurrently; or leaving them sequential because the coupling costs more in legibility than 1.4s buys. Note the probe's result decides whether the transfer happens, so probe→transfer is a genuine data dependency — a naive merge would either always transfer or need the remote script to branch. The hostname fetch has no such dependency. - Where does the composed script live? Three-state tools probe: only the official claude layout counts as provisioned #151 built a pure
probe_scriptthat reports resolved paths as markedkey valuelines and names no state at all — the host decides the state from those facts via_is_official_claude, the same predicate_claude_sourceuses, so the two sides cannot drift. (This supersedes the earlier "prints exactly one of three states" wording, which is what let a shim underversions/be trusted; see the amendment on Three-state tools probe: only the official claude layout counts as provisioned #151.) It never executes the candidate binary. If a composed script is the answer, it must reuse that script rather than growing a second copy of the probe logic (principle 2: one concept beats two that overlap). - Failure legibility. Three round trips fail with three distinguishable errors; one composed script can fail opaquely. Say how a failure in the merged form still names which stage failed — this is the main cost against merging and must be answered, not waved.
- How is the outcome tested?
test/test_devpod_spawn_counts.pypins exact devpod argv sequences per command and is the established seam for round-trip regressions; a reduction here should show up as a changed pin there, so the saving cannot silently regress.
Code changes graduate as build ticket(s); this ticket only decides. Measure the actual per-round-trip cost before and after rather than assuming the ~0.5s working number — PR #156 adds env-gated timing instrumentation that should make this measurable.
Blocked by #151 — the probe's final shape is an input to this decision.
Amendment (2026-08-09). Question 2 originally described #151's probe as printing one of three state tokens. It does not, and deliberately so — see the amendment on #151. Any composed script this ticket designs must therefore compose a script that reports facts, and must keep the state decision on the host side; re-introducing state naming in the container would restore the two-predicate drift that #151 removed. The "reuse rather than grow a second copy" constraint is unchanged and now matters more, since the thing being reused is a fact reporter rather than a classifier.
Activity
blooop commentedon Aug 9, 2026
breadcrumb: measured, not inherited — a
devpod ssh --commandround trip costs ~1.73s, not ~0.45–0.56s. The map's working number is real but is for a different command shape:test_devpod_spawn_counts.py's own docstring attributes 0.44s todevpod listand 0.56s todevpod status, which are local devpod CLI calls, not an exec into a container.Method (reproducible, this machine, 2026-08-09): isolated
DEVPOD_HOMEunder the scratchpad, docker provider, one throwaway workspacegrill157onmcr.microsoft.com/devcontainers/base:ubuntu-24.04, deleted afterwards; the shareddevpod listwas verified unchanged./usr/bin/time, median of n:devpod ssh ws --command 'true'devpod ssh ws --command 'sudo hostname ws'devpod ssh ws --command "bash -lc <the shipped probe_script()>"devpod status ws --output jsondevpod list --output jsonTwo things fall out. The real probe costs the same as
true, so a round trip is ~99% connection/process setup and ~0% work — collapsing N trips into 1 saves (N-1) × ~1.7s essentially exactly. And the ticket's "roughly 1.4s of the cold path" is understated by ~3.5x: three ssh trips are ~5.2s.Not measured, and not claimed: the transfer trip's end-to-end cost (it streams ~342MB), and any of this on hardware other than this one. PR #156 is still open, so no instrumented in-tree number exists; this is external
timearound realdevpod.blooop commentedon Aug 9, 2026
breadcrumb: two findings from the same scratch container that bear on the answer, both observed rather than reasoned.
1.
sudo hostnamecannot succeed in a default devcontainer. Ingrill157(unprivileged docker provider, stockmcr.microsoft.com/devcontainers/base:ubuntu-24.04):whoami→vscode,sudo -n true→ rc 0, andsudo hostname grill157→ rc 1,hostname: you must be root to change the host name.hostnameafterwards still reports the container id. sudo is not the blocker — setting a hostname needsCAP_SYS_ADMIN, which Docker drops by default. This repo's own devcontainer is privileged (the docker-in-docker feature brings"privileged": true), which is why it works here and is invisible in this repo's own testing. Sosetup_hostnameis paying a measured ~1.75s round trip to fail silently for every unprivileged container —attach_workspaceignores its return value, and the failure is on stderr nobody reads. That is on the warm interactive attach too, where it is now the single largest item on the path.2. The composed-trip failure legibility mechanism works, demonstrated by accident. The composed run above printed:
The hostname stage failed, said so on a marked line, and the probe still answered correctly in the same trip. That is question 3's answer executing rather than being argued: a stage that names itself is more legible than what ships today, where the same failure is silent.
Also verified:
ProbeResult.parseneeds no change to compose. It walks lines, skips anything not carryingPROBE_MARK, and only reads thetools/versions/claudekeys — so extra stage output, in front of or behind the probe, is already inert to it. Reuse ofprobe_script()verbatim is therefore free, and question 2's "no second copy of the probe logic" costs nothing to honour.blooop commentedon Aug 9, 2026
breadcrumb: question 4's seam checked by driving it, not by reading it.
test/test_devpod_spawn_counts.py's existingDevpodSpawnsrecorder already reaches the cold path —workspace_up("brand-new", workspace_id=..., workspace_identity=...)under the recorder produced, in order:So the file needs no new fake to pin a cold launch; it just has no cold pin at all today — every existing pin is warm (
status/list). That absence is why the trip count on the path this ticket is about can move without any test noticing.One isolation hazard found in passing, which any cold pin must handle: that call ran the real
host_payload()against this developer's machine, and the transfer trip above appeared only because this host happens to have a claude and a gh to lend. On a host with neither, the same code takes theabsentarm and spawns aprovision_scripttrip instead — so a cold pin that does not stubhost_payloadasserts a different argv sequence depending on whose laptop runs it.test_starting_a_workspace_drops_the_snapshotalready calls into this today without stubbing; it survives only because it asserts on the workspace list rather than on the spawns.blooop commentedon Aug 9, 2026
agent-decided (maintainability): the three trips collapse to two, not one. The hostname folds into the probe's trip as a named stage of a single cold-path setup pass; probe → transfer stays two trips and must not be merged, because every way of merging it either pays the ~342MB stream on the path that exists to skip it, or gives the container back an opinion about "the official layout" — the two-predicate drift #151 spent three review rounds removing. Failure legibility is not a cost here but a gain: each stage names itself on a marked line, so the merged form makes visible a hostname failure that today is silent.
Measured, not inherited — see the breadcrumbs above for method and raw numbers.
The number the ticket asked me to check was wrong, and in the direction that matters
The ticket, and the map's Notes, price a round trip at ~0.45–0.56s and the three trips at "roughly 1.4s". That figure is real but belongs to a different command shape.
test_devpod_spawn_counts.py's own docstring attributes it todevpod list(0.44s) anddevpod status(0.56s) — local devpod CLI calls that never enter a container. I reproduced both (0.42s, 0.50s) and then measured the shape this ticket is actually about:devpod ssh <ws> --command 'true'— 1.73s (median of 7)probe_script()overbash -lc— 1.75s (median of 5)The probe costs the same as
true. A round trip is ~99% connection and process setup and ~0% work, which means collapsing N trips into one saves (N−1) × ~1.7s, near-exactly — there is no amortization to hope for and none to lose. Three ssh trips are ~5.2s, not 1.4s.Directly measured on the merge this decides:
PR #156 is still open, so none of this used in-tree instrumentation; it is external
/usr/bin/timearound realdevpodagainst a throwaway workspace under an isolatedDEVPOD_HOME, deleted afterwards. Not measured and not claimed: the transfer trip's end-to-end cost, and any of this on other hardware. This machine, this date, one image.I record this the way #158 recorded its correction: the ~0.45–0.56s working number must not be quoted for a
devpod ssh --commandtrip. It is right forlist/statusand understates an exec-into-container trip by ~3.5x.Q1 — Collapse or overlap? Collapse the hostname into the probe. Leave probe → transfer alone.
probe → transfer: no, and this is principle 1 deciding
The ticket names the data dependency and asks me not to paper over it. There are exactly three ways to merge, and all three are worse than the 1.7s they buy:
_is_official_claudeand is asked by both_claude_sourceandProbeResult.parse— one definition, one language. A shell copy of it is a second definition, and the history here is not hypothetical: the first copy trusted a shim parked underversions/that the host refused, and the review that caught it needed two further rounds to close the symlinked-$HOMEand stock-~/.profilefaces of the same defect. Buying 1.7s by re-creating that seam fails principle 1 outright — whoever reads this in a year would find two answers to one question, and no way to tell which one ran.There is no fourth option, because the branch has to happen somewhere and the two candidates are "the host, after a trip" (status quo) or "the container, with its own copy of the predicate" (forbidden). Sequential is not a concession here; it is the shape that keeps the predicate singular.
hostname → probe: yes
The hostname has no data dependency at all, the merge is measured at ~1.92s, and — decisively — the merged form is the one that makes an existing silent failure visible (Q3). Merging beats overlapping-concurrently: concurrency would save the same wall clock while keeping two devpod processes, two failure paths, and interleaved output, and would need threading on a path that currently has none. The map's own cost model counts processes; principle 2 says one concept beats two that overlap.
Was "leave them sequential" seriously weighed? Yes, and it lost on evidence I did not expect
The honest case for leaving them alone is that ~1.9s is noise beside a
devpod upthat runs for seconds to minutes. What defeats it is not the 1.9s. It is that the trip being folded in does not work, and nobody can see that it doesn't. In the scratch container:sudo -n true→ rc 0,sudo hostname grill157→ rc 1,hostname: you must be root to change the host name. Setting a hostname needsCAP_SYS_ADMIN, which Docker drops by default; this repo's own devcontainer is privileged (docker-in-docker brings"privileged": true), which is exactly why the repo cannot see this in its own testing. So today devlaunch spends a measured ~1.75s per launch on a call that fails for every unprivileged container, andattach_workspacediscards the boolean that says so.A decision that leaves it sequential leaves that invisible. The merge is the change that makes it speak.
Q2 — Where does the composed script live?
Composed on the host, from
probe_script()verbatim plus independent stage snippets. Nothing about the probe is copied, re-expressed, or moved into the container.This costs nothing to honour, because
ProbeResult.parsealready composes. It walks the report line by line, skips every line that does not carryPROBE_MARK, and reads only thetools/versions/claudekeys. Foreign output before or after the probe is already inert to it — that tolerance was built for login-profile banners, and a stage line is the same shape of noise.ProbeResult.parseand_is_official_claudeare unchanged by this ticket. The container gains stage-outcome lines and gains no opinions.Binding constraints on the composed form:
probe_script()verbatim — a string-level test should assert the composed script containsprobe_script(), so a future edit cannot fork it.set -espanning stage boundaries. A stage's failure is contained to that stage.transfer_scriptusesset -eucorrectly for what it is — an all-or-nothing sequence — and that is precisely why it must not be a stage in this pass._probealready relies on: a non-zerodevpod sshmeans the transport failed, never that a stage did.provisioning_disabled().ensure_toolsreturns before probing whenDEVLAUNCH_NO_TOOLSis set. If the hostname rides inside a pass that inherits that gate, disabling tools silently disables the hostname — an unrelated coupling introduced by the merge, and exactly the kind of thing that is discovered a year later. Whether the pass runs and whether the tools stages do anything are two questions.attach_workspacekeeps its standalonesetup_hostnamefor a warm attach, where there is no pass to fold into. Only the cold path stops paying separately.Accepted, and small: a cold
dl <ws> -- cmdand a colddl <ws> upcurrently set no hostname and would now set one for free, inside a trip they already pay. That is a behaviour change, and it is an improvement — a container created by a one-shot launch is presently nameless until somebody attaches interactively.Q3 — Failure legibility. Concretely, and it improves rather than degrades
The objection is right in general: one script can fail opaquely. It does not apply to this composition, and I can show it rather than argue it. The composed run I timed printed:
The hostname stage failed, named itself, and the probe still answered correctly in the same trip. Compare the status quo, where that same failure produces nothing at all:
setup_hostnamereturnsFalseandattach_workspacethrows it away.The contract that makes this hold:
if <stage>; then echo "<MARK> stage <name> ok"; else echo "<MARK> stage <name> failed $?"; fi.ok,failed(rc), andnot reached— the last being the absence of any line for that stage, which is what a truncated report or a mid-script death looks like. That is the real state space, so it is modelled as one (principle 3): a two-valued reading would have to call "never ran" and "ran fine" the same thing, or invent a sentinel.ok. This is the whole legibility claim, and it is what three separate trips give for free today only in principle — in practice the hostname trip's failure is already discarded, so three trips are not currently three distinguishable errors. Two of them are, and the third is silence.devpod sshstill means the connection did not get through, distinct from any stage's failure. That discrimination exists today in_probeand the merge preserves it exactly.So the answer to "how does a failure in the merged form name which stage failed" is: because naming the stage is the composition's job, and it is the only part of the composition that is new. A merged form without per-stage outcomes would indeed be a legibility regression and should not be merged.
Q4 — How the outcome is tested
test/test_devpod_spawn_counts.pyis the seam, as the ticket says — and I drove it to check the claim rather than assert it. Its existingDevpodSpawnsrecorder already reaches the cold path:workspace_up(...)under it recordscontext options,up, the probessh, and the transferssh, in order (breadcrumb above has the argv). No new fake is needed.What it lacks is a cold pin at all — every pin in the file today is warm (
status/list). That absence is why the trip count on this ticket's own path can move with nothing failing. So:ssh --commandcarrying the composed pass, then (payload permitting) the transfer, and no separatesudo hostnamespawn. The saving is then pinned as a count, not a hope.test_attaching_to_a_running_workspacemust stay green untouched — it still expectsstatus,ssh --command sudo hostname,ssh. It is the guard that the warm path was not touched, so a diff that edits it has broken the scope line.host_payload. The run above took the transfer arm only because this host has a claude and a gh to lend; a host with neither takes theabsentarm and spawnsprovision_scriptinstead, so an unstubbed pin asserts a different sequence per developer.test/unit/test_tools.pyfor the pass itself, in the kinds already there: string assertions (the composed script containsprobe_script(); noset -espans stages), real-bash runs against scratch$HOMEs (a failing stage still lets the probe answer, and its marked line appears), and theRunnerfake for trip counts per probe answer.DEVLAUNCH_NO_TOOLScase gets its own test: tools disabled, hostname still handled. Constraint 5 is invisible without it.not reached: a report truncated before a stage's line must read as not-reached, not as ok.What this opens, and why the build is sequenced behind it
The
CAP_SYS_ADMINfinding is not this ticket's to decide — it lands on the warm interactive attach, which this ticket is scoped out of, and the hostname is user-visible behaviour that does work in privileged containers including this repo's own. But it makes "fold the hostname in" conditional on the hostname trip being worth keeping in any form, and there is at least one zero-trip alternative worth weighing (devlaunch already edits the container's login profile intransfer_scriptandprovision_script, and a prompt is not the same thing as a hostname).Building the fold before that is settled risks building a fold around something that gets deleted. So the grilling ticket goes first and blocks the build ticket. Recorded plainly rather than merged into this ticket's answer, because it is warm-path fog my measurement opened, not a question #157 was asked.
blooop commentedon Aug 9, 2026
breadcrumb: graduated. #167 (
wayfinder:grilling) — does the hostname round trip earn its ~1.75s, givensudo hostnamecannot succeed withoutCAP_SYS_ADMIN; warm-path, so #157 could not decide it. #168 (wayfinder:build) — the one cold-path setup pass with per-stage outcomes. Both parented to map #139; #167 blocks #168, because folding around a trip that may be deleted is waste. No build ticket exists for merging probe→transfer: that merge is refused, not deferred.