feat(ca): flat lifecycle commands — list, create, ssh, wake, sleep, delete - #1064
Merged
Conversation
…elete The lifecycle operations already existed, wearing flag costumes on a command whose job is something else: `--new` creates, `--rm` destroys, `--keep-awake` declines to sleep. `launch()` even short-circuits `--rm` before the launch pipeline with a comment noting it is not a launch. This gives each one a name. The line that matters is creation. `ca create` makes a VM and nothing else; `ca ssh` connects to an agent that exists and errors otherwise; `ca start` and `railway code` stay the create-and-launch path. So a mistyped agent name is an error rather than a second billed VM. `ca ssh` rather than `ca connect`: it is an ssh connection over the same relay sandboxes use, and `railway sandbox ssh` already means "get onto the box, optionally resuming a named session" — durable-session resume included. Bare `ca ssh` attaches to the agent's session, `ca ssh <agent> -- bash` is a plain shell, and `connect` is a visible alias so both spellings work. Notes on the shape: - One resolver for every verb, in src/controllers/cloud_agent.rs: an explicit name or id, then this environment's remembered agent, then your sole live one, then a list of candidates. No interactive prompt anywhere in it — a lifecycle command that stops to ask is unusable in a script, and ambiguity has a better answer than a guess. - `list` defaults to the whole account via myCloudAgents. Scoping it to the linked environment would print nothing in the common case, since agent work is rarely done from a linked directory. - `ensure_running` will not adopt the launcher's habit of treating a crashed agent as a cue to create a fresh one. That is a fine answer to "get me coding" and a bad one to "wake this agent". - `sleep --all` exists and `delete --all` does not: agents have no idle timeout so bulk sleep is the cost valve, but bulk disk destruction behind one flag is not. - Pointer bookkeeping lives in the controller, so a delete cannot leave `railway code` waking a corpse. `--rm` keeps working and now points at `ca delete`, and the launcher's exit hints name the agent instead of printing a raw ssh line and two UUIDs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…asleep" Both found running the lifecycle end to end. `ca ssh <agent> --session <typo>` woke a sleeping agent, failed the session lookup, and returned — leaving a machine with no idle timeout awake and billing. Measured at 12s, so the wake was real, not a no-op. Now a failed connect puts back what the run changed, and only that: an agent found already running was someone's deliberate state, possibly with a session open in another terminal, and a failed connect here is no reason to suspend it. The sleep mutation also returns before the agent finishes transitioning, so `ca sleep foo && ca list` printed "is asleep" followed by "running". Reworded to describe the action rather than assert a state the next command contradicts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sleeping an agent could silently lose its most recent work. `cloudAgentSleep` snapshots the disk without quiescing the guest, so pages still dirty in its page cache are absent from the image the next wake restores. Measured on scratch agents, three runs isolating the variable: write, no sync + sleep-on-disconnect -> lost write + sync + explicit sleep, 10s quiesce -> survived write + sync + sleep-on-disconnect, immediate -> survived So it is the flush that matters, not the timing — waiting longer before sleeping would only narrow the window. A side ssh running `sync` is enough, because sync(2) flushes the whole filesystem regardless of which process dirtied it, so it covers whatever the durable session was writing. The flush is paired with the mutation inside controllers::cloud_agent::sleep rather than left to callers: there are four paths that suspend an agent (the two `railway ca` ones, the launcher's disconnect, and the TUI's), and any one of them forgetting is silent data loss. The bare mutation is now private. It is best-effort and bounded at 5s. Failing to reach an agent must not stop us sleeping it — agents have no idle timeout, so the alternative to an imperfect sleep is a machine that bills until someone remembers it. Deliberately not ssh_plumbing, whose ~20s retry budget exists for waking agents and would make every disconnect slow whenever the relay is unhealthy. `sleep --all` runs its flushes concurrently so the cost-control command does not become a second per agent. Verified end to end: the write that vanished before now survives the same sleep/wake, and a disconnect costs ~1s more (1.0s -> 2.1s). The real fix belongs server-side, in cloudAgentSleep quiescing the guest before it snapshots — the dashboard and any future API caller have the same problem, and only the platform can make it a guarantee rather than a narrowed window. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two conflicts, both where #1063's telemetry landed on lines this branch had already changed: - `prepare_inner`'s resolve_target call: keep master's telemetry wrapper, bind `_project_id` since this branch removed `Prepared::project_id`. - The TUI's `close_and_sleep`: keep both. Sleeping goes through the controller so the disk is flushed first, and the failure still fires `quit_sleep_failed` — an agent left billing with nothing attached is exactly what that event is for. The no-environment fallback keeps the bare mutation, since without a relay target there is nothing to flush through. `run_agent_op`'s trailing `let _ = environment_id` goes: the Sleep arm now uses it to reach the agent.
#1063 covered launches, the TUI's manage-screen ops, and setup. The flat CLI verbs this branch adds had only the generic per-dispatch event, which says `command="cloud_agent"` and cannot say which verb ran. Adds `track_lifecycle`, following the module's existing shape: - One event per verb — `cli_list`, `cli_create`, `cli_ssh`, `cli_wake`, `cli_sleep`, `cli_delete`, with `_failed` variants — carrying real duration, which is the interesting part for `create` and `wake` since both wait for RUNNING. - Detail slugs for the paths worth separating: `cli_ssh_attach` vs `cli_ssh_new_session` vs `cli_ssh_command` (reattaching to existing work, provisioning a first session, and one-shot commands are three different things), and `cli_sleep_all` for the fleet-wide cost valve. The `cli_` prefix keeps these distinct from `track_agent_op`'s `agent_sleep` and friends. Same three mutations, two surfaces — merging them would hide which one people actually reach for. A test pins that separation. Five verbs are wrapped at the dispatch, where the shape is identical. `ssh` tracks itself: it ends in `std::process::exit` to propagate the remote command's status, which would skip anything wrapped around it. Its body moved to `ssh_connect` returning the exit code, so the event fires before the exit, and a non-zero remote status is reported as a success — ssh worked, the command it ran did not. Verified: `ca ssh <agent> -- sh -c 'exit 3'` still exits 3. No free text in any of it: fixed slugs only, with `error_message` following the same convention as every other failure event in the CLI.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds six flat lifecycle subcommands to
railway ca.ca list(ls)-enarrows,--allincludes teammates',--jsonfor scriptsca create(new)--variable,--env-file,--no-wait,--jsonca ssh(connect)-- bashfor a plain shell,--sessionto pick one,--keep-awaketo skip sleep-on-disconnectca wakeca sleep--allfor every running agent you ownca delete(rm)-yto skip)Agents are addressed by name or id. With neither, commands use this directory's agent, then your only live one, then list the candidates.
Structure
src/controllers/cloud_agent.rs— agent model normalised across the generated query types, the operations, the shared resolver, and config pointer bookkeeping.src/commands/cloud_agent/lifecycle.rs— the six verbs.ca createmakes a VM only;ca sshnever creates.ca startandrailway coderemain the create-and-launch path.listis account-wide viamyCloudAgents, so it works in an unlinked directory.railway code --rm,--keep-awakeand--newkeep working;--rmpoints atca delete.Telemetry
Extends #1063's
cloud_agent::telemetryto the new verbs, which otherwise had only the generic per-dispatch event that can't say which verb ran.cli_list,cli_create,cli_ssh,cli_wake,cli_sleep,cli_delete, with_failedvariants and real duration.cli_ssh_attach/cli_ssh_new_session/cli_ssh_command, andcli_sleep_all.cli_prefix keeps these separate from the TUI'sagent_sleep/agent_wake/agent_delete— same mutations, different surface. A test pins the separation.Fixes
cloudAgentSleepsnapshots the disk without quiescing the guest, so pages still dirty in its page cache were absent after a wake. Sleep now runssyncover a side ssh first, paired insidecontrollers::cloud_agent::sleepso none of the four suspend paths can skip it. Best-effort, 5s cap;sleep --allflushes concurrently. Adds ~1s to a disconnect.ca sleepreported "asleep" before the transition finished, contradicting a followingca list.Testing
957 unit tests, clippy clean. Full
create → ssh → wake → sleep → deleterun against live agents: create RUNNING in 7.3s, remote command in 1s, auto-wake in 12s, delete clears the local pointer. Error paths checked (unknown name,--allwithout-e,deleteover a pipe,wakeon a running agent). Disk-loss case re-run after the fix and confirmed resolved.Not included
forward,exec, andfork/checkpoint/template.🤖 Generated with Claude Code