Skip to content

fix(opencode): follow the attached tmux pane on the execution host - #24737

Merged
nwparker merged 6 commits into
mainfrom
nwparker/OpenCode-Tmux-Status
Oct 3, 2026
Merged

nwparker merged 6 commits into
mainfrom
nwparker/OpenCode-Tmux-Status

Conversation

@nwparker

@nwparker nwparker commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 11 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​892 0 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​892
Prod 37 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1348 $\color{#cf222e}{\Huge{\mathbf{−}}}$​85 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1263

ELI5

When OpenCode runs inside tmux, Orca now follows the agent in the pane that the attached tmux client is showing. An agent waiting in another pane no longer changes the visible agent's status.

What Changed

The execution host validates the managed terminal's process identity, foreground tmux client, socket owner and selected pane. Its hook server keeps inner observations and the selected outer status in its existing canonical store. Switching to an unattested pane clears the displayed status; losing contact retains the evidence without claiming the process exited. Replays preserve the original observation age, and certified terminal retirement clears the matching subjects.

The plugin supplies optional tmux socket/pane metadata without spawning commands. Local macOS/Linux and SSH hosts use the same bounded probe; Windows/WSL local probing is excluded. Existing ordinary hook ingress remains behind its current adapter. The lifecycle fixes in #24612 are merged; this PR targets main and contains the tmux changes. A resumed refresh also rechecks its exact pane owner after the asynchronous root lookup, so a retired refresh cannot erase newer canonical status.

Why

All inner agents inherit one outer Orca pane key, so the previous last hook to arrive won even when it belonged to an inactive tmux pane. Choosing on the execution host keeps sidebar, CLI, mobile and dashboard readers on one authority. Terminal title guesses or a reader-side precedence rule cannot establish the attached client's selected pane.

Linked Issue

Fixes #10039

Visual Proof

Before: real SSH OpenCode 1.18.34, optional tmux metadata disabled with env -u TMUX -u TMUX_PANE to exercise the existing compatibility path. The selected pane has completed A, but a newly started inactive D asks for shell permission and the tab incorrectly shows Waiting. An earlier installed v2.0.16 baseline collision was also recorded privately.

Before: inactive permission overrides completed selected agent

After: real SSH agents with the optional metadata; selected A shows Done while B is waiting. Selecting B shows its genuine permission request.

After: selected A Done
After: selected B Waiting

Testing

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

Hidden isolated macOS Orca with installed OpenCode v2.0.16: selected A Done, B Waiting, unhooked C unavailable, return to A with the original observation time, detach unavailable. Real SSH Linux OpenCode 1.18.34: same selected transitions through the built relay, main listener and renderer; disconnected relay consumer replays unavailable. Final rebuilt head3389a867a37 with relay0.1.0+044fcd58921b repeated A/B/C and renderer disconnect/reconnect unavailable proof. Returning A retained approximately141 seconds of original evidence age (1ms relative-clock transit difference). A production SSH multiplexer client independently exercised the actual managed PTYs and reconnect. Linux v2 binaries could not be downloaded from the official release endpoint; these are v1 SSH proofs, not a claimed v2 Linux run.

234 tests across 19 suites passed, including canonical takeover of an existing OpenCode 2 status, unavailable replay, launch identity attestation, retirement, unrelated structured subjects, bounded process batching, fair socket probing and lifecycle regression. Node/web typechecks, changed-code quality gate, Electron build and relay builds passed. Full mobile/web preparation is limited by missing workspace dependencies; no complete pnpm build claim. Windows/WSL runtime validation is reported separately, not inferred from these tests.

Review

Execution host owns all status selection. No new stream opcode. SSH relays use immutable content-hash installation and strict launch-version negotiation. Probes use targeted PIDs, bounded output/timeouts/concurrency and a fair rotation over active sockets. Folder workspaces are exercised. No window activation or user agent configuration changes.

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

Current reviewed head includes the post-await ownership correction from d41d53ee15dd4275de72652efc6f4224591c8306. A refresh suspended while resolving a terminal root now checks that its exact pane owner still exists and has not stopped before clearing or publishing status. This preserves a newer Waiting result after terminal retirement or owner replacement. The inner-subject predicate moved without changing its behavior; private standalone-status changes are excluded.

Four independent exact-source/canonical-store controls reproduced Waiting revision3 becoming empty revision4 on the previous public head. All four preserve Waiting revision3 with no clear on the prepared head, for clearPane/stop and same/replacement roots. These are controlled asynchronous boundaries; the real macOS/SSH CLI and renderer proof above remains the earlier scope. The owner also passed18 focused tmux tests and Node typecheck/quality after the correction. Fresh full-head CI is required before merge.

Prepared against named main 84a4e8a9b0a1a0bd938528aa2dc6e49636917a47: all48 owned paths match the focused corrected public tree, and every other main entry and file mode is unchanged. Original broader status/WSL proposals #9318/#13385/#10066 remain open for separate validation.

Checklist

  • This PR is focused on tmux status selection
  • User-facing before/after, mechanism and approach explained
  • Before/after screenshots attached
  • Self-reviewed for correctness, security and performance
  • Cross-platform, SSH/remote and path impact considered
  • Full-head checks: CI after this update

@nwparker
nwparker force-pushed the nwparker/OpenCode-Tmux-Status branch from 3389a86 to 085227f Compare October 2, 2026 12:04
@nwparker
nwparker changed the base branch from nwparker/OpenCode-Agent-Status to main October 2, 2026 12:04
@nwparker
nwparker marked this pull request as ready for review October 2, 2026 12:05
@pullfrog

pullfrog Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

This run croaked 😵

The workflow encountered an error before any progress could be reported. Please check the link below for details.

Pullfrog  | Rerun failed job ➔ | View workflow run | via Pullfrog | 𝕏

@coderabbitai

coderabbitai Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

📝 Walkthrough

Walkthrough

The change adds tmux metadata and managed-PTY resolution for OpenCode hooks. It tracks hook status by inner tmux pane and projects the attached pane’s status to the outer pane. Local and relay servers store, replay, and clear canonical tmux status. Relay envelopes also carry evidence age and unavailable-status information through SSH and remote ingestion.

Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to bd982

The change routes tmux-aware OpenCode status through the execution host and adds evidence-age and unavailable-status transport. The remaining notes are a misplaced doc comment and a small timer-cleanup hardening suggestion. No merge-blocking behavior problem has been identified.

Security Architecture Review

Security architecture risk: 🔵 Low · up to bd982

The change checks the managed terminal and attached tmux pane before selecting its status, and restricts remote clearing to matching ownership information. No new execution privilege was demonstrated. Some uncertainty remains about authenticating the reporting process and how downstream automation uses status.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — A holder of the shared relay hook token can address managed terminals served by that relay, including terminals in different workspaces when their identifiers are known. Target-root and workspace checks constrain acceptance but do not prove sender ownership. Remote unavailable messages cannot clear a status belonging to a different connection under the inspected matching checks.

Security Findings and Attack Paths

  • inferred — A token-bearing process could submit another terminal's pane, workspace, and tmux identifiers; attachment validation authenticates the target relationship rather than the reporting process. The available main path already accepted caller-selected status identity under the shared token. No introduced worsening or execution-authority gain was established; downstream automation remains a coverage gap, not a verified attack outcome.

Trust Boundaries and Controls

  • observed — Controls include authentication before body processing, tmux metadata syntax checks, matching managed-root workspace, same-account socket ownership, and unique client attachment. Client resolution checks live process state, TTY, foreground process group, tmux command identity, and ancestry reaching the managed root.

Resilience and Maintainability Implications

  • observed — Refresh completions recheck stopped state, tracked-object identity, retirement, and root PID/incarnation before projection. Failed probes retain prior evidence rather than claiming exit. Local retirement cleanup removes inner and selected subjects; relay replay checks retirement before publication.

Hardening Proposals

  • proposed — If status consumers authorize execution or restoration, require independent live terminal/session attestation rather than treating selected hook status as authenticated sender authority. Stronger per-terminal authentication would need an explicit isolation model, especially where terminals share an operating-system account.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 30 functions across 48 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed #10039 requires pane selection to follow the attached tmux client and prevents inactive agents from overwriting visible status. TmuxAgentHookOwner stores observations under distinct inner-pane subje…
Out of Scope Changes check ✅ Passed The changes support #10039’s tmux status selection across local hosts and SSH. The hook metadata, bounded attachment probe, canonical status and relay handling, lifecycle cleanup, and tests all implem…
Title check ✅ Passed The title clearly summarizes the main change: selecting tmux status based on the attached pane on the execution host.
Description check ✅ Passed The description covers the user-facing change, implementation, rationale, linked issue, visual proof, testing, and compatibility considerations. It also states that full-head CI is still pending and t…
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

Preserves all 47 reviewed PR source/test blobs exactly; main owns the workflow package-variable contract fix.
@nwparker

nwparker commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

Updated to c83934119aa8a133b9d38e0e3e00ba0fe3d0b937 with current main's E2E package-variable contract fix. All 47 reviewed product/test post-blobs remain byte-identical. The repeated shard failure checked package names in the installer script after the workflow moved them to ORCA_E2E_APT_PACKAGES; this update retains main's corrected contract test. New-head CI is running.

@pullfrog pullfrog 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.

✅ No new issues found.

Reviewed changes

This PR has no prior Pullfrog review, so the review covers the full 47-file diff at c8393411. The only commit since the previously pushed head 085227f6 is merge c8393411 ("Merge current main to retain the corrected E2E dependency contract"); git diff 085227f6 c8393411 over the PR's files is empty, so every PR blob is unchanged and the reviewed source is the same feature that was independently reviewed.

  • Added the execution-host tmux selection owner (TmuxAgentHookOwner): inner observations live in the canonical store under tmux-inner:* subjects and only the pane the attached client shows is projected.
  • Added bounded, same-uid attachment probing (tmux-host-attachment-probe) and strict client resolution (tmux-client-attachment) — targeted PIDs, no whole-host scan, full foreground proof required.
  • Wired the owner into the main hook server and the SSH relay, with certified retirement / tab close / pane clear removing the matching inner and outer subjects.
  • Added optional evidenceAgeMs / statusUnavailable wire fields plus the AgentHookUnavailableEnvelope (payload: null), preserving the original observation age across replay and clearing only the matching host/workspace/generation.
  • Updated the renderer to drop a completed projection only on an explicit host statusUnavailable clear.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (2)
src/relay/agent-hook-server-contract.ts (1)

20-27: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move the JSDoc block to isPaneSurfaceRetired.

The doc comment describes isPaneSurfaceRetired. It now sits above getTmuxManagedPty, so editors show the retirement description on the wrong option. Place getTmuxManagedPty before the comment, or give it its own comment.

src/shared/tmux-agent-hook-owner.ts (1)

306-311: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Stop the refresh interval in stop(). Do not rely on the last clearPane call to stop it.

stop() calls clearPane for each outer pane. clearPane clears this.timer only after the last outer pane is removed. If stop() runs when outers is empty but timer is still set, the interval stays alive. Each tick then calls refresh(), and refresh() returns early because stopped is true. unref prevents a process hang, but the owner can still leak a timer. Clear the interval in stop() directly.

Proposed fix
   stop(): void {
     this.stopped = true
     for (const paneKey of this.outers.keys()) {
       this.clearPane(paneKey)
     }
+    clearInterval(this.timer)
+    this.timer = undefined
   }

ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 6a2972b3-eabd-4503-8548-d8ecdbd89802
📥 Commits

Reviewing files that changed from the base of the PR and between 84a4e8a and bd9820f.

📒 Files selected for processing (48)
  • src/main/agent-hooks/local-tmux-managed-pty.test.ts
  • src/main/agent-hooks/local-tmux-managed-pty.ts
  • src/main/agent-hooks/server-tmux-canonical.test.ts
  • src/main/agent-hooks/server-tmux-wire.test.ts
  • src/main/agent-hooks/server/server-authority-fences.ts
  • src/main/agent-hooks/server/server-cleanup.ts
  • src/main/agent-hooks/server/server-ingest-normalization.ts
  • src/main/agent-hooks/server/server-ingest-remote.ts
  • src/main/agent-hooks/server/server-lifecycle.ts
  • src/main/agent-hooks/server/server-listeners.ts
  • src/main/agent-hooks/server/server-state.ts
  • src/main/agent-hooks/server/server-structured-status-row.ts
  • src/main/agent-hooks/server/server-tab-cleanup.ts
  • src/main/agent-hooks/server/server-tmux-status.ts
  • src/main/opencode/status-plugin-post-source.ts
  • src/main/ssh/ssh-relay-session-agent-hooks.integration.test.ts
  • src/main/ssh/ssh-relay-session.ts
  • src/main/startup/main-process-pty-startup.ts
  • src/relay/agent-hook-canonical-status.test.ts
  • src/relay/agent-hook-canonical-status.ts
  • src/relay/agent-hook-envelope-build.ts
  • src/relay/agent-hook-envelope-publication.ts
  • src/relay/agent-hook-request.ts
  • src/relay/agent-hook-server-contract.ts
  • src/relay/agent-hook-server.ts
  • src/relay/agent-hook-spool-ingest.ts
  • src/relay/pty-handler.ts
  • src/relay/relay-agent-hook-runtime.ts
  • src/relay/relay-tmux-hook-owner.ts
  • src/renderer/src/hooks/ipc-events/agent-status-listeners.ts
  • src/renderer/src/hooks/ipc-events/agent-status-unavailable.test.ts
  • src/shared/agent-hook-listener.ts
  • src/shared/agent-hook-listener/listener-event.ts
  • src/shared/agent-hook-listener/provider-dispatch.ts
  • src/shared/agent-hook-listener/providers/opencode-family-events.ts
  • src/shared/agent-hook-relay.ts
  • src/shared/agent-status-ipc-payload.ts
  • src/shared/agent-status-store-parent.test.ts
  • src/shared/agent-status-store-parent.ts
  • src/shared/tmux-agent-hook-owner.test.ts
  • src/shared/tmux-agent-hook-owner.ts
  • src/shared/tmux-client-attachment.test.ts
  • src/shared/tmux-client-attachment.ts
  • src/shared/tmux-hook-refresh-retirement.test.ts
  • src/shared/tmux-host-attachment-probe.test.ts
  • src/shared/tmux-host-attachment-probe.ts
  • src/shared/tmux-selected-status.ts
  • src/shared/tmux-status.test-fixture.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 1 remain after this review.

@pullfrog pullfrog 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.

✅ No new issues found.

Reviewed changes

Incremental re-review of the commits pushed after the prior Pullfrog review at c8393411. The net PR-authored delta is the awaited-authority fence plus its regression test; the other delta commit is a merge of main that touched no PR-authored file.

  • Fenced refresh after the awaited root lookup: TmuxAgentHookOwner.refreshAttachments now re-checks this.stopped and that the refreshing outer is still the one registered for its paneKey after await this.options.getRoot(...). A refresh suspended across certified retirement or owner replacement can no longer call unavailable/clearPane and erase newer canonical status.
  • Relocated isTmuxInnerSubject: moved from tmux-agent-hook-owner.ts to tmux-selected-status.ts and re-exported from the owner, so both existing importers (src/relay/agent-hook-canonical-status.ts, src/main/agent-hooks/server/server-listeners.ts) resolve unchanged.
  • Regression coverage: new src/shared/tmux-hook-refresh-retirement.test.ts drives the race across clear-pane/stop × same/replacement root. Ablation confirms all four cases fail without the guard (revision 3→4 and the status row cleared), and the related tmux suites pass.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@nwparker
nwparker merged commit f7b1f9d into main Oct 3, 2026
60 checks passed
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.

[Bug]: OpenCode status does not follow active tmux window; multiple inner agents overwrite the same outer pane status

1 participant