Skip to content

feat(agent-sdk): run assigned tasks in pre-warmed sandboxes - #16539

Merged
captainsafia merged 13 commits into
masterfrom
safia/prewarmed-assignment-client
Oct 20, 2026
Merged

captainsafia merged 13 commits into
masterfrom
safia/prewarmed-assignment-client

Conversation

@captainsafia

@captainsafia captainsafia commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Description

A pre-warmed standby sandbox can now receive a run from the server, install it in place, and run it. Before this change, a standby sandbox prepared, heartbeated, and exited at its deadline, so no run ever used it. This is the client side of the server's assignment contract, where the server commits a run to a sandbox before it tells the sandbox.

The protocol has no accept or reject step. The sandbox receives assignment or retired on a control socket and sends only heartbeat. After it installs the run's credential, it reconnects with that credential. The server admits the connection, revokes the standby credential, and replays the assignment; the replay confirms delivery. Only then does the run start, so a cancelled or superseded run never starts.

Commits

Each commit is one review unit, in reading order. The branch is linear on master at #16538 (ae60375ef). Only the branch tip is validated; intermediate commits do not all build.

# Commit Changes
1 52b0c69 feat(terminal): stop a local shell's event loop for replacement Adds Message::ShutdownForReplacement, so the PTY event loop can stop a shell without ending the terminal.
2 09a451a feat(terminal): replace an idle local shell with a new environment Adds TerminalView::replace_shell, which restarts the shell behind a terminal with a new directory and environment. It accepts only an idle, writable local shell, and its error names the condition that refused the replacement. The pane routes the request to the local terminal manager of that view, which starts the replacement on the cloud shell-recovery resources. The replacement environment becomes the baseline for later shell recoveries. Planned replacement is not supported on Windows, because pre-warmed sandboxes run only on Linux.
3 d03c91b feat(mcp): restart templatable MCP servers with a new runtime environment Adds a runtime environment that overrides the environment of spawned CLI MCP servers without being persisted. replace_runtime_environment stops every server and returns the ones to restart. The runtime environment is native-only.
4 830a748 feat(server-api): talk to pre-warmed sandboxes over the control socket Replaces the HTTP heartbeat with the /ws control socket and its assignment, retired, and heartbeat messages. 410 Gone maps to "unavailable". A message type the client does not know decodes as Unknown, so the server can add types without breaking a waiting sandbox. The decode test uses the server's own assignment fixture. The configuration query keeps #16538's fingerprint/spec contract.
5 cecdda2 refactor(agent-sdk): resolve agent driver options in one place split_run_options is the only destructure of AgentDriverOptions. create and the pre-warmed path both apply the result with apply_run_configuration, so a new option cannot reach one path and miss the other.
6 ab51390 refactor(agent-sdk): let launches without CLI arguments reuse run setup Execution-config parsing, Git credential bootstrap, team metadata refresh, the Factory-repository decision, and Bedrock OIDC setup now accept a launch with no RunAgentArgs, so the assignment path calls them instead of copying them.
7 63f521e feat(agent-sdk): let a run reuse a workspace prepared in standby WorkspaceState records whether the workspace is pending or prepared. A prepared workspace skips cloning, setup commands, and the file-based MCP scan. same_preparation lets the assignment path refuse a run that asks for a different workspace.
8 fbcc0c5 feat(agent-sdk): install a pre-warmed assignment on the standby driver prepare_prewarmed_assignment installs the run credential first, then installs the run: options, secrets, attachments, cloud providers, Bedrock, the shell and MCP environment, and the task binding. MCP servers get only the assignment's runtime environment, which is what a worker gives them in a normal run, and never the terminal's managed secrets. The team metadata refresh uses the same retry as a normal cloud run. An installation failure is reported against the run, because the sandbox cannot hand the run back.
9 133f95b feat(agent-sdk): run assigned tasks from a pre-warmed standby sandbox run_lifecycle waits in standby for the assignment, installs it, confirms delivery, and runs it. It holds an assignment that arrives before preparation finishes, ignores replays of the same assignment, and ignores unknown messages on the same connection.
10 96319d8 refactor(agent-sdk): identify a pre-warmed assignment by its execution ID The standby driver dedupes and confirms replayed assignments on execution_config.execution_id, and the client no longer reads an assignment ID. A sandbox gets at most one assignment and an execution gets at most one sandbox, so the separate ID was redundant.
11 0298a80 fix(server-api): skip non-text frames on the pre-warmed control socket Review fix. tungstenite gives server pings to the reader, so a ping dropped the socket and the sandbox reconnected about once a minute. The reader now skips frames that are not text.
12 66af38f fix(agent-sdk): continue a pre-warmed run when MCP servers fail to restart Review fix. The MCP restart result now goes through handle_mcp_startup_result, so a server that fails to restart degrades the run as it does in a cold run, unless strict MCP startup is set.
13 cbf6838 test(terminal): run the planned replacement baseline test only on Unix CI fix. The baseline test used the app context only on Unix, so Windows Clippy rejected the unused closure parameter. Windows refuses planned shell replacement, so the test now runs only on Unix.

Decisions and behavior changes

  • The run waits for delivery confirmation. Starting immediately and confirming in the background would start sooner, but it could run a task the server already cancelled. Confirmation retries transient failures for 5 minutes, then fails, so an unreachable server does not hold a run until its deadline.
  • Cloud providers are accepted. The server assigns runs that use them, and refusing an assignment now fails the run outright.
  • Secrets for agent run (commit 5). A typed auth secret is now skipped as a whole when one of its variables is set in environment_variables. Before, only the colliding variable was replaced.
  • No planned shell replacement on Windows. Pre-warmed sandboxes run only on Linux, so the Windows child exit watcher stays unchanged.
  • No standby shutdown report. The server learns that a sandbox stopped from its Namespace instance callback, and it has no sandbox shutdown endpoint. warp-agent-docker#281 calls harness-support report-shutdown --standby-id, which this PR does not add.

Review guide

Start with commits 5 and 8; together they hold most of the risk. In apply_run_configuration, check which runtime state survives an in-place assignment: the harness, the output format, and a prepared workspace. In prepare_prewarmed_assignment, check that nothing can fail before the run credential is installed, apart from parsing the task ID. In commit 2, the replacement refusal conditions in TerminalView::replace_shell decide when an assignment can install.

Linked Issue

Part of REMOTE-3002. This PR does not add the typed assignment context or the standby telemetry correlation that the issue describes. Plans: Align the pre-warmed sandbox client with the new server assignment contract · Address the automated review of PR #16539 · Trim the PR #16539 diff · Conversation

  • The linked issue is labeled ready-to-spec or ready-to-implement. The work is tracked in Linear, not in a GitHub issue.
  • Where appropriate, screenshots or a short video of the implementation are included below (especially for user-visible or UI changes). No user-visible UI changed.

Testing

On the branch tip:

  • cargo nextest run -p warp --lib -E 'test(prewarmed) | test(agent_sdk::) | test(control_reader)': 1018 passed.
  • cargo clippy --locked -p warp --all-targets --tests -- -D warnings: passed.
  • CI's wasm Clippy command, cargo clippy --locked --target wasm32-unknown-unknown --profile release-wasm-debug_assertions -- -D warnings: passed.
  • ./script/format: no changes.

The wider filter that adds test(shell_replacement) | test(shell_recovery) | test(terminal_manager) | test(templatable_manager) | test(local_tty::event_loop_tests) passed 1046 tests on the previous tip. The terminal and MCP manager code has not changed since then.

The standby tests cover an early assignment, replays, unknown messages, non-text frames, retirement before and after installation, Gone and transient failures during confirmation, and both deadlines. A mock-server test checks that an installation failure is reported against the run with the run's own credential. The standby bootstrap test runs oz agent run-cloud-standby against a mock server through readiness.

  • I have manually tested my changes locally with ./script/run. No live Namespace assignment ran, because the server side of the socket contract is not deployed.

Agent Mode

  • Warp Agent Mode - This PR was created via Warp's AI Agent Mode

CHANGELOG-NONE

Co-Authored-By: Warp agent@warp.dev

@cla-bot cla-bot Bot added the cla-signed label Oct 18, 2026
@warp-factories warp-factories Bot added the factory-ci-permission-pending Awaiting PR author consent for factory CI diagnosis and branch fixes label Oct 18, 2026
@warp-factories

Copy link
Copy Markdown
Contributor

@captainsafia, Warp CI failed on this PR's current head. May windows runner test diagnose it, commit fixes, and push to this same PR branch? Your consent also opts this PR into automatic handling of future CI failures. Please reply to this request mentioning @warp with explicit permission. We will not change the branch without it.

Responding as windows runner test: Open session · View in factory

@captainsafia
captainsafia marked this pull request as ready for review October 18, 2026 21:53
@warp-for-oss

warp-for-oss Bot commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

@captainsafia

I'm starting a first review of this pull request.

You can view the conversation on Warp.

I completed the review and no human review was requested for this pull request.

Comment /warp-agent-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

@warp-for-oss warp-for-oss Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Overview

This PR adds the client-side pre-warmed sandbox assignment flow, including control-socket assignment handling, in-place driver/shell/MCP environment replacement, and supporting terminal/standby tests.

Concerns

  • The planned shell replacement failure path in TerminalManager still invokes the old cloud-shell recovery failure callback before branching to the new replacement callback, which consumes the anyhow::Error and prevents the file from compiling.

Verdict

Found: 1 critical, 0 important, 0 suggestions

Request changes

Comment /warp-agent-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Comment thread app/src/terminal/local_tty/terminal_manager.rs
@warp-factories warp-factories Bot removed the factory-ci-permission-pending Awaiting PR author consent for factory CI diagnosis and branch fixes label Oct 18, 2026
captainsafia pushed a commit that referenced this pull request Oct 19, 2026
…16542)

## Description
Keep standby GraphQL credentials separate from run credentials.
Session-authenticated standby requests carry the sandbox ID and
pre-warmed workload token, not an ambient token without a run ID.

**Merge and deploy [server
#20185](warpdotdev/warp-server#20185) first.**
This client requires its standby middleware branch.

Startup selects standby identity from `--standby-id` before
authenticated background requests. Both identity header names are
reserved against configured overrides. The normal run-ID setter leaves
standby mode, so the separate [assignment client
PR](#16539) can reuse its
existing handoff. Workload-token issuance and caching remain shared.

## Linked Issue

[REMOTE-3568](https://linear.app/warpdotdev/issue/REMOTE-3568/authenticate-pre-warmed-standby-graphql-requests-with-sandbox-identity)
- [ ] The tracking issue is in progress in Linear; this task does not
use the GitHub ready-to-implement label.
- [ ] No rendered UI changes; staging validation follows both
deployments.

## Testing
- [x] `cargo nextest run -p warp_server_client --no-fail-fast`: 84
passed.
- [x] `cargo nextest run -p warp --no-fail-fast prewarmed`: 14 passed.
- [x] `cargo check -p warp --lib`
- [x] `cargo clippy -p warp_server_client --all-targets --tests -- -D
warnings` and the same command for `-p warp`.
- [x] `./script/format`
- [ ] `./script/presubmit`: all format/lint checks passed, including
workspace and default-app Clippy. Workspace nextest failed on sandbox
integration paths: gcloud/IAP credentials, foreground-window recording,
and unrelated pager/UI assertions. It did not reach doc tests. An
initial attempt also exhausted the Cargo volume; a retry disabled
incremental caching after removing generated caches.
- [ ] I have manually tested my changes locally with `./script/run`: not
run; authentication is tested at the transport boundary.

Unit tests use `WARP_ISOLATION_PLATFORM=docker` with a generated
synthetic `WARP_WORKLOAD_TOKEN` to avoid the sandbox's unavailable
Namespace issuer. Header tests cover exclusive standby credentials,
bootstrap exclusion, reserved-header filtering and the transition back
to run credentials.

After both changes deploy, verify the staging standby reaches `ready`,
heartbeats advance, and no sandbox retires with `provider stopped`. This
end-to-end check is pending, not a local test result.

CI is intentionally not green while this PR is a draft:
`.github/workflows/ci.yml` skips `params` on drafts, and the aggregate
job rejects that skip. Marking ready must run the real CI jobs before
merge.

## Agent Mode
- [x] Warp Agent Mode - This PR was created via Warp's AI Agent Mode

CHANGELOG-NONE

<!-- warp:pr-description-artifacts start -->
_Plans_:
- _[Standby GraphQL credential
isolation](https://staging.warp.dev/drive/notebook/DwxWynuqJiRIxAdv6m1L2D)_
<!-- warp:pr-description-artifacts end -->

Co-authored-by: warp-agent-staging[bot] <240773466+warp-agent-staging[bot]@users.noreply.github.com>
@captainsafia
captainsafia force-pushed the safia/prewarmed-assignment-client branch from 65b1a8d to d2e6494 Compare October 19, 2026 21:28
captainsafia and others added 10 commits October 19, 2026 15:00
Add `Message::ShutdownForReplacement` so the PTY event loop can stop a
shell without marking the terminal model exited. The terminal keeps its
model and view, and a replacement shell can attach to them.

On Windows, `ChildExitWatcher` now owns its callback context and its
`Drop` waits for an in-flight exit callback with `UnregisterWaitEx`.
Without this, a replaced shell's delayed exit notification could reach
the event loop of the shell that replaced it. This applies to every
Windows terminal, not only to replaced ones.

Co-Authored-By: Warp <agent@warp.dev>
Add `TerminalView::replace_shell`, which restarts the shell behind an
existing terminal with a new working directory and environment. It
resolves after the replacement shell bootstraps.

- `TerminalView` accepts a replacement only for an idle local shell: it
  refuses while a command runs, the alt screen is active, the terminal is
  read-only or a shared-session viewer, or a recovery or replacement is
  already pending. Input stays disabled until the request settles, and a
  request that does not settle within 75 seconds fails.
- The pane routes `Event::ReplaceShell` and `Event::CancelShellReplacement`
  to the local terminal manager. Other terminal managers refuse.
- The local terminal manager stops the old event loop with
  `ShutdownForReplacement`, then spawns the new shell on the cloud
  shell-recovery resources. The old shell's input and exit notifications
  cannot reach the new one.
- `TerminalModel::prepare_shell_replacement` keeps the planned exit of the
  old shell from ending the terminal.

Co-Authored-By: Warp <agent@warp.dev>
…ment

Add a runtime environment to `TemplatableMCPServerManager` that overrides
the static environment of every CLI server it spawns. Values come only from
the caller and are never persisted with the installation.

`replace_runtime_environment` installs a new runtime environment, stops
every running or starting server, and returns the installations to restart
together with a completion receiver for each shutdown. CLI-spawned and
built-in servers are stopped but not returned; the run's own MCP startup
starts them again. WASM keeps the environment and starts nothing.

`shutdown_server` now delegates to `shutdown_server_and_wait`, which
reports when the server reaches `NotRunning`.

Co-Authored-By: Warp <agent@warp.dev>
Replace the HTTP heartbeat with the server's control socket at
`/internal/prewarmed-sandboxes/{id}/ws?generation=`. The socket
authenticates with the bearer credential and the Namespace workload token,
and both headers are marked sensitive.

- The sandbox sends only `heartbeat`. The server sends `assignment`, which
  carries the committed execution configuration, runtime environment, run
  API key, and run deadline, or `retired` before it closes the socket. The
  assignment's `Debug` output redacts the run key and the configuration
  values.
- A `410 Gone` handshake is reported through
  `is_prewarmed_sandbox_unavailable`, so callers can tell a retired sandbox
  or an inactive run from a transient failure.
- The status report sends the generation as `fingerprint`, which is the
  field the server binds. It sent `generation` before, so every readiness
  report was rejected.
- `testdata/prewarmed_assignment_fresh_run.json` is the server's own
  fixture, so the decode test checks what the server actually sends,
  including the `factoryBranch` and `expectedSha` fields.

Co-Authored-By: Warp <agent@warp.dev>
`split_run_options` is now the only code that destructures
`AgentDriverOptions`. It returns two values:

- `TerminalLaunchInputs`: the options consumed when the terminal session
  is created, which have no driver state.
- `RunConfiguration`: the driver state, with every optional override
  resolved to its default and the terminal environment fully built.

`AgentDriver::create` builds the terminal, then calls
`with_terminal` and `apply_run_configuration`. A driver that already owns
a terminal can call `apply_run_configuration` to take a new run in place.
A new option field now fails the build until it is handled in one place,
instead of in each path that applies options.

`build_secret_env_vars` takes the terminal environment that is applied on
top of the secrets. Behavior changes for `create`:

- A typed auth secret is skipped as a whole when any of its variables is
  set in `environment_variables`. Before, the environment variable replaced
  only the colliding name and left the rest of the secret in place.
- A raw `WARP_METADATA_*` secret that collides with `environment_variables`
  is an error, as it already was for the process environment.
- Login is checked before options are resolved, so a logged-out caller gets
  `NotLoggedIn` rather than a configuration error.

Co-Authored-By: Warp <agent@warp.dev>
A pre-warmed assignment arrives as an execution configuration, with no
`RunAgentArgs`. These run-setup steps now accept that case, so the
assignment path uses the same code as `agent run` instead of a copy:

- `build_execution_task_and_options` takes `Option<&RunAgentArgs>`.
  Without arguments, the task and execution IDs come from the
  configuration, and the harness transport, snapshot timeouts, and MCP
  startup settings use their defaults.
- `configure_bedrock_oidc_credentials` switches the run to OIDC-federated
  AWS credentials and waits for the first set. `agent run` now calls it in
  place of its inline copy.
- `bootstrap_git_credentials_for_task` and `set_ambient_agent_task_id` are
  generic over the calling entity. Git bootstrap takes the
  `--configure-git-credentials-with-github` value instead of the whole
  arguments struct.

`agent run` keeps its behavior.

Co-Authored-By: Warp <agent@warp.dev>
Replace the driver's `Option<WorkspaceConfiguration>` with
`WorkspaceState`, which records whether the workspace is pending or was
already prepared by a standby sandbox.

- `prepare_resources` leaves `WorkspaceState::Prepared`, which keeps the
  prepared configuration and the repositories to scan for skills.
- `AgentDriver::run` matches on the state once. A prepared workspace skips
  the global skill clone, the Factory definition clone, environment
  preparation, and the file-based MCP scan, and it reuses the harness
  directory recorded at preparation. A pending workspace keeps the existing
  setup unchanged.
- `apply_run_configuration` keeps a prepared workspace in place.
- `WorkspaceConfiguration::same_preparation` compares the source
  repositories, setup commands, and clone requests, so a caller can check
  that a run asks for exactly what was prepared before it reuses it.

Co-Authored-By: Warp <agent@warp.dev>
Add `AgentDriver::prepare_prewarmed_assignment`, which turns the standby
driver into the driver for one assigned run, in the same process and
terminal. The server commits the run before it delivers the assignment,
so the sandbox cannot hand a run back. The run's own credential is
installed first, and any later installation failure is reported against
the run with `report_driver_error`.

Installation, in order:

1. Install the run API key and the ambient task ID.
2. Check that the assignment is an Oz run the client can prepare. Cloud
   providers are accepted, because the server assigns runs that use them.
3. Bootstrap Git credentials, refresh workspace metadata, and resolve
   configured skills through the shared run-setup helpers.
4. Build the run options with `build_execution_task_and_options`, and
   refuse a workspace that differs from what standby prepared.
5. Load task secrets, attachments, cloud providers, and Bedrock credentials
   as `agent run` does.
6. Apply the options with `split_run_options` and
   `apply_run_configuration`, then replace the shell and the MCP runtime
   environment with the run's environment.
7. Bind the terminal to the task and share the session when requested.

Supporting changes:

- `TerminalDriver::bind_prewarmed_task` sets the window team, the
  controller's task and attachments directory, and the shared-session
  source, then starts sharing. The share senders move onto
  `TerminalDriver`, so a binding after creation can still resolve them.
- `TerminalDriver::replace_shell` wraps the terminal view's replacement.
- `replace_prewarmed_mcp_environment` restarts retained MCP servers with
  the run environment and waits for them to start.
- `sandbox_deadline` becomes a driver field, so the run's deadline replaces
  the standby deadline for the shutdown timer and MCP headers.
- `UserWorkspaces::set_window_team_uid` accepts an optional team, so a
  personal run can clear the standby window's team.

Co-Authored-By: Warp <agent@warp.dev>
A standby sandbox used to prepare, heartbeat over HTTP, and exit at its
deadline. It now waits on the control socket for the server to commit a
run to it, installs that run, and runs it in the same process.

The lifecycle has three phases:

- Standby: prepare, report readiness, and heartbeat until an assignment
  arrives. An assignment that arrives before preparation finishes is held
  until it does, because the ready report's response can race the
  assignment frame. A replay of the same assignment is ignored, and a
  different one is an error. `retired`, or the standby deadline, exits
  cleanly.
- Install: drop the standby socket and install the assignment, bounded by
  the run deadline.
- Confirm delivery: reconnect with the run's own credential. The server
  admits that connection, revokes the standby credential, and replays the
  assignment; the replay confirms delivery. `410 Gone` or `retired`
  means the run is no longer active, so the sandbox exits without starting
  it. Transient failures retry for up to 5 minutes, so an unreachable
  server fails the run promptly instead of holding it until its deadline.

The run starts only after delivery is confirmed, so a cancelled or
superseded run never starts. The test fixtures move to
`standby_test_support.rs`.

Co-Authored-By: Warp <agent@warp.dev>
…n ID

The assignment ID was redundant, because a sandbox gets at most one
assignment and an execution gets at most one sandbox. The standby
driver now dedupes and confirms replayed assignments on
execution_config.execution_id.

Co-Authored-By: Warp <agent@warp.dev>
@captainsafia
captainsafia force-pushed the safia/prewarmed-assignment-client branch from d2e6494 to 96319d8 Compare October 19, 2026 22:02

@vorporeal vorporeal left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

read through all of the commits, and had an agent read the full PR, leave some comments from its own findings, and leave some more from me asking it questions as i read.

approving to unblock, but i think there's a bunch of stuff in here that's worth doing (whether it's in this PR or in a follow-up is up to you).

let pending = self.pending_shell_recovery.take();
self.view.update(ctx, |surface, ctx| {
surface.on_cloud_shell_recovery_failed(failure_class, error, ctx);
if let Some(request_id) = pending.and_then(|pending| pending.planned_request_id) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

pending review comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

oops created this so there was a pending review that the agent could attach additional comments to and then i forgot to delete it

Comment on lines +693 to +696
anyhow::ensure!(
!cfg!(windows),
"shell replacement is not supported on Windows"
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

i assume this is more of a TODO than a "we cannot support this"? (otherwise, we're saying that we can't do pre-warmed sandboxes on windows, right?)

restored_working_directory: String,
used_fallback_directory: bool,
replacement_session_id: SessionId,
planned_request_id: Option<Uuid>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this isn't associated with any sort of "request", right? should this be planned_replacement_id?

}

/// A shell replacement that has not finished bootstrapping.
struct PendingShellRecovery {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

might be worth a quick rename of this, given we're now using it for more than just recovery. perhaps PendingShellReplacement? (or PendingShellRespawn?)

return Ok(None);
};
let message = message.context("Pre-warmed sandbox control stream failed")?;
let text = message

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[agent] The server pings this socket every 54s (socketPingPeriod in warp-server logic/ai/ambient_agents/prewarmed/socket.go, gorilla/websocket). tungstenite 0.24 queues the pong and also returns Message::Ping to the stream consumer, so this text() check turns every server ping into an error. maintain_standby_connection then drops the socket and reconnects after 1s. So every standby sandbox tears down and reopens its socket about once a minute for its whole life, re-running auth each time, with nothing logged.

It also stretches the heartbeat cadence to ~55s, because the 30s heartbeat timer restarts on each new connection. The server only treats a sandbox as an assignment candidate if it heartbeated within 90s (candidateHeartbeatWindow in assignment.go), so a single missed heartbeat at that cadence makes the sandbox temporarily unassignable.

Delivery confirmation is not affected, since the server replays the assignment immediately on connect, well before the first ping.

Could this skip non-text frames instead, e.g. loop with let Some(text) = message.text() else { continue };, the way the shared-session code does in app/src/terminal/shared_session/sharer/network.rs:1521? A Close frame would then end the stream normally on the next read. A test with a fake stream that yields a Ping and then a Text message would cover it.

let (global_skill_resolution, prepared_workspace) = match workspace
.ok_or(AgentDriverError::InvalidRuntimeState)?
{
// A standby sandbox already cloned the repositories and ran the setup

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[agent] FYI, not blocking: this branch does no refresh after the assignment arrives (no fetch/checkout of the prepared repos, no Factory definition checkout, and global skills are only resolved, not cloned), so a pre-warmed run works on branch heads as of standby preparation. I understand a post-assignment refresh is planned; two things to keep in mind when it lands:

  • Global skill repos are separate checkouts from the workspace repos, so the refresh needs to cover them too, or a global skill repo added/updated after standby will be missing or stale.
  • Repos can carry expected_sha, and same_preparation compares clone_requests, which include it. If the server ever sends an assignment whose pinned SHA differs from what standby prepared, the run (already committed) fails at same_preparation. A refresh that checks out the expected SHA, with same_preparation ignoring the SHA, would be the more forgiving behavior.


/// Installs a committed assignment on this standby driver and returns the task to run.
///
/// The server commits the run before delivering it, so the sandbox cannot hand it back and

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[agent] Question about the overall failure model, since cloud agent reliability is a top priority right now: because the sandbox can't hand a committed run back, every failure between the assignment and the run actually starting turns into a failed run, even though nothing has executed yet. That includes installation failures here (a retained MCP server failing to restart, same_preparation rejecting the workspace, the shell replacement being refused or timing out, secrets or Bedrock setup failing), plus the post-install exits in run_lifecycle (deadline during installation, delivery not confirmed within the window). In all of those cases a fresh cold sandbox would very likely have succeeded.

Could these pre-start failures fall back to a cold start instead of failing the run? This doesn't need an accept/decline step in the protocol. Roughly:

  • the sandbox reports "installation failed before the run started" as a distinct outcome (e.g. a specific status or error class on the run, or a socket/HTTP call), using the run key it already holds;
  • the server treats that as "release this execution back to pending and retire the sandbox", then dispatches the run cold;
  • retries are capped (e.g. at most one pre-warmed attempt per execution, then cold only) so a bad config can't loop.

The task never reaching IN_PROGRESS before these failures seems like a reasonable server-side signal that requeuing is safe. Is this something you've considered, or planned as a follow-up? It would need matching server work in warp-server's prewarmed assignment code.

Comment on lines +302 to +304
type ControlSink = Pin<Box<dyn futures::Sink<websocket::Message, Error = websocket::Error> + Send>>;
type ControlStream =
Pin<Box<dyn futures::Stream<Item = Result<websocket::Message, websocket::Error>> + Send>>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: you can leverage warpui::r#async::BoxFuture to simplify the types here.

Comment on lines +116 to +118
fn sharing_expected(should_share: bool) -> bool {
should_share && !warp_core::channel::ChannelState::server_root_url().contains("ngrok")
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

should should_share be overwritten at (or at least closer to) the source based on this ngrok check? helps ensure that all consumers get it right.

result
}

async fn install_assignment(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[agent] Installation currently runs every step one after another, but most steps don't depend on each other. Since installation latency is the whole point of pre-warming, it seems worth restructuring this into a few clearly ordered groups, where each group runs in parallel and steps are serialized only where a real dependency exists.

Dependencies (after the run credential and task ID are installed, which should stay first):

Step Kind Needs Needed by
Validate payload; derive team scope, share requests, skill specs, Bedrock role/region, runtime_env (+ key, deadline), Factory contract pure/sync payload everything
Fetch task secrets network task ID + key shell (via build_secret_env_vars)
Fetch + download attachments (incl. handoff) network task ID + working_dir task prompt only
Bootstrap Git credentials network + git config write task ID the run itself (nothing in installation)
Refresh team metadata (up to 30s of retries) network key building options (conservatively), bind, run
Mint Bedrock OIDC credentials network task ID + team scope (both known from the payload) the run
Resolve configured skills local only (clone_repositories = false) prepared checkouts building options
Build options/task, same_preparation, cloud providers, split_run_options + apply_run_configuration sync, main thread skills, secrets, attachments, Bedrock config shell, bind
Replace shell up to 75s resolved_env_vars (depends on secrets) sharing
Restart retained MCP servers (stop up to 20s, start up to 20s) process spawn only runtime_env nothing in installation
Bind task + start sharing + wait for share network, up to 20s team scope; should follow the shell replacement the run

The notable ones:

  • MCP depends only on runtime_env, which is known right after parsing the payload. Its one small link is wait_for_mcp_servers_started reading me.mcp_startup_timeout, which is the default either way and could be passed explicitly.
  • All network steps need only the credential. Attachments and Bedrock currently write into options/task as they go; they'd need to return results instead.
  • Sharing should follow the shell replacement, but waiting for the share doesn't need to block the MCP restart.
  • The cold path already does this kind of thing: fetch_secrets_and_attachments fetches secrets, attachments, metadata, and the handoff snapshot with one futures::join!.

Suggested shape:

async fn install_assignment(...) -> Result<Task, AgentDriverError> {
    // 0. Pure: validate and derive everything from the payload. No I/O.
    let plan = AssignmentPlan::from_payload(config, runtime_env, api_key, expires_at)?;

    // The one step with no inputs beyond the payload, and the slowest: start it now, join it last.
    let mcp_restart = Self::replace_prewarmed_mcp_environment(spawner, plan.mcp_environment.clone());

    // 1. Network: everything that only needs the run credential.
    let (secrets, attachments, (), (), bedrock) = futures::try_join!(
        fetch_task_secrets(spawner, task_id),
        fetch_and_download_attachments(ai_client, server_api, task_id, &attachments_dir),
        AgentDriverRunner::bootstrap_git_credentials_for_task(spawner, ..),
        AgentDriverRunner::refresh_team_metadata(spawner, true),
        mint_bedrock_credentials(spawner, task_id, &plan.team_scope, plan.bedrock),
    )?;

    // 2. Sync, on the main thread: resolve skills (local), build options/task, check
    //    same_preparation, load cloud providers, attach secrets/attachments/bedrock, then
    //    split_run_options + apply_run_configuration.
    let (task, should_share) = spawner.spawn(move |me, ctx| me.install_run_configuration(..)).await??;

    // 3. Shell replacement (needs resolved_env_vars from step 2).
    Self::replace_shell_with_resolved_env(spawner).await?;

    // 4. Sharing (after the shell) overlaps with the rest of the MCP restart.
    futures::try_join!(Self::bind_and_share(spawner, task_id, plan.share, should_share), mcp_restart)?;
    Ok(task)
}

If you'd rather keep this strictly "groups, each a join", the MCP restart can go in step 3 next to the shell replacement. That's simpler, but gives up overlapping MCP with the network group. Starting it early seems worth it: it's the only step with no inputs and usually the slowest, and one future started early and joined at the end stays easy to follow.

Roughly, the critical path goes from the sum of everything (five network round trips + shell + MCP stop/start + share wait) to the slowest network call + the shell replacement + whichever is longer of the share wait and the remaining MCP time. That's an estimate from reading the code, not a measurement.

Notes:

  • Use try_join! so the first error cancels the rest; dropping a half-finished MCP restart or shell replacement is fine, since a failed installation ends the process.
  • I didn't confirm whether building options actually reads team metadata. The shape above doesn't depend on it, because step 2 waits for all of step 1. If it doesn't, the refresh could move off the critical path entirely (joined before bind).
  • This pairs with the MCP comment on mcp_startup.rs: if MCP restart failures degrade instead of failing the run, joining the MCP restart last makes a degraded result just a warning.
  • Nothing else waits on the MCP manager during installation (the next wait is in run()), so the overlapping-waits warning on wait_for_mcp_servers_started isn't an issue, as long as that stays true.

@captainsafia
captainsafia force-pushed the safia/prewarmed-assignment-client branch from 96319d8 to e86e6f3 Compare October 20, 2026 04:29
captainsafia and others added 2 commits October 19, 2026 21:32
tungstenite hands server pings to the reader, so a ping dropped the socket and the sandbox reconnected about once a minute.

Co-Authored-By: Warp <agent@warp.dev>
…start

Route the restart result through handle_mcp_startup_result so a failed restart degrades the run as it does in a cold run, unless strict MCP startup is set.

Co-Authored-By: Warp <agent@warp.dev>
@captainsafia
captainsafia force-pushed the safia/prewarmed-assignment-client branch from e86e6f3 to 66af38f Compare October 20, 2026 04:32
The test uses the app context only for Unix model events, so Windows Clippy rejected the unused closure parameter. Windows refuses planned shell replacement, so the test has no Windows behavior to cover.

Co-Authored-By: Warp <agent@warp.dev>
@captainsafia

Copy link
Copy Markdown
Contributor Author

@vorporeal Thanks! I'll take advantage of the approval to unblock being able to test the end-to-end in the next nightly. I've addressed a few of the impact impact items in the PR here and will circle back with the others in follow-ups.

@captainsafia
captainsafia merged commit c67ff19 into master Oct 20, 2026
31 checks passed
@captainsafia
captainsafia deleted the safia/prewarmed-assignment-client branch October 20, 2026 05:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants