Skip to content

fix(terminal): keystrokes typed while a terminal opens are lost; node PTY relay lifecycle races#107214

Merged
steipete merged 2 commits into
mainfrom
claude/orca-tech-stack-347469
Jul 14, 2026
Merged

fix(terminal): keystrokes typed while a terminal opens are lost; node PTY relay lifecycle races#107214
steipete merged 2 commits into
mainfrom
claude/orca-tech-stack-347469

Conversation

@steipete

Copy link
Copy Markdown
Contributor

What Problem This Solves

Fixes an issue where keystrokes typed into a freshly opened Control UI terminal were silently dropped until the gateway session finished opening. For "open in terminal" on Claude/Codex catalog sessions that live on a paired node, that window covers a remote eligibility check plus a remote PTY spawn — user-visible seconds of lost input.

It also closes several lifecycle races in the node PTY relay and terminal session management found while auditing the paths end to end:

  • The relay invoke was not pinned to the node connection that authorization checked, so a node reconnect between the final auth re-check and dispatch could send the resume command over a connection that was never authorized (the registry already had expectedConnId/ROUTE_CHANGED for exactly this; the relay never used it).
  • Node-side input frames arriving after PTY exit wrote to a dead PTY with no guard (the data/exit paths had one; input did not).
  • A throwing resize on a dead PTY left a zombie session alive, while a throwing write correctly finalized it.
  • Scrollback-ring and relay-prelude truncation could split a UTF-16 surrogate pair, replaying permanent mojibake into a reattached terminal.
  • The relay's pre-registration output buffer was unbounded.
  • The protocol schema claimed clients use terminal.data seq for gap detection; no client does (transport is ordered) — the description now states the real contract.

Why This Change Was Made

Hardening pass over the embedded terminal and the node PTY forwarding path, keeping each fix at its owner boundary: connection pinning in the gateway relay/open handler, input guarding on the node host, buffering in the Control UI panel, and truncation safety in one shared surrogateSafeTail helper. Startup input buffering is bounded (8 KiB) and latches off on overflow so replayed input can never be reordered or partially truncated. No protocol shape, config, or plugin SDK changes.

User Impact

Typing immediately after opening a terminal — especially resuming a Claude/Codex session on a paired node — now reaches the shell instead of vanishing. Dead terminals are torn down promptly on resize, reattach replay no longer corrupts wide characters, and a node reconnect during open fails closed with a visible error instead of dispatching over an unauthorized route.

Evidence

  • Focused suites, all green: node scripts/run-vitest.mjs src/gateway/terminal src/gateway/node-registry.test.ts src/gateway/server-methods/terminal.test.ts src/node-host/pty-command.test.ts src/node-host/invoke-payload.test.ts src/node-host/runner.test.ts → 6 files, 121 tests; node scripts/run-vitest.mjs ui/src/components/terminal → 3 files, 32 tests.
  • New regression coverage: connection pinning + ROUTE_CHANGED surfacing, seq-gap buffering bounds (128-frame cap → idle teardown), input-after-exit/abort no-ops, out-of-range resize rejection, coerceNodeInvokeInputPayload bounds, node.invoke.input/cancel/close runner routing, seq monotonicity across detach→attach, surrogate-safe truncation, startup-input flush ordering + overflow latch + failed-open discard.
  • Structured second-model review of the full bundle: clean, no actionable findings.
  • pnpm check:changed delegated gates: running at PR creation; will be green on the head SHA before merge.

@openclaw-barnacle openclaw-barnacle Bot added app: web-ui App: web-ui gateway Gateway runtime size: L maintainer Maintainer-authored PR labels Jul 14, 2026
@steipete
steipete force-pushed the claude/orca-tech-stack-347469 branch from 5ecc413 to bcd3716 Compare July 14, 2026 07:44
steipete added 2 commits July 14, 2026 01:01
Pin node relay invokes to the authorized node connection (ROUTE_CHANGED
guard), stop node-side input writes to settled/dead PTYs, finalize
sessions on resize failure like write failure, keep surrogate pairs
intact across scrollback and relay prelude truncation, bound the relay
pre-registration output buffer, and buffer Control UI keystrokes typed
while terminal.open/attach is in flight so slow node-relay opens no
longer drop input. Correct the terminal.data seq doc to its real
contract and pin seq monotonicity, seq-gap bounds, input coercion, and
runner event routing with new tests.
Move Control UI startup input buffering into terminal-startup-input.ts
(LOC budget), drop a stale session-manager comment, remove a map
spread, and fix test typings for event frames and typed mock access.
@steipete
steipete force-pushed the claude/orca-tech-stack-347469 branch from bcd3716 to 2247173 Compare July 14, 2026 08:04

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 22471737f1

ℹ️ 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 on lines +202 to +207
createBackend = async () =>
await createNodeRelayBackend({
registry: context.nodeRegistry,
nodeId: relay.plan.nodeId,
expectedConnId: access.node.connId,
command: relay.plan.command,

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 Badge Re-run policy before pinning replacement node

When an async plugin invoke policy/approval is pending and the node reconnects with the same terminal command, this final authorization picks up the replacement connection and expectedConnId pins the resume invoke to that new connId, even though applyPluginNodeInvokePolicy was evaluated against the earlier access.node. The policy helper's own invokeNode path treats a connId change as ROUTE_CHANGED, so this path should fail closed or rerun the plugin policy for the final node; otherwise a replacement node can receive a terminal command under policy/approval granted for a different connection.

Useful? React with 👍 / 👎.

@steipete
steipete merged commit 89f53f1 into main Jul 14, 2026
107 checks passed
@steipete
steipete deleted the claude/orca-tech-stack-347469 branch July 14, 2026 08:22
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jul 15, 2026
… PTY relay lifecycle races (openclaw#107214)

* fix(terminal): harden node PTY relay and terminal session lifecycle

Pin node relay invokes to the authorized node connection (ROUTE_CHANGED
guard), stop node-side input writes to settled/dead PTYs, finalize
sessions on resize failure like write failure, keep surrogate pairs
intact across scrollback and relay prelude truncation, bound the relay
pre-registration output buffer, and buffer Control UI keystrokes typed
while terminal.open/attach is in flight so slow node-relay opens no
longer drop input. Correct the terminal.data seq doc to its real
contract and pin seq monotonicity, seq-gap bounds, input coercion, and
runner event routing with new tests.

* refactor(terminal): extract startup input buffer and fix CI findings

Move Control UI startup input buffering into terminal-startup-input.ts
(LOC budget), drop a stale session-manager comment, remove a map
spread, and fix test typings for event frames and typed mock access.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app: web-ui App: web-ui gateway Gateway runtime maintainer Maintainer-authored PR size: L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant