12 KiB
Long-Running Command Side-Agent Plan
Goal
When an Agent Mode shell command runs longer than three seconds, automatically open a side-agent chat that monitors the process, reports meaningful status, and feeds its observations back into the main agent conversation.
The side agent should use the configured profile's lower-cost coding/typing model where appropriate. It must not silently rotate across arbitrary models.
Current architecture
Relevant existing paths:
app/src/ai/blocklist/action_model/execute/shell_command.rsShellCommandExecutoremitsShellCommandExecutorEvent::ExecuteCommandwhen an agent command starts.action_result_futurepolls/waits for command completion and snapshots.
app/src/terminal/view.rshandle_shell_command_executor_eventexecutes the command.- It already schedules a delayed check using
LONG_RUNNING_COMMAND_DURATION_MSand detectsblock.is_active_and_long_running(). - It currently locks the input in Agent Mode after the command becomes long-running.
app/src/ai/blocklist/agent_view/controller.rsAgentViewControllerenters fullscreen or inline Agent View.AgentViewEntryOrigin::LongRunningCommandalready exists.
app/src/ai/blocklist/context_model.rs- Owns conversation selection and pending query state.
app/src/ai/blocklist/controller.rsBlocklistAIController::send_request_inputsends requests for an existing conversation.RequestInputcarries bothmodel_idandcoding_model_id.
app/src/ai/blocklist/controller/response_stream.rs- Resolves provider configuration from a model ID.
- Retry/failure behavior is centralized here.
app/src/ai/agent/mod.rsAIAgentInput::UserQuerysupportsrunning_command: Option<RunningCommand>.RunningCommandcarries command text, block ID, terminal snapshot, cursor, and alt-screen state.
app/src/ai/agent/api/convert_to.rs- Converts local
AIAgentInputinto request API input.
- Converts local
app/src/ai/agent/api/convert_conversation.rs- Converts shell command snapshots/results back into agent context.
Product behavior
Trigger
- Start a three-second timer when an Agent Mode command begins executing.
- If the command completes, is cancelled, or the block is no longer active before the timer fires, do nothing.
- If it is still active and long-running at three seconds, start monitoring automatically.
- Do not start a second monitor for the same command/block.
Side-agent conversation
- Create a new local Agent View conversation associated with the same terminal surface.
- Enter the side conversation in the existing fullscreen/subagent chat UI, using
AgentViewEntryOrigin::LongRunningCommand. - The side agent must not steal or destroy the main conversation state.
- The UI should clearly indicate that the conversation is monitoring a running command.
- The side agent should use the active profile's
coding_model_idas its model, because this is the configured typing/lower-cost role.
Initial monitoring prompt
Generate a structured prompt containing:
- The exact command text.
- Working directory, when available.
- Start time and elapsed time.
- Current output snapshot.
- Exit status: unavailable while running.
- Explicit instructions to:
- identify normal progress,
- describe observable success signals,
- describe observable failure signals,
- flag suspicious stalls or lack of progress,
- flag dangerous behavior or unexpected writes,
- avoid declaring success without evidence,
- avoid taking actions unless explicitly authorized.
Example guidance:
Monitor the running command below. Do not assume it succeeded merely because output stopped. Look for concrete completion or success indicators, explicit errors, non-zero exit status, repeated retries, blocked input, lock waits, and no-progress intervals. For database operations, flag a statement that appears stuck while updating a small number of rows, but distinguish a legitimate long-running query from a deadlock or lock wait when possible. Report observations and uncertainty; do not interrupt or modify the process.
Ongoing updates
- Feed fresh command snapshots/output to the side conversation while it remains active.
- Prefer existing
RunningCommand/long-running snapshot representations over inventing a second terminal-output protocol. - Throttle updates to avoid flooding the model and UI. Initial proposal: no more than one update every 2 seconds, and only when output or command state changes.
- Include elapsed time on every update or enough metadata for the side agent to calculate it.
- On completion, send a final update containing:
- exit code,
- final output,
- completion timestamp,
- whether the command was cancelled or preempted.
Main-agent context integration
Every side-agent observation must become available to the main agent.
Preferred design:
- Store a compact monitor transcript on the command's main conversation/action state.
- When the side agent emits a meaningful observation, append a system/tool-style context item to the main conversation.
- On command completion, append a final monitor summary to the main conversation before the main agent receives the command result.
- Preserve the monitor transcript across retries/session restoration where the command conversation is persisted.
- Avoid duplicating every token from the side-agent stream; persist completed observations and periodic summaries instead.
The main agent should receive explicit provenance, e.g.:
Side-agent monitor update for command
<id>at00:07: no completion signal observed; output unchanged for 5 seconds; process may be waiting on a database lock. This is an observation, not a confirmed failure.
Cleanup
- Stop monitor updates when the command finishes, is cancelled, or the terminal view closes.
- Close or mark the side conversation complete without losing its transcript.
- If the user manually opens another conversation, keep the side monitor associated with the original command rather than replacing the main conversation.
- If monitoring fails, report the monitor failure to the main agent but never terminate the user command because the monitor failed.
Model fallback behavior
The profile has two explicit model roles:
RequestInput.model_id: thinking/base model.RequestInput.coding_model_id: typing/coding model.
For ordinary requests:
- Try the selected thinking model.
- If the provider reports a rate-limit or budget-exhausted error before any client/tool action has occurred, retry once with the profile's configured coding model.
- Do not retry if both model IDs are equal.
- Do not rotate through arbitrary models or provider entries.
- Do not switch models after tools/actions have already executed.
- If both roles fail, return a clear user-facing rate-limit/budget message.
For the side agent specifically:
- Start directly with
coding_model_id. - If it rate-limits, do not silently switch to the thinking model unless a future product decision explicitly allows that.
- Surface that monitoring is unavailable while leaving the main command running.
Proposed implementation phases
Phase 1: Model fallback state
Files:
app/src/ai/blocklist/controller/response_stream.rsapp/src/ai/blocklist/controller/response_stream_tests.rsapp/src/ai/agent/api/impl.rsapp/src/server/server_api.rs
Tasks:
- Add a structured provider-budget/rate-limit classification instead of matching only formatted strings at the final UI boundary.
- Add
coding_model_idto the retry/fallback state available toResponseStream. - Add a one-time
retry_with_coding_modelpath. - Re-resolve provider configuration using the coding model ID.
- Ensure the retry does not occur after client actions or when IDs match.
- Add tests for:
- thinking model succeeds,
- thinking model rate-limits and coding model succeeds,
- both models rate-limit,
- same model IDs do not retry,
- non-rate-limit errors retain existing behavior,
- post-action rate limits do not silently switch models.
Phase 2: Command monitor state
Files:
app/src/terminal/view.rsapp/src/ai/blocklist/action_model/execute/shell_command.rs- likely a new monitor model/module under
app/src/ai/blocklist/
Tasks:
- Add a terminal-view-scoped monitor registry keyed by command/block ID.
- Replace the current one-off delayed input-lock check with a monitor-start event or extend the existing event with a long-running transition.
- Capture command metadata and snapshots without holding
TerminalModellocks across async work. - Add cancellation/completion cleanup paths.
- Add focused unit tests for timer race cases.
- Add deterministic unit coverage for duplicate monitor registration and cleanup.
Phase 3: Side-agent conversation creation
Files:
app/src/ai/blocklist/agent_view/controller.rsapp/src/ai/blocklist/agent_view/conversation_selection.rsapp/src/ai/blocklist/context_model.rsapp/src/ai/blocklist/controller.rsapp/src/terminal/view.rs
Tasks:
- Add an explicit API for creating a monitoring conversation without treating it as a user navigation action.
- Use
AgentViewEntryOrigin::LongRunningCommand. - Ensure the main active conversation ID is not overwritten unintentionally.
- Add a monitor-specific conversation association record.
- Select the coding model for monitor requests.
Phase 4: Monitoring prompt and updates
Files:
app/src/ai/agent/mod.rsapp/src/ai/agent/api/convert_to.rsapp/src/ai/blocklist/controller.rs- new monitor prompt/context helpers
Tasks:
- Build the initial structured monitoring prompt.
- Reuse
RunningCommandand shell snapshot conversion where possible. - Add throttled snapshot updates.
- Add explicit completion/cancellation summaries.
- Redact secrets using the existing input redaction path before sending snapshots to the side agent.
Phase 5: Main-agent context bridge
Files:
app/src/ai/blocklist/controller.rsapp/src/ai/blocklist/agent_view/monitor conversation codeapp/src/ai/agent/conversation.rs- persistence models/snapshot code as needed
Tasks:
- Define a structured side-agent observation type.
- Append meaningful observations to the main conversation context.
- Persist completed summaries and association metadata.
- Avoid token-by-token duplication.
- Add tests proving the main request receives monitor observations before and after command completion.
Phase 6: UI polish and failure handling
Tasks:
- Add a visible “Monitoring command…” indicator in the side-agent header or conversation metadata.
- Show monitor unavailable/rate-limited state without stopping the command.
- Ensure user takeover and command cancellation are represented in both conversations.
- Add telemetry only for high-level lifecycle events, not command output or secrets.
Safety and correctness requirements
- Never terminate or modify the user's process because the side agent failed.
- Never send unredacted command output or secrets to the side agent.
- Never acquire nested
TerminalModellocks across async boundaries. - Do not let side-agent tool calls execute arbitrary commands by default.
- Do not claim success based solely on inactivity.
- Preserve the main agent's existing action ordering and tool-result pairing invariants.
- Keep monitoring optional at the implementation boundary so it can be disabled if the UI or provider is unavailable, while the three-second trigger remains automatic when the feature is enabled.
Validation plan
Targeted checks:
cargo test -p galaxy --lib response_streamcargo test -p galaxy --lib shell_commandcargo test -p galaxy --lib agent_viewcargo test -p galaxy --lib blocklistcargo check -p galaxy --lib
Manual verification:
- Run
sleep 1: no side agent opens. - Run
sleep 5: side agent opens automatically at approximately three seconds. - Run a command with periodic output: side agent reports progress.
- Run a command with no output but successful completion: side agent does not claim failure prematurely and receives the final exit code.
- Run a command that exits non-zero: side agent identifies failure and main agent receives the summary.
- Run a simulated database lock/wait: side agent flags suspicious lack of progress with uncertainty.
- Trigger a provider budget limit on the thinking model: coding model is tried once.
- Trigger limits on both profile models: clear rate-limit message is shown and no crash occurs.