Skip to content

fix(socket): return not_found when surface_id is provided but unresolvable - #2150

Merged
lawrencecchen merged 1 commit into
manaflow-ai:mainfrom
anthhub:fix/surface-ref-silent-fallback
Mar 27, 2026
Merged

lawrencecchen merged 1 commit into
manaflow-ai:mainfrom
anthhub:fix/surface-ref-silent-fallback

Conversation

@anthhub

@anthhub anthhub commented Mar 25, 2026 •

Copy link
Copy Markdown

Summary

  • When surface_id is explicitly provided in a socket command but fails to resolve (stale ref, unknown ordinal, etc.), the four affected functions now return a not_found error instead of silently falling back to ws.focusedPanelId.
  • When surface_id is not provided, the existing focused-pane fallback is preserved unchanged (backward compatible).
  • Affected functions: v2SurfaceSendText, v2SurfaceSendKey, v2SurfaceClearHistory, v2SurfaceReadText (all in Sources/TerminalController.swift).

Root Cause

v2UUID(params, "surface_id") calls v2ResolveHandleRef() internally. If the ref is unresolvable, it returns nil. The previous pattern v2UUID(params, "surface_id") ?? ws.focusedPanelId silently promoted this nil to the focused panel, causing two distinct failure modes:

Fix

// Before
let surfaceId = v2UUID(params, "surface_id") ?? ws.focusedPanelId

// After
let surfaceId: UUID?
if params["surface_id"] != nil {
    surfaceId = v2UUID(params, "surface_id")
    guard surfaceId != nil else {
        result = .err(code: "not_found", message: "Surface not found for the given surface_id", data: nil)
        return
    }
} else {
    surfaceId = ws.focusedPanelId
}

Test Plan

  • cmux send --surface surface:9999 "hello" → ERROR: not_found: Surface not found for the given surface_id (non-zero exit)
  • cmux send-key --surface surface:9999 Enter → same error
  • cmux read-screen --surface surface:9999 → same error
  • cmux send "hello" (no --surface) → operates on focused pane (backward compat preserved)
  • cmux send --surface surface:1 "hello" with a valid ref → succeeds as before
  • cmux read-screen --surface surface:29 with a valid terminal ref → returns screen content

Fixes #2042, Fixes #2045

🤖 Generated with Claude Code


Summary by cubic

Return not_found when a socket command includes an unresolvable surface_id, instead of silently targeting the focused pane. Behavior with no surface_id is unchanged. Fixes #2042 and #2045.

  • Bug Fixes
    • Updated v2SurfaceSendText, v2SurfaceSendKey, v2SurfaceClearHistory, and v2SurfaceReadText to error with not_found if the provided surface_id cannot be resolved.
    • If surface_id is not provided, continue falling back to ws.focusedPanelId (backward compatible).

Written for commit 4c977f1. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes
    • Enhanced validation and error handling for terminal surface operations. Explicitly provided surface identifiers are now validated more rigorously, with clearer "Surface not found" error messages for invalid identifiers. When no identifier is provided, operations default to the currently focused surface.

…nresolvable

Previously, v2SurfaceSendText, v2SurfaceSendKey, v2SurfaceClearHistory, and
v2SurfaceReadText would silently fall back to ws.focusedPanelId when a caller
supplied a surface_id that could not be resolved (e.g. a stale ref or an ordinal
whose mapping had not yet been registered). This caused two distinct bugs:

- manaflow-ai#2042: Commands like `cmux send --surface surface:9999` would succeed (exit 0)
  and deliver input to the focused pane instead of returning an error, making
  automation that targets specific surfaces unreliable.

- manaflow-ai#2045: When the fallback landed on a browser panel, the subsequent
  ws.terminalPanel(for:) check failed and returned "Surface is not a terminal",
  making valid terminal surfaces appear broken when addressed by ref.

The fix adds an explicit check: if params["surface_id"] is present but
v2UUID() returns nil (resolution failure), we immediately return a not_found
error instead of falling back to the focused pane. When surface_id is absent,
the existing focused-pane fallback is preserved for backward compatibility.

Fixes manaflow-ai#2042, Fixes manaflow-ai#2045

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel

vercel Bot commented Mar 25, 2026

Copy link
Copy Markdown

@anthhub is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Mar 25, 2026 •

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c22e8e61-7aa2-4471-9003-76590ca096d6

📥 Commits

Reviewing files that changed from the base of the PR and between 99ca3c9 and 4c977f1.

📒 Files selected for processing (1)
  • Sources/TerminalController.swift

📝 Walkthrough

Walkthrough

This change modifies four v2 terminal surface operations in TerminalController.swift to treat explicitly provided surface_id parameters as authoritative, returning a not_found error when resolution fails instead of silently falling back to the focused panel.

Changes

Cohort / File(s) Summary
Surface ID validation in terminal operations
Sources/TerminalController.swift
Modified v2SurfaceSendText, v2SurfaceSendKey, v2SurfaceClearHistory, and v2SurfaceReadText to differentiate between explicit surface_id parameters (which must resolve successfully or error) and absent parameters (which fall back to ws.focusedPanelId). Replaced combined fallback expression with conditional logic that validates explicit IDs and returns "Surface not found for the given surface_id" when resolution fails.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • PR #1799 — Modifies TerminalController.swift with the same pattern of treating explicit surface IDs as authoritative and returning not_found errors for invalid explicit parameters.

Poem

🐰 Hop-hop! No more silent falls,
When surfaces can't be found.
A clear "not found" now calls,
No more mis-routing 'round.
Explicit params reign supreme! 🎯

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: returning a not_found error when surface_id is provided but unresolvable.
Description check ✅ Passed The description covers all required template sections: Summary (what/why), Testing (test plan with example commands), Checklist completion status, and includes affected functions and root cause analysis.
Linked Issues check ✅ Passed The PR fully addresses both linked issues: #2042 (return error instead of silently falling back to focused pane) and #2045 (fix ref resolution failures for terminal surfaces).
Out of Scope Changes check ✅ Passed All changes are scoped to the four affected v2 terminal functions in TerminalController.swift, directly addressing the linked issue requirements with no extraneous modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@greptile-apps

greptile-apps Bot commented Mar 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a silent-fallback bug in four socket surface commands: when surface_id is explicitly provided but unresolvable, the commands now correctly return not_found instead of transparently operating on ws.focusedPanelId. The fix is minimal, backward-compatible, and correctly scoped — unfocused-pane fallback is preserved when surface_id is absent.

Key changes:

  • v2SurfaceSendText, v2SurfaceSendKey, v2SurfaceClearHistory, v2SurfaceReadText in Sources/TerminalController.swift each replace the v2UUID(params, "surface_id") ?? ws.focusedPanelId one-liner with an explicit params["surface_id"] != nil branch that guards on nil and returns not_found.

Notes:

  • The identical 9-line block is duplicated across all four functions; extracting a shared helper would improve maintainability.
  • The new not_found error responses pass data: nil — including the raw surface_id string from params would make error messages more actionable for CLI users.
  • Per the project's regression-test policy in CLAUDE.md, a two-commit structure with a failing test first is expected for bug fixes. The existing tests_v2/ Python socket infrastructure appears sufficient to cover this case, and no tests were added.

Confidence Score: 4/5

  • Safe to merge with minor follow-ups recommended: regression tests and minor style cleanup.
  • The fix is logically correct and backward compatible — all four affected code paths are updated consistently. The v2UUID / v2ResolveHandleRef lookup is called once per handler (not twice), and the outer guard let surfaceId correctly handles the nil fallback case. No data-loss or security risk. Score is 4 rather than 5 because the project's CLAUDE.md regression-test policy (two-commit structure) isn't followed, and the existing Python socket test infrastructure makes the test practical to write.
  • Sources/TerminalController.swift — four identical 9-line blocks could be extracted into a helper, and regression tests should be added per project policy.

Important Files Changed

Filename Overview
Sources/TerminalController.swift Four socket-command handlers (v2SurfaceSendText, v2SurfaceSendKey, v2SurfaceClearHistory, v2SurfaceReadText) updated to return not_found when an explicit surface_id fails to resolve via v2ResolveHandleRef, instead of silently falling back to ws.focusedPanelId. Logic is correct and backward compatible; minor style issues: non-idiomatic guard optional != nil (vs. guard let), missing data payload in the new error, and no regression tests added per CLAUDE.md policy.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Socket command received\nwith params] --> B{params has\nsurface_id?}
    B -- Yes --> C[v2UUID resolves\nsurface_id]
    C --> D{UUID resolved?}
    D -- No --> E[return not_found\n'Surface not found for\nthe given surface_id']
    D -- Yes --> F[surfaceId = resolved UUID]
    B -- No --> G[surfaceId = ws.focusedPanelId\nfallback preserved]
    F --> H{surfaceId != nil?}
    G --> H
    H -- No --> I[return not_found\n'No focused surface']
    H -- Yes --> J[ws.terminalPanel lookup]
    J --> K{Is terminal\npanel?}
    K -- No --> L[return invalid_params\n'Surface is not a terminal']
    K -- Yes --> M[Execute command\nsend_text / send_key /\nclear_history / read_text]
Loading

Reviews (1): Last reviewed commit: "fix(socket): return not_found error when..." | Re-trigger Greptile

Comment on lines +5284 to +5290
guard surfaceId != nil else {
result = .err(code: "not_found", message: "Surface not found for the given surface_id", data: nil)
return
}
} else {
surfaceId = ws.focusedPanelId
}

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.

P2 Non-idiomatic optional check duplicated 4×

The guard surfaceId != nil pattern leaves surfaceId as a UUID? even after the guard passes, which is why the outer guard let surfaceId is still needed. The more idiomatic Swift approach combines both into a single binding and eliminates the double lookup:

if params["surface_id"] != nil {
    guard let resolvedId = v2UUID(params, "surface_id") else {
        result = .err(code: "not_found", message: "Surface not found for the given surface_id", data: nil)
        return
    }
    surfaceId = resolvedId
} else {
    surfaceId = ws.focusedPanelId
}

This avoids the guard <optional> != nil compiler-warning-prone pattern and makes the flow clearer. The same refactor applies to the identical blocks in v2SurfaceSendKey (line ~5344), v2SurfaceClearHistory (line ~5387), and v2SurfaceReadText (line ~5447).

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +5281 to +5290
let surfaceId: UUID?
if params["surface_id"] != nil {
surfaceId = v2UUID(params, "surface_id")
guard surfaceId != nil else {
result = .err(code: "not_found", message: "Surface not found for the given surface_id", data: nil)
return
}
} else {
surfaceId = ws.focusedPanelId
}

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.

P2 Regression tests missing per CLAUDE.md policy

CLAUDE.md specifies a two-commit regression-test policy for bug fixes (failing test first, then fix). The Python socket test infrastructure in tests_v2/ is capable of exercising this exact path — e.g., calling surface.send_text with a bogus surface_id like "surface:9999" and asserting the response code is not_found.

Both #2042 and #2045 are testable end-to-end through the existing cmux.py client. If a test is impractical for some infra reason, that should be stated explicitly per the policy.

This same observation applies to all four affected functions.

if params["surface_id"] != nil {
surfaceId = v2UUID(params, "surface_id")
guard surfaceId != nil else {
result = .err(code: "not_found", message: "Surface not found for the given surface_id", data: nil)

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.

P2 Error response omits raw surface_id in data

The not_found error response here omits the input value in data: nil. Downstream callers and CLI error output would benefit from knowing which surface_id string failed to resolve. All other surface-related errors in these functions (e.g., invalid_params: Surface is not a terminal) include the resolved UUID. Including the raw string here would aid debugging:

result = .err(
    code: "not_found",
    message: "Surface not found for the given surface_id",
    data: ["surface_id": params["surface_id"] as? String ?? ""]
)

The same applies to all four occurrences (lines ~5345, ~5388, ~5448).

@cubic-dev-ai cubic-dev-ai 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.

No issues found across 1 file

@lawrencecchen
lawrencecchen merged commit 71f0e69 into manaflow-ai:main Mar 27, 2026
4 of 5 checks passed
@lawrencecchen

Copy link
Copy Markdown
Contributor

Thank you for the contribution!

bn-l pushed a commit to bn-l/cmux that referenced this pull request Apr 3, 2026
…nresolvable (manaflow-ai#2150)

Previously, v2SurfaceSendText, v2SurfaceSendKey, v2SurfaceClearHistory, and
v2SurfaceReadText would silently fall back to ws.focusedPanelId when a caller
supplied a surface_id that could not be resolved (e.g. a stale ref or an ordinal
whose mapping had not yet been registered). This caused two distinct bugs:

- manaflow-ai#2042: Commands like `cmux send --surface surface:9999` would succeed (exit 0)
  and deliver input to the focused pane instead of returning an error, making
  automation that targets specific surfaces unreliable.

- manaflow-ai#2045: When the fallback landed on a browser panel, the subsequent
  ws.terminalPanel(for:) check failed and returned "Surface is not a terminal",
  making valid terminal surfaces appear broken when addressed by ref.

The fix adds an explicit check: if params["surface_id"] is present but
v2UUID() returns nil (resolution failure), we immediately return a not_found
error instead of falling back to the focused pane. When surface_id is absent,
the existing focused-pane fallback is preserved for backward compatibility.

Fixes manaflow-ai#2042, Fixes manaflow-ai#2045

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants