Skip to content

[REMOTE-2661] Allow a debug agent in a retained setup-failure session (warp client) - #14916

Merged
dmichelin merged 26 commits into
masterfrom
factory/remote-2661-warp-client
Sep 2, 2026
Merged

[REMOTE-2661] Allow a debug agent in a retained setup-failure session (warp client)#14916
dmichelin merged 26 commits into
masterfrom
factory/remote-2661-warp-client

Conversation

@warp-agent-staging

@warp-agent-staging warp-agent-staging Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Description

What: An authorized user can run a debug agent inside the retained sandbox of a cloud run that failed during environment setup. The debug agent does not reopen the run and does not clear its failure record. A silent agent turn does not consume the idle timer.

Why: A run that fails during setup keeps its session alive for a bounded debug window (REMOTE-2208). But there is no way to get an agent into that session. If you start an agent the ordinary way, it creates a fresh conversation and flips the run back to "in progress". That destroys the failure record that you want to debug.

How: A no-token prompt against a retained, eligible session bootstraps a debug conversation through the server's authenticated follow-up path, not the direct viewer path. The client suppresses task-state reporting for that conversation, so the conversation cannot overwrite the failure record. Each debug turn pins the idle timer for its duration, so an autonomous agent does not lose its own session.

Server companion: warp-server#14231. Deploy the server PR first. Against an older server, the client fails closed (debug_agent_available defaults to false). Thus an early client deploy is inert, not wrong.

Reviewing guide

Suggested order:

  1. task.rs — Start with is_setup_failure_debug_session_open. All downstream code trusts this one field.
  2. cloud_conversation_continuation.rs — Read resolve_ai_query_routing and the new routing variant. Check the priority order (retained-debug check before live-viewer check).
  3. terminal_view_adaptor.rs — Read the AgentPromptRequested handler as a whole, not as a diff, because a brace moved. This handler is the security boundary.
  4. driver.rs — Read DebugWindowController and arm_setup_failure_debug_window. This is the most complex state machine in the PR. Check the pin and unpin transitions against the ConversationStatus variants.
  5. conversation.rs / local_agent_task_sync_model.rs — Read TaskSyncMode::PreserveTerminalSetupFailure and the locations that set and read it.
  6. input.rs / slash_commands/mod.rs — Read the submission entry points. Check that /agent and plain input resolve to the same routing.
  7. The remaining files (footer indicator, GraphQL schema and query, tests) are plumbing for items 1-6.

Architecture overview

Six pieces, each with one job:

  1. Eligibility (AmbientAgentTask::is_setup_failure_debug_session_open, task.rs) — The client trusts one server-computed boolean: debug_agent_available. The server knows the failure state, error code, feature flags, and window status from the same snapshot. The client does not re-derive this data.
  2. Routing (cloud_conversation_continuation.rs) — Adds the AIQueryRouting::RetainedSetupFailureDebug variant. The resolver checks it before ordinary live-viewer routing. Thus every entry point (plain input, /agent, an attached viewer) converges on the authenticated follow-up service. No entry point uses the direct viewer-prompt path or a local conversation.
  3. Sharer authorization (terminal_view_adaptor.rs + the new setupFailureDebugAuthorization GraphQL query) — The sharer cannot authenticate the requester itself. Thus the sharer honors a no-token prompt only after an explicit true from the server. There is no local fallback.
  4. Idle-timer pin (driver.rs, DebugWindowController) — Runs inside the agent process. A debug turn pins the idle window open for its duration and re-arms it on completion. This is necessary because no viewer keystroke refreshes the window during an autonomous turn.
  5. Task-state suppression (TaskSyncMode in conversation.rs, local_agent_task_sync_model.rs) — The debug conversation reports its ID (the injection and transcript APIs need it). But the conversation never derives task state from its own status. Thus it cannot overwrite the original failure record. The server independently guards the same rule.
  6. Atomic failure report (driver.rs) — The driver sends the terminal failure state, the status message, and the first debug deadline in one server update. Thus the server never sees a failure without a debug-window deadline.

Testing

Unit tests cover:

  • DebugWindowController pin, re-arm, and idempotence

  • The sharer authorization decision (accept, deny, error, no token)

  • Routing eligibility gated on the real debug_agent_available field, with team-owned runs included

  • A later prompt that reuses the existing debug conversation

  • TaskSyncMode suppression of task-state derivation.

  • I have manually tested my changes locally with ./script/run

Agent Mode

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

Implements the warpdotdev/warp (Rust client) half of REMOTE-2661: a
debug agent can be started inside a retained post-setup-failure
session without reopening the run, clearing its failure record, or
losing the idle timer to a silent agent turn.

- Add a `DebugWindowController` in `AgentDriver` that pins the
  post-failure idle window for the duration of a debug turn and
  re-arms the full interval on any terminal conversation outcome,
  idempotent by turn (conversation) id.
- Tag a debug conversation bootstrapped with no server token into a
  retained setup-failure session with
  `TaskSyncMode::PreserveTerminalSetupFailure`, so
  `LocalAgentTaskSyncModel` keeps reporting its conversation id but
  never derives task lifecycle state from it.
- Add `AIQueryRouting::RetainedSetupFailureDebug` so both the
  tombstone's own input and an already-attached live viewer submit
  through the authenticated run follow-up service instead of the
  direct viewer prompt path or the local agent.
- Reject a no-token agent prompt from a viewer on the sharer side
  while retaining a setup-failed run, closing the bypass an old or
  modified viewer could use to start a conversation directly.
- Add a "Debug" tombstone CTA for an authorized caller when the
  client-side eligibility proxy and an open debug window are met.

Co-Authored-By: Warp Agent <agent@warp.dev>
@cla-bot cla-bot Bot added the cla-signed label Aug 11, 2026
@warp-agent-staging
warp-agent-staging Bot requested a review from dmichelin August 11, 2026 00:03
oz-agent and others added 3 commits August 11, 2026 01:01
…#14231)

Switches from the client-side eligibility proxy to the server-computed
debug_agent_available capability now returned by GET /agent/runs/{id}
and the list endpoint (warp-server#14231), and reads the paired
status_message.debug_agent_active display flag. Both default to false
via #[serde(default)] so an older server fails closed.

is_open_for_setup_failure_debug_bootstrap now ANDs debug_agent_available
in alongside the existing client-observable conditions; the server
remains the sole authority regardless. Adds end-to-end coverage proving
the debug tombstone CTA and AIQueryRouting::RetainedSetupFailureDebug
only activate when the real field is true.

Co-Authored-By: Warp Agent <agent@warp.dev>
Adds `debugAgentActive` as a top-level field on `UpdateAgentTaskInput`
(sibling to `sessionDebugUntil`, confirmed against the server's
implementation on factory/remote-2661-server-setup-failure-debug),
threaded through the AIClient trait, the cynic mutation type, and the
vendored GraphQL schema mirror.

DebugWindowController's pin/unpin transitions in
arm_setup_failure_debug_window now publish Some(true)/Some(false)
alongside the deadline; the throttle on publish_debug_window_deadline
no longer suppresses a debug_agent_active transition, since a pin/unpin
is a discrete state change rather than a sliding value. All other
update_agent_task call sites pass None (untouched).

Co-Authored-By: Warp Agent <agent@warp.dev>
Replaces the sharer's flawed local-cache/is_sharer heuristic for a
no-token REMOTE-2661 debug bootstrap prompt with a real server
authorization callback, per the review's decision.

- Adds the setupFailureDebugAuthorization GraphQL query (schema mirror
  + cynic query type) matching the server's implementation on
  factory/remote-2661-server-setup-failure-debug: input {taskId,
  workloadToken, participantFirebaseUid} -> {authorized: Boolean!}.
- Adds AIClient::setup_failure_debug_authorization and wires it into
  ServerApi.
- The sharer resolves the requesting participant's firebase_uid from
  the already-local session-sharing presence list
  (PresenceManager::viewer_firebase_uid), issues its own workload
  token, and calls the server before honoring a no-token prompt
  against an eligible retained setup-failure session. Every outcome
  other than an explicit Ok(true) — unresolvable UID, denial,
  UserFacingError, or a failed call — rejects the prompt. There is no
  local fallback.
- Extracts the authorization decision into
  is_setup_failure_debug_prompt_authorized, a pure async function
  testable independent of workload-token issuance (which isn't
  mockable in this test harness), with 5 regression tests covering:
  authorized principal accepted, unauthorized participant rejected,
  server error rejected, unresolvable participant rejected without
  calling the server, and failed workload-token issuance rejected
  without calling the server.

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

Copy link
Copy Markdown
Contributor Author

End-to-end verification: 4/4 required states proven against a real local stack

Run 019fef07-4c18-7a53-a088-48c9d37ea4f5, environment q1ZcbtEBf2wIqCvcHvFYeo.

1. Setup fails, run FAILED with its message. state=FAILED, error_code=environment_setup_failed, message="Environment setup failed: Failed to run setup command: ls /workspace/remote-2661-missing-dependency-dir", session_debug_until=04:44:28, retained_debug_execution_id=6, debug_agent_available=true, is_sandbox_running=true. The retained agent process stayed alive; its log shows idle_timeout_scheduled timeout=1800s outcome=setup_failure.

2. Debug prompt reaches an agent in the broken environment. POST /agent/runs/{id}/followups with {"message":"/agent why did my setup fail"} returned HTTP 200 at 04:15:14. The timeline then recorded setup_failure_debug_started (04:15:14.759, exec=6) and setup_failure_debug_finished (04:15:15.633, exec=6). These events are produced by setupFailureDebugTimelineEventType mapping the agent's own reported task state (IN_PROGRESS → started, SUCCEEDED → finished), so their presence means the agent genuinely received the prompt and ran a turn, and the server converted its status reports into debug events rather than lifecycle transitions.

3. Run still FAILED afterwards, message and code intact. Sampled continuously and again 8 minutes later: state=FAILED, error_code=environment_setup_failed, message byte-identical, retained_debug_execution_id still 6. The timeline contains no second oz_run_claimed and no second oz_run_failed — the debug turn produced only non-lifecycle debug events. The terminal state was never rewritten.

4. Timer refreshes, pins, re-arms. Sampled every 2s: before, until=2026-08-11T04:44:28.057911817Z active=false; on acceptance at 04:15:15, until=2026-08-11T04:45:15.054934977Z active=true (refreshed and pinned); after at 04:15:17, until=2026-08-11T04:45:16.400373102Z active=false (re-armed, later still).

Reproduction. Branches: warp-server factory/remote-2661-server-setup-failure-debug, warp factory/remote-2661-warp-client. Client built with cargo build --bin warp --features gui and WITH_LOCAL_SERVER=1 WITH_LOCAL_SESSION_SHARING_SERVER=1 WARP_CLOUD_MODE_DEFAULT_HOST=local-dev, after ./script/install_channel_config (must log channel: local). config/local.yaml needs failure_session_debug_agent: true; failure_session_retention was already true; both are required. Stack: ./script/oz-local up --detach --wait --oz-path /workspace/warp/target/debug/warp --worker-dir /workspace/oz-agent-worker --session-sharing-dir /workspace/session-sharing-server -e WARP_ISOLATION_PLATFORM=docker_sandbox. Environment created via upsertCloudEnvironment with failureSessionRetentionMinutes: 30 and setup commands ending in one that fails without exiting the shell. Run submitted via POST /api/v1/agent/runs with worker_host: "local-dev".

Scaffolding — everything stubbed or patched, none of it in the feature's path.

  1. Firebase user directory stubbed (FIREBASE_AUTH_EMULATOR_HOST, serving only accounts:lookup), because profile.needsSsoLink forces an uncached Identity Toolkit call this sandbox is denied. Ambient auth infrastructure; no feature code changed.
  2. LLM mocked via /debug/llm-mock for determinism — no provider credentials exist in the sandbox. This shapes the content of the agent's answer, not whether a turn ran or what the run state did.
  3. WARP_ISOLATION_PLATFORM=docker_sandbox passed to tasks, correcting a misdetection: the runner is itself a Namespace sandbox, so the agent saw /var/run/nsc/token.json and shelled out to nsc instead of using the WARP_WORKLOAD_TOKEN the server already provided.
  4. session-sharing-server/script/server shebang #!/bin/sh#!/bin/bash (sibling checkout, neither PR) — bash-only syntax meant args were silently dropped and --features local_warp_server never applied.
  5. session-sharing-server/script/bootstrap gcloud components install pubsub-emulator made conditional.
  6. Fixture API key flipped to a user principal in the local DB so upsertCloudEnvironment would accept it.
  7. An ADC wrapper so the sandbox's credential helper keeps seeing the platform key while the stack uses the local fixture key.
  8. Root-owned CARGO_HOME and target/ chowned; net.git-fetch-with-cli=true.

Items 4–8 are environment plumbing. Items 1–3 are the only ones touching a runtime path, and none is inside the retained-session or debug-window logic under verification.

Why there is no screenshot. The Oz web app could not be reached honestly. The app shell serves at /agents.html on the Vite dev server, and a browser session could be authenticated by minting a session cookie the Admin SDK accepts in emulator mode — /auth/session returned authenticated: true. But the app's Apollo client authenticates through apollo-auth-wrapper.firebase.tsx, i.e. Firebase client-side sign-in against the real staging project, for which no account exists here. Making the UI show real data would have required patching that wrapper as a third layer of scaffolding on top of a stubbed directory and a forged cookie, at which point the capture stops being evidence. MSW mode would render instantly but shows mocked data, which would be a fabricated capture. The verification therefore rests on API and database observation of a genuinely running stack. A rendered capture is cheap on any deployment where Firebase auth works normally; the feature itself needs nothing further.

oz-agent and others added 6 commits August 11, 2026 05:58
Fixes a defect found in live verification: warp-server can redeliver
the same logical no-token setup-failure debug bootstrap prompt (same
idempotency key) when an earlier delivery's acknowledgement is lost,
e.g. on the warp-server <-> session-sharing-server hop. The sharer had
no way to recognize a redelivery, so each one created an independent
conversation, and BlocklistAIController::send_query's existing
cancel-active-conversation-before-send behavior meant each new
delivery cancelled the previous one's in-flight stream. A participant
watching the first conversation that appeared would see it go dead
while the real response eventually landed in a different, invisible
conversation -- or never land at all if redeliveries kept arriving
faster than one attempt could finish.

Fix: record the local conversation created for a task's no-token
bootstrap prompt (SharedSessionState::pending_setup_failure_debug_bootstraps),
and reuse it for a later no-token submission on the same task rather
than starting a new one. The lookup is self-invalidating: it only
returns a hit while the recorded conversation is still live and has
not yet been assigned a server conversation token. Once tokened, the
bootstrap has durably succeeded, so a later no-token submission is a
distinct request, not a retry, and correctly falls through to starting
fresh.

Verified safe to reuse even while the recorded conversation's prior
attempt is still streaming: send_query already cancels whatever is
the terminal surface's active conversation before sending a new query
to it -- the same handling an ordinary "user sends a second message
before the first finishes" submission gets. Reuse only changes which
conversation a repeated bootstrap prompt lands in, never whether an
in-flight attempt gets cancelled, and this decision happens
synchronously on the app's single event loop, so there is no window
for a concurrent handler to observe a half-cancelled conversation.

Extracted the reuse decision into a pure `reusable_bootstrap_conversation`
function (mirroring the is_setup_failure_debug_prompt_authorized
pattern from the finding-1 fix) since BlocklistAIController has no
existing test harness in this codebase to exercise the full wiring
through. Added 4 regression tests covering: no prior conversation
(first delivery), a retry reusing an untokened conversation, a tokened
conversation never being reused, and a vanished/dropped conversation
never being reused.

Also makes a redelivery observable per review request: reuse and
fresh-start both log an info line naming which happened.

Co-Authored-By: Warp Agent <agent@warp.dev>
Adds a test proving the actual property the bootstrap-retry defect
broke: cancelling an in-flight exchange with
CancellationReason::FollowUpSubmitted { is_for_same_conversation: true }
(exactly what BlocklistAIController::send_query does when a retry
reuses the recorded conversation) keeps the conversation InProgress
rather than visibly flipping it to Cancelled, and the retry's own
exchange -- submitted to that same conversation -- runs to completion
and reports Success.

This complements the existing reusable_bootstrap_conversation tests,
which only proved the conversation-identity decision (reuse vs. new)
without proving an answer actually arrives. No existing test in this
file previously covered the composite "cancel then resend on the same
conversation completes" property; the closest,
optimistic_cli_subagent_completion_with_in_flight_stream_reports_success,
only exercises the cancellation half.

Full end-to-end coverage through the real send_shared_session_query /
send_query / ResponseStream::new path remains out of reach in this
harness: ResponseStream::new dispatches a real network request via
ServerApiProvider, which is why every existing test in this file
injects a stream with register_mock_stream_for_test instead of driving
send_query directly.

Co-Authored-By: Warp Agent <agent@warp.dev>
…wer-input subscription

Response to a line-count audit requested after review.

- Removed DebugWindowController::force_close and its test. It carried
  #[allow(dead_code)] and had no production call site: its own doc
  comment noted the sandbox-deadline select! in AgentDriver::run
  already achieves the same effect on shutdown. Speculative API for a
  caller that doesn't exist is not something the spec should require;
  raised with the orchestrator to correct TECH.md if needed.

- Factored the duplicated viewer-input refresh subscription out of
  arm_debug_window and arm_setup_failure_debug_window into a shared
  subscribe_to_viewer_input_refresh helper. Both installed an
  identical terminal_driver subscription matching
  TerminalDriverEvent::SharedSessionViewerInput, logged the same
  trigger=viewer_input line, and called publish_debug_window_deadline
  the same way — differing only in which refresh function they passed
  (IdleTimeoutSender::refresh vs. the pin-aware
  DebugWindowController::refresh_from_last_armed). The extraction
  takes that refresh function as a closure parameter, so the pin-aware
  difference between the two call sites is preserved exactly, not
  flattened.

- Merged debug_window_controller_refresh_from_last_armed_is_inert_while_pinned
  and debug_window_controller_refresh_idle_is_inert_while_pinned into
  one debug_window_controller_refresh_is_inert_while_pinned test: both
  asserted the identical guarantee (a refresh entry point is a no-op
  while pinned) against the two different refresh methods. Left the
  three idempotency tests (duplicate pin, duplicate finish, finish of
  an unknown turn) and the flag-off/authorization/redelivery-outcome
  tests untouched, per instruction not to make a redelivery-adjacent
  test failure harder to localize.

cargo nextest run -p warp -E 'test(debug_window) | test(setup_failure)
| test(cloud_conversation_continuation) | ...' — 83/83 passed.
cargo clippy -p warp --lib --tests -- -D warnings — clean.
./script/format — applied, no behavior change. git diff --check — clean.

Co-Authored-By: Warp Agent <agent@warp.dev>
…ibility check

The retained-setup-failure tombstone's Debug CTA (and the pre-existing
ContinueInCloud CTA it shares its eligibility check with) required
task_creator_access() to match the exact literal creator UID before a
conversation exists for the task. `RunCreatorInfo`/`TaskPrincipalInfo`
is only ever a natural person or service account (see
RunCreatorInfoType on the server) -- creator never represents a team.
Team ownership lives in a separate field (RunItem.scope on the public
API) that the client never deserialized. The practical effect: any
team member other than the exact person who happened to create the
run saw a silent no-CTA tombstone, with no error, even though the
server's own CanAccessTask authorization (used both at follow-up
submission and at debug-agent bootstrap authorization time) would
genuinely have allowed them.

This affects both CTAs that share task_creator_access, since it was
inherited unchanged from the pre-existing ContinueInCloud path rather
than written for REMOTE-2661; fixing the shared predicate improves
both naturally rather than diverging their behavior.

- Added AmbientAgentTask::scope (TaskScope { scope_type, uid }),
  deserializing the server's existing RunItem.scope field the client
  never consumed.
- task_creator_access now also accepts a viewer who belongs to the
  run's owning team (scope.type == "Team"), via the same
  UserWorkspaces::team_from_uid_across_all_workspaces check
  conversation_access() already uses for the post-conversation-exists
  case elsewhere in this file. Falls back to the exact-creator check
  unchanged for a personal (non-team) run.
- Does not touch the server-side CanAccessTask authorization boundary
  or the injection-time authorization check; this only aligns what
  the client is willing to *show* with what the server was already
  willing to *allow*.

Added two tests: a team member who is not the literal creator gets
the Debug CTA on a team-owned retained setup-failure run; a viewer on
an unrelated team still gets no CTA.

cargo nextest run -p warp (targeted REMOTE-2661 sweep, 85/85; full
cloud_conversation_continuation suite, 32/32) -- all passed.
cargo clippy -p warp --lib --tests -- -D warnings -- clean.
./script/format -- no diff beyond intended changes. git diff --check
-- clean.

Co-Authored-By: Warp Agent <agent@warp.dev>
…r silently fall back on unknown eligibility

Two independent, requester-discovered gaps in the retained-setup-failure
debug flow, on top of the already-shipped tombstone/live-viewer routing.

1. `/agent`/`/cloud-agent` declare `Availability::NOT_CLOUD_AGENT`, so
   they are unavailable in any cloud/ambient pane -- including a
   retained setup-failure debug pane, which is exactly the surface the
   feature's own request text describes typing `/agent why did my
   setup fail` into. Typing the command there previously did nothing
   visible: the slash command menu doesn't offer it, and forcing it
   through would have tried to start a brand-new *local* conversation
   via `EnterAgentView`, which is wrong inside a retained *cloud*
   debug session.

   Fix: `GuiSlashCommandDataSource::availability` no longer marks a
   retained-setup-failure-debug-eligible pane as `CLOUD_AGENT` (a new
   `is_retained_setup_failure_debug_editable_for_task` helper answers
   the eligibility question from just the task id, reusing the same
   check `resolve_ai_query_routing` already trusts). `/agent`/`/new`'s
   execution arm now checks the shared routing function first and, if
   it resolves to `RetainedSetupFailureDebug`, emits
   `Event::SubmitSetupFailureDebugFollowup` directly instead of
   `EnterAgentView` -- the exact same authenticated follow-up path the
   pane's own (no-slash-command) input already used correctly.

2. `resolve_ai_query_routing`'s retained-setup-failure-debug check can
   only answer "eligible" once the task has actually been fetched into
   `AgentConversationsModel`. An absent task was previously
   indistinguishable from a genuinely ineligible one at that call
   site, so an ambient viewer whose task hadn't been fetched yet (e.g.
   joined via a direct session link rather than through the task list)
   silently fell through to the ordinary live-viewer path -- which is
   exactly the path this feature closes.

   Fix, two parts:
   - Eagerly warm the task cache at shared-session join time
     (`get_or_async_fetch_task_data`), as soon as the ambient task id
     is known -- well before a viewer can type anything, instead of
     leaving routing correctness dependent on incidental task-list
     poll timing.
   - `maybe_route_ai_query_to_remote_target` no longer treats "unknown
     eligibility" the same as "known ineligible" for an attached
     ambient viewer: if the task isn't cached yet, it blocks the
     submission with a clear toast and (re)triggers the fetch, instead
     of silently proceeding to the direct viewer prompt path.

New tests: two team-authorization tests were already in place from the
prior session; this change adds
`maybe_route_ai_query_to_remote_target_blocks_ambient_viewer_with_unresolved_task`,
proving the guard blocks the submission, never forwards a prompt, and
surfaces a toast, for the specific "eligibility resolvable but not yet
cached" case that motivated this fix.

cargo nextest run -p warp: 210/210 (targeted REMOTE-2661 + slash
command sweep), 5/5 (routing-specific), 715/715 (broader sweep) --
all passed. cargo clippy -p warp --lib --tests -- -D warnings -- clean.
./script/format -- no diff beyond intended changes. git diff --check --
clean.

Co-Authored-By: Warp Agent <agent@warp.dev>
…e debug bootstrap

The sharer's AgentPromptRequested handling for a no-token, purpose-tagged
bootstrap request (REMOTE-2661) authorized and started the conversation,
but never told session-sharing-server which conversation resulted. The
server's inject-message endpoint needs that conversation ID back to
resolve the caller's synchronous wait, so every bootstrap injection was
indistinguishable from a failed one and kept getting retried.

- Network::send_agent_prompt_acknowledgement / set_pending_bootstrap_ack
  / take_pending_bootstrap_ack (sharer): new pending-ack correlation
  state and the upstream message that reports a bootstrap's resulting
  conversation token.
- terminal_view_adaptor.rs: registers the pending ack right after
  accepting a purpose-tagged request (both the is_sharer fast path and
  the post-authorization path), and sends the acknowledgement once
  BlocklistAIHistoryEvent::ConversationServerTokenAssigned reports the
  new conversation's token.
- idempotency_key threaded through every send_agent_prompt_rejection
  call site so a bootstrap rejection is recorded under the same key a
  retry looks up.
- Viewer-side network.rs updated for the new required
  purpose/idempotency_key fields on AgentPromptRequest (always None for
  an ordinary live-viewer prompt) and the new
  AgentPromptFailureReason::NotEligibleForPurpose variant.

Depends on warpdotdev/session-sharing-protocol#65 for the new
AgentPromptPurpose/idempotency_key/AcknowledgeAgentPromptRequest wire
types; pinned to that PR's branch commit until it merges.

cargo check -p warp: clean.
cargo check -p warp --tests: clean.
cargo clippy -p warp --lib -- -D warnings: clean.
cargo fmt --check -p warp: clean.

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

Copy link
Copy Markdown
Contributor

Pushed a follow-up commit closing the last gap in the retained setup-failure debug flow: the sharer authorized and started a debug conversation for a purpose-tagged bootstrap request, but never reported which conversation resulted back to session-sharing-server. That server endpoint requires a conversation_id in its response to resolve the caller's synchronous wait, so every bootstrap injection was indistinguishable from a failed delivery and kept getting retried — the debug agent could never actually be observed as started.

This required corresponding changes in two upstream repos:

This repo's commit adds the sharer-side piece: it registers a pending-ack correlation when accepting a purpose-tagged request, and sends the acknowledgement once BlocklistAIHistoryEvent::ConversationServerTokenAssigned reports the new conversation's token. Cargo.toml is temporarily pinned to the protocol PR's branch commit until it merges (see the TODO(REMOTE-2661) comment).

cargo check -p warp / --tests, cargo clippy -p warp --lib --tests -- -D warnings, and cargo fmt --check -p warp all clean.

Comment thread Cargo.toml Outdated
serde_with = "^3.21"
serde_yaml = "0.8"
session-sharing-protocol = { git = "https://github.com/warpdotdev/session-sharing-protocol.git", rev = "b30fdd06379a3d073b398eabd106abed5b443aae" }
# TODO(REMOTE-2661): point back at main once warpdotdev/session-sharing-protocol#65 merges.

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.

make sure this gets reverted

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not reverted yet, deliberately — leaving this open. The pin is now 4888e758, the head of session-sharing-protocol#65, which is where AgentPromptRequest.purpose and AgentPromptFailureReason::NotEligibleForPurpose are gone; this client will not compile against main until #65 merges. The TODO(REMOTE-2661) above it is the reminder to point back at main at that moment, and the merge-order warning at the top of the PR description names the same gate.

Responding as wilson: Open session · View factory task

@warp-agent-staging warp-agent-staging Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Overview

Client leg of REMOTE-2661, reviewed for size after a request to trim the feature's ~5.4k added lines. The largest single cut in the whole feature lives here, and the read also turned up a routing predicate that diverges from the spec.

Concerns

  • Retained-session routing stops working after the first bootstrap (important). AmbientAgentTask::is_open_for_setup_failure_debug_bootstrap requires conversation_id().is_none() (app/src/ai/ambient_agents/task.rs:330-365), and both is_retained_setup_failure_debug_editable and resolve_ai_query_routing gate on it (cloud_conversation_continuation.rs:106-124,191-201). Once the first bootstrap persists a conversation ID, an attached viewer stops resolving to RetainedSetupFailureDebug and falls back to the direct LiveRemoteVm path — but PRODUCT.md §7 and TECH "Update the Warp client tombstone and routing" require every later prompt to reuse the conversation through the authenticated run-follow-up service, never the direct viewer path. Split bootstrap eligibility from retained-session routing so the latter stays true for an eligible retained failure after a token exists.
  • Same predicate ignores the pinned window (important). It requires an unexpired deadline rather than also honoring debug_agent_active while a turn has the timer pinned, which PRODUCT.md §1 and §24 both call for. A long autonomous turn that pins past the displayed deadline would lose its own entry point.
  • A second idempotency mechanism guarding one failure mode (~375-390 lines, suggestion). The task-ID→conversation retry map at app/src/ai/blocklist/controller/shared_session.rs:31-50,850-1097 is ~125-135 production lines, plus shared_session_tests.rs (68) and controller_tests.rs:425-608 (184). TECH.md:96 calls it "a second, independent safeguard" that "does not replace the protocol-level idempotency key", and this PR's own body concedes it does not prevent the failure on its own; it is keyed only by task_id, not the transmitted key, and stops reusing once the token is assigned, so it is strictly weaker than the service contract. Once session-sharing-protocol#65 and session-sharing-server#484 are merged, deployed, and their retry contract proven, this is the biggest safe cut in the feature — but only in that order.
  • A 184-line test that does not exercise what it is filed under (suggestion). controller_tests.rs:425-608 (cancelling_and_resending_within_one_conversation_reports_success) never reaches the new task-ID map or reuse path; it drives pre-existing same-conversation cancellation and would have passed before this PR. Reduce it to a small contract test, or delete it with the map above. Every other added test earns its place under the test-value gate — the timer pin/re-arm cases, the sync-mode regression, the routing capability and team-member cases, and the authorization true/deny/error/no-UID/no-token cases each pin a distinct spec requirement.
  • Comment volume and eligibility logging (~120-150 lines, suggestion). The non-test diff adds 417 ////// lines: controller/shared_session.rs +68, driver.rs +62, terminal_view_adaptor.rs +47, ambient_agents/task.rs +46, continuation routing +35. Keep the why-level comments on the authorization boundary and the timer invariant; drop the call-path recounting and issue history per AGENTS.md. Separately, task.rs:330-365 and cloud_conversation_continuation.rs:262-304 emit warn! on every eligibility evaluation on a routing/input path — that is breadcrumb-level noise at warn severity and carries no feature behavior.
  • Duplicated unresolved-task-cache policy (~40-55 lines, suggestion). The same "cache unresolved, block and fetch" handling appears at app/src/terminal/input.rs:4197-4247 and app/src/terminal/input/slash_commands/mod.rs:485-535, after eager warming at shared_session/viewer/terminal_manager.rs:848-859. Keep the fail-closed behavior, but collapse the duplicated model locking, lookup, fetch, and toast into one Input helper used by both ordinary submit and /agent//new, and assert it once at the helper level.

Not trim candidates: the timer pin/re-arm controller (PRODUCT.md §18-24 requires idempotence by turn ID and a pin while at least one turn is active, so the turn-keyed set is not gratuitous), TaskSyncMode::PreserveTerminalSetupFailure, TaskScope/team access, and setupFailureDebugAuthorization — I looked for an existing workload-token query the sharer could reuse for a task+participant check and found none on master. Its ~250 production/schema lines are proportionate to a credit-spend authorization boundary; its four fail-closed inputs already converge through one .unwrap_or(false).

Verdict

Checks: build not run, tests not run, CI not checked, visual proof n/a — this was a read-only size analysis, so the two routing findings above are unverified against a running client.

Found: 0 critical, 2 important, 4 suggestions, 0 nits

Of the +2,462 here, 934 lines (37.9%) are test-only and 1,528 are non-test; conservative non-overlapping trim is ~550 lines, leaving a floor near ~1.9k.

Responding as wilson: Open session · View factory task

warp-agent-staging Bot and others added 11 commits August 19, 2026 21:24
Resolves three conflicts:
- `terminal_manager.rs`: union of the two import additions.
- `view.rs`: union of the two re-export lists.
- `cloud_conversation_continuation.rs`: adopt master's rename of
  `task_creator_access` to `task_ownership_access` and its new
  `completed_child_conversation_access` wrapper, keeping this branch's
  owning-team membership check inside the renamed function.
Bumps the protocol pin to warpdotdev/session-sharing-protocol#65 head
(4888e758), where `AgentPromptRequest.purpose` and
`AgentPromptFailureReason::NotEligibleForPurpose` no longer exist.

Replaces the `purpose`-tag gate on registering a bootstrap acknowledgement
with the predicate session-sharing-server itself applies: an idempotency key
with no conversation to continue. The two must agree -- a bootstrap the
server waits on but the sharer never acknowledges is treated as an
undelivered injection and retried until the caller's deadline -- and both
sides evaluate it against the parsed token, so an absent or unparseable
conversation id counts as no conversation on either side.
Splits bootstrap eligibility from retained-session routing, so a later prompt
keeps going through the authenticated follow-up service once the first
bootstrap has persisted a conversation ID (PRODUCT.md §7) instead of falling
back to the direct viewer path, and keeps the session eligible while a debug
turn pins the idle timer past the last published deadline (§1, §24). Both are
covered by new routing tests.

Removes the client task-ID bootstrap-reuse map. session-sharing-server#484's
Redis claim, keyed by the transmitted idempotency key, is now the durable
guarantee against a duplicate bootstrap conversation; the map was keyed by
task ID, stopped reusing once a token existed, and is redundant against it.
The conversation is still tagged PreserveTerminalSetupFailure. Its two test
files go with it: the pure reuse-decision tests, and a 184-line outcome test
that only drove pre-existing same-conversation cancellation.

Sends the setup-failure state, status message, and the debug window's first
deadline in one update, closing the interval where the server saw a terminal
setup failure without knowing a debug window was opening.

Collapses the duplicated "cache unresolved, block and fetch" handling in
ordinary submit and /agent into one Input helper, with its regression folded
to a single assertion at the helper. Fail-closed behavior is unchanged.

Drops the eligibility warn! breadcrumbs from the routing and input paths and
trims call-path recounting from the comments.
Three test-only breakages from the master merge, where files new on master
construct types this branch added fields to (and vice versa):

- `orchestration_child_tracker_tests.rs` and `child_agent/materialization_tests.rs`
  build `AmbientAgentTask` literals and need this branch's `debug_agent_available`
  and `scope`. Both are `false`/`None`, which is the fail-closed default and
  irrelevant to what either test asserts.
- `controller_tests.rs` builds a `StreamFinished` and needs master's new
  `request_charges`, matching the sibling literal already in the same file.

`cargo check -p warp --lib --tests` passes with these.
`routing_allows_live_input_only_for_executable_shared_session_role` built a
bare app with no singletons, and panicked with "Cannot get singleton model of
type AgentConversationsModel that was never registered".

Pre-existing on this branch rather than new: `resolve_ai_query_routing` has
evaluated retained-debug eligibility -- which reads `AgentConversationsModel`
-- before the live-viewer branch since the original feature commits, so the
same call sits at this spot in b78530e. It went unnoticed only because the
branch did not build, so nothing ran the test.

Registering the model empty is the right fixture, and is what the routing
tests added alongside it already do: with no task row, an ordinary live viewer
must still route to `LiveRemoteVm`, which is exactly what this test asserts.
The synchronous inject contract it existed to satisfy is gone: warp-server
now marks a bootstrap follow-up delivered on HTTP success like every other
follow-up, and learns the conversation ID through the ordinary
ApplyClientUpdates task-sync path the sharer already reports on.

Removes register_bootstrap_ack_if_bootstrap_request and its two call sites,
the AcknowledgeAgentPromptRequest send at ConversationServerTokenAssigned,
and Network's pending-ack bookkeeping.
With the durable bootstrap outcome record gone from session-sharing-server,
the key a rejection echoed back had no consumer: the claim is taken and
looked up server-side from the inject request body, never from the field
forwarded to the sharer. Nothing in warp read the key for behavior even
before this - every use was a pass-through onto the wire.

That removes warp's last dependency on session-sharing-protocol#65, which
is closing, so the pin reverts to main (b30fdd06) - already what master
tracks. Cargo.lock reverts wholesale, which also undoes two unrelated
resolution drifts the branch had picked up (windows-core and heck).
This branch dropped master's `#[cfg(not(target_family = "wasm"))]` from the
import, but the only use in the file, conversation_is_cloud_oz_for_slash_command,
is itself wasm-gated. The result was a hard `-D warnings` failure on the wasm
clippy target. It went unnoticed because GitHub skips CI for draft PRs, so no
build had ever run against this branch on any target.
…-warp-client

# Conflicts:
#	app/src/ai/agent_sdk/driver_tests.rs
#	app/src/terminal/input/slash_commands/data_source/gui.rs
#	skills-lock.json
@dmichelin
dmichelin marked this pull request as ready for review August 25, 2026 20:31
dmichelin and others added 2 commits August 26, 2026 16:27
…-warp-client

# Conflicts:
#	app/src/ai/agent_sdk/driver_tests.rs
#	app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs
#	app/src/terminal/view.rs
@warp-local-for-testing-only

Copy link
Copy Markdown

An unexpected error has occurred: managed MCP server 01a01a21-3ec9-71a1-99f2-f13b1773c920 is not active

@dmichelin
dmichelin enabled auto-merge (squash) September 1, 2026 23:28
@dmichelin
dmichelin merged commit 09127d8 into master Sep 2, 2026
72 of 78 checks passed
@dmichelin
dmichelin deleted the factory/remote-2661-warp-client branch September 2, 2026 00: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.

3 participants