Skip to content

fix: refuse agent delivery to exited panes - #389

Merged
EtanHey merged 2 commits into
mainfrom
fix/365-dead-pane-refusal
Aug 10, 2026
Merged

fix: refuse agent delivery to exited panes#389
EtanHey merged 2 commits into
mainfrom
fix/365-dead-pane-refusal

Conversation

@EtanHey

@EtanHey EtanHey commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Summary

  • refuse routed agent-path delivery when a fresh target screen is a bare shell
  • scope discovery to the routed target so unrelated pane churn cannot block healthy relays
  • revalidate after final route resolution to preserve the route-rebinding safety behavior
  • preserve deliberate raw surface/command/key delivery

Review revision

  • prediction before tests: the unrelated-churn reproduction would fail before the fix, deliver once after it, and leave the shell/ref-binding safety tests unchanged
  • actual: exact match; the regression went RED with isError:true, then GREEN with one target send, while all four focused safety/availability tests passed

Test plan

  • bun run test — 2,503 passed, 0 failed, 1 skipped
  • bun run typecheck
  • bun run build
  • bun run pre-pr — 63 passed
  • git diff --check
  • CodeRabbit local review — 0 findings

Refs #365

— cmuxlayerCodex (worker) · codex/gpt-5.6-sol

Note

Refuse agent delivery to panes in a bare shell state

  • Adds control_state (from parseScreen) to all DiscoveredAgent objects in agent-discovery.ts, exposing whether a surface is running an agent CLI or a bare shell.
  • Introduces AgentDiscovery.scanTarget for target-scoped discovery that validates surface UUID/ref/workspace binding stability around the read operation.
  • Guards send_to/send_to_agent routed delivery in server.ts by calling scanTarget before delivery and again after final route resolution, throwing when control_state === 'shell'.
  • Replaces the prior cached fleet-wide discovery lookup for identity checks with fresh target-scoped reads, reducing sensitivity to unrelated surface changes.
  • Risk: routed delivery now fails if the target pane has exited its agent CLI between route resolution steps, whereas previously it would proceed.

Macroscope summarized f702442.

Co-Authored-By: cmuxlayerCodex running gpt-5.6-sol <noreply@anthropic.com>
@cursor

cursor Bot commented Aug 10, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_cf32ae9d-5ee1-41b9-aeec-d7109f00b455)

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Discovery results now include control_state. Agent-routed delivery performs fresh validation before input submission and fails closed when the route points to a bare shell. Tests cover shell fallback, route rebinding, and updated Claude startup fixtures.

Changes

Agent delivery safety

Layer / File(s) Summary
Discovery control-state contract
src/agent-discovery.ts
DiscoveredAgent now includes control_state. Discovery copies parsed state and uses "unknown" when screen parsing fails.
Delivery surface validation
src/server.ts
Agent-routed delivery performs fresh occupant checks before mutation and again before input submission. Bare-shell surfaces are rejected.
Delivery validation coverage
tests/server-agent-tools.test.ts, tests/v2-interact-kill.test.ts
Tests verify rejection without sending, route-rebinding race handling, and the Claude startup marker.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Server as server.ts
  participant Discovery as agent-discovery
  participant Terminal as routed terminal
  Client->>Server: request send_to or send_to_agent
  Server->>Discovery: perform fresh discovery
  Discovery-->>Server: return control_state
  Server->>Terminal: resolve routed surface
  Server->>Discovery: recheck resolved surface
  Discovery-->>Server: return current control_state
  Server->>Terminal: submit input only when agent TUI is present
Loading

Possibly related PRs

Poem

A rabbit checks the pane with care,
No shell-bound message enters there.
Fresh states guide each routed hop,
Race-bound routes make sending stop.
“Claude Code” marks the ready screen—
Safe little burrows, crisp and clean!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: refusing agent delivery to panes that have exited to a bare shell.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/365-dead-pane-refusal

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread src/server.ts
// modes bypass this helper and remain available for deliberate recovery.
const normalizedUuid = (value: string | null | undefined): string | null =>
value?.trim().toLowerCase() || null;
const assertAgentRouteHasTui = async (candidateRoute: typeof route) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High src/server.ts:9349

The final assertAgentRouteHasTui(route) check is separated from the first text mutation by several awaited operations (resolveAgentIoRoute, assertDeliveryRouteCurrent, and assertDeliveryTargetIsSafe inside deliverInputChunks). If the agent exits to a bare shell after the TUI check but before deliverInputChunks sends text — while the same surface UUID/ref remains bound — assertDeliveryTargetIsSafe reads the screen but does not reject control_state === "shell", so deliverInputChunks types the fleet message into the shell. This is the exact exited-pane command-execution race the assertAgentRouteHasTui guard was added to prevent. Consider making assertDeliveryTargetIsSafe reject control_state === "shell" so the shell-exit race is covered at the final mutation boundary, or moving the TUI check immediately before the first client.send/client.pasteText call.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @src/server.ts around line 9349:

The final `assertAgentRouteHasTui(route)` check is separated from the first text mutation by several awaited operations (`resolveAgentIoRoute`, `assertDeliveryRouteCurrent`, and `assertDeliveryTargetIsSafe` inside `deliverInputChunks`). If the agent exits to a bare shell after the TUI check but before `deliverInputChunks` sends text — while the same surface UUID/ref remains bound — `assertDeliveryTargetIsSafe` reads the screen but does not reject `control_state === "shell"`, so `deliverInputChunks` types the fleet message into the shell. This is the exact exited-pane command-execution race the `assertAgentRouteHasTui` guard was added to prevent. Consider making `assertDeliveryTargetIsSafe` reject `control_state === "shell"` so the shell-exit race is covered at the final mutation boundary, or moving the TUI check immediately before the first `client.send`/`client.pasteText` call.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f1d1e09ce3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/server.ts
Comment on lines 9439 to +9440
route = await engine.resolveAgentIoRoute(args.agent_id);
await assertAgentRouteHasTui(route);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Recheck the TUI after acquiring the surface write lock

This final shell check still occurs before withSurfaceWrite acquires the per-surface lock. If another routed write is finishing concurrently, this call can observe the agent TUI, the first write can then submit an exit-triggering command and release the lock, and this call can acquire the lock and type into the resulting shell; the only check inside the critical section validates that the registry route is unchanged. Repeat the TUI check inside the locked callback immediately before mutation so concurrent cmuxlayer deliveries cannot reopen the shell-injection path.

Useful? React with 👍 / 👎.

Comment thread src/server.ts Outdated
Comment on lines +9350 to +9351
discovery.invalidate();
const freshOccupant = (await discovery.scan(true)).find((entry) =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid a fleet-wide scan for every routed message

discovery.scan(true) reads all terminal screens and enumerates the complete topology before and after the reads, even though this guard needs only the target surface. The helper is invoked twice per delivery, and the broadcast loop calls deliverAgentInput serially for every target, so broadcasting across N agents now performs roughly 2N² read-screen operations plus repeated topology walks; a moderately sized fleet can incur substantial latency or tool timeouts before messages are delivered. Use a binding-validated targeted screen read, or reuse one fresh discovery snapshot across a broadcast.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/server.ts (1)

9440-9456: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Revalidate the control state at each physical terminal write.

Line 9440 validates the TUI before later awaits in withSurfaceWrite and deliverInputChunks. assertDeliveryRouteCurrent only validates the route identity. If the agent exits during that interval, client.send, client.pasteText, or a recovery client.sendKey can type routed text into a bare shell.

Add a write-only validation hook that checks both the current route and assertAgentRouteHasTui immediately before each terminal write. Apply it to chunk writes, Return, and recovery Return. Add a test that changes the screen to a shell after the Line 9440 check and verifies zero writes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server.ts` around lines 9440 - 9456, Extend assertDeliveryRouteCurrent to
also call assertAgentRouteHasTui immediately before terminal writes, or add a
dedicated write-validation hook that performs both checks. Pass this hook
through withSurfaceWrite and deliverInputChunks so it runs before every
client.send, client.pasteText, and recovery client.sendKey, including chunk
writes, Return, and recovery Return. Add coverage that switches the screen to a
shell after the initial validation and verifies no terminal writes occur.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/server.ts`:
- Around line 9440-9456: Extend assertDeliveryRouteCurrent to also call
assertAgentRouteHasTui immediately before terminal writes, or add a dedicated
write-validation hook that performs both checks. Pass this hook through
withSurfaceWrite and deliverInputChunks so it runs before every client.send,
client.pasteText, and recovery client.sendKey, including chunk writes, Return,
and recovery Return. Add coverage that switches the screen to a shell after the
initial validation and verifies no terminal writes occur.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 97e45d41-1cf3-4ea7-9c16-21e3d682b130

📥 Commits

Reviewing files that changed from the base of the PR and between 6df1e64 and f1d1e09.

📒 Files selected for processing (4)
  • src/agent-discovery.ts
  • src/server.ts
  • tests/server-agent-tools.test.ts
  • tests/v2-interact-kill.test.ts
📜 Review details
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-03-15T10:42:35.917Z
Learnt from: EtanHey
Repo: EtanHey/cmuxlayer PR: 1
File: tests/quality-tracking.test.ts:171-200
Timestamp: 2026-03-15T10:42:35.917Z
Learning: In tests/quality-tracking.test.ts for the cmuxlayer project, ensure that at or above 80% context quality degradation, behavior depends on depth: depth-0 agents receive a /compact command; depth > 0 agents are killed and logged (kill + log). Respawn of non-root agents is out of scope for v1. Treat the design doc quality tracking section as the authoritative source for this behavior, and align test expectations accordingly.

Applied to files:

  • tests/server-agent-tools.test.ts
  • tests/v2-interact-kill.test.ts
🔇 Additional comments (4)
src/agent-discovery.ts (1)

6-6: LGTM!

Also applies to: 17-17, 137-137, 156-156

src/server.ts (1)

9341-9380: LGTM!

tests/server-agent-tools.test.ts (1)

146-146: LGTM!

Also applies to: 6998-7050, 7206-7266

tests/v2-interact-kill.test.ts (1)

132-132: LGTM!

Co-Authored-By: cmuxlayerCodex running gpt-5.6-sol <noreply@anthropic.com>
@cursor

cursor Bot commented Aug 10, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_29286ff2-e30b-4f4b-b6e0-6c9e0576fb28)

Comment thread src/agent-discovery.ts
);
}

const completedMatches = (await this.deps.listSurfaces())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High src/agent-discovery.ts:178

scanTarget validates surface identity by surface.ref when target.surface_uuid is absent, but ref is mutable and can be recycled to a different UUID while keeping the same ref and workspace. If the surface is rebound during scanSurface, the validation passes and returns stale screen evidence for the old occupant — which can route keystrokes to the new occupant. Compare the initial and completed stable surface.id values whenever either side provides a UUID, not just when target.surface_uuid is present.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @src/agent-discovery.ts around line 178:

`scanTarget` validates surface identity by `surface.ref` when `target.surface_uuid` is absent, but `ref` is mutable and can be recycled to a different UUID while keeping the same ref and workspace. If the surface is rebound during `scanSurface`, the validation passes and returns stale screen evidence for the old occupant — which can route keystrokes to the new occupant. Compare the initial and completed stable `surface.id` values whenever either side provides a UUID, not just when `target.surface_uuid` is present.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f70244289e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/server.ts
Comment on lines +9350 to +9353
if (
freshOccupant &&
!freshOccupant.read_error &&
freshOccupant.control_state === "shell"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Detect shell prompts beneath stale TUI banners

When an exited CLI leaves recognizable TUI text in the last 30 screen lines—for example, Claude Code followed by the restored shell prompt—this condition does not fire. detectAgentType scans the entire buffer, and inferControlState classifies an idle known agent as ready before considering the trailing shell prompt, so the routed text can still execute as a shell command. Determine shell fallback from current trailing prompt evidence rather than requiring the whole-screen parse to equal shell.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant