Skip to content

broker(pty): replace TerminalQueryParser with alacritty EventListener#879

Merged
willwashburn merged 4 commits into
mainfrom
worktree-agent-a496ab143c411ff7c
May 18, 2026
Merged

broker(pty): replace TerminalQueryParser with alacritty EventListener#879
willwashburn merged 4 commits into
mainfrom
worktree-agent-a496ab143c411ff7c

Conversation

@willwashburn
Copy link
Copy Markdown
Member

Closes #867.

Summary

  • Replaces VoidListener with a new RelayEventListener that captures alacritty Event::PtyWrite (DSR / DA1 / DA2 / CPR responses) and writes them back to the PTY's stdin via an std::sync::mpsc channel drained by a dedicated thread.
  • CPR responses now reflect the real cursor position from the VT grid instead of the hardcoded 1;1 the hand-rolled parser always returned.
  • Deletes TerminalQueryParser, TerminalQueryState, and terminal_query_responses from helpers.rs, plus all call sites in pty_worker.rs, wrap.rs, and the main.rs test module.
  • Makes Snapshot::from_term generic over EventListener so offline snapshot tests still use VoidListener while live capture uses RelayEventListener.

Why this shape

  • send_event runs inside Processor::advance (sync), so the channel must be non-blocking. std::sync::mpsc keeps the listener Sync, never blocks, and avoids requiring a tokio runtime in PtySession::spawn. The drainer is a std::thread matching the existing reader-thread shape; it exits when term (and its listener / sender) is dropped at session teardown.
  • The drainer takes the writer lock, but the reader thread does not hold writer while in processor.advance, and resize doesn't touch writer either — so no deadlock with the existing reader.

Test plan

  • cargo build clean.
  • cargo test — 615 pass / 2 ignored / 0 fail.
  • cargo clippy --all-targets -- -D warnings clean.
  • New unit: listener_answers_dsr_with_terminal_okESC[5nESC[0n lands at the writer.
  • New unit: listener_answers_cpr_with_real_cursor_positionESC[3;5H ESC[6nESC[3;5R (proves CPR is grid-accurate, not hardcoded).

🤖 Generated with Claude Code

…#867)

PR #836 wired the alacritty VT grid into every PtySession but passed
VoidListener to Term::new, so alacritty parsed query sequences
(DSR/DA1/DA2/CPR) and discarded the responses. A parallel hand-rolled
TerminalQueryParser in helpers.rs answered those queries with a
hardcoded 1;1 CPR reply that ignored the real cursor position.

Replace VoidListener with RelayEventListener, which owns a
std::sync::mpsc::Sender<Vec<u8>>. alacritty's send_event(PtyWrite)
hands query response bytes to the channel; a dedicated drainer thread
takes the writer lock and pushes them down the PTY. The listener is
non-blocking so it is safe to call while processor+term locks are
held, and the drainer exits when the listener is dropped at
PtySession teardown.

CPR responses now reflect the live cursor position (verified by the
new unit test: ESC[3;5H ESC[6n yields ESC[3;5R, not ESC[1;1R).

Delete the hand-rolled parser and its tests in helpers.rs, plus its
three call sites in pty_worker.rs, wrap.rs, and the test module in
main.rs. Make Snapshot::from_term generic over EventListener so it
accepts both Term<RelayEventListener> from a live PtySession and the
Term<VoidListener> used in offline tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@willwashburn willwashburn requested a review from khaliqgant as a code owner May 18, 2026 02:02
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 18, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9debdf6e-81b7-4aee-ae5d-6dc10af211c7

📥 Commits

Reviewing files that changed from the base of the PR and between c95a609 and 51c9372.

📒 Files selected for processing (3)
  • .trajectories/completed/2026-05/traj_piik8r6zu3i7.json
  • .trajectories/index.json
  • src/pty.rs
✅ Files skipped from review due to trivial changes (2)
  • .trajectories/completed/2026-05/traj_piik8r6zu3i7.json
  • .trajectories/index.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/pty.rs

📝 Walkthrough

Walkthrough

Replaces a hand-rolled terminal query parser with Alacritty's RelayEventListener. Adds a bounded mpsc writeback channel drained by a dedicated thread, generalizes Snapshot::from_term to any EventListener, removes parser code and tests, and adds wiring tests verifying DSR/DA1/CPR behavior.

Changes

RelayEventListener Integration

Layer / File(s) Summary
RelayEventListener type definition
src/pty.rs
Imports Event/EventListener, defines RelayEventListener with a bounded sync sender and non-blocking try_send forwarding of Event::PtyWrite bytes.
PtySession wiring & writeback drainer
src/pty.rs
PtySession now uses Term<RelayEventListener>; spawn() creates a bounded std::sync::mpsc channel, constructs the listener, stores the session writer_arc, and spawns a drainer thread that recv()s writeback bytes and writes+flushes them to the PTY. with_term signature updated and unit wiring tests added (DSR/DA1/CPR).
Snapshot::from_term generalization
src/snapshot.rs
Snapshot::from_term is now generic over L: EventListener, allowing snapshots from Term<RelayEventListener> and Term<VoidListener>.
Remove TerminalQueryParser infrastructure
src/helpers.rs, src/main.rs, src/pty_worker.rs, src/wrap.rs
Deletes the hand-rolled TerminalQueryParser, its state/tests and helper functions; removes parser call sites and related imports/tests; adds comments that RelayEventListener answers terminal queries.
Trajectory documentation
.trajectories/completed/2026-05/traj_piik8r6zu3i7.json, .trajectories/completed/2026-05/traj_piik8r6zu3i7.md, .trajectories/index.json
Adds completed trajectory record, write-up, and updates index with new entry and lastUpdated timestamp.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • AgentWorkforce/relay#836: Landed the alacritty integration this PR builds on; both refactor PTY terminal handling to use alacritty::Term and remove hand-rolled parsing.
  • AgentWorkforce/relay#870: Introduced Snapshot::from_term tied to Term<VoidListener} that this PR generalizes to accept Term<L: EventListener>.

Suggested reviewers

  • khaliqgant

Poem

🐰 I watched the parser hop away,

Alacritty listens now each day.
Threads carry answers, neat and fleet,
CPR returns the cursor's seat.
Hooray — the relay keeps things spry and gay!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: replacing TerminalQueryParser with an alacritty EventListener in the PTY broker module.
Description check ✅ Passed The description follows the template with complete sections: summary lists all key changes, test plan is comprehensive with specific test names, and all sections are well-filled.
Linked Issues check ✅ Passed All coding requirements from issue #867 are met: RelayEventListener replaces TerminalQueryParser, event routing handles DSR/DA1/DA2/CPR, old parser code is deleted, Snapshot::from_term is generalized, and all unit tests pass.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #867: RelayEventListener implementation, TerminalQueryParser removal, call site cleanup, and helper generalization with no tangential modifications.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ 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 worktree-agent-a496ab143c411ff7c

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

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

No issues found across 9 files

Re-trigger cubic

Copy link
Copy Markdown

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

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: c95a6095d0

ℹ️ 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/pty.rs Outdated
// (DSR/DA1/DA2/CPR). The listener is sync and non-blocking; the
// drainer thread below takes the writer lock and pushes the
// bytes down the PTY.
let (writeback_tx, writeback_rx) = std_mpsc::channel::<Vec<u8>>();
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 Bound PTY writeback queue to avoid unbounded memory growth

Using std::sync::mpsc::channel here creates an unbounded buffer between parser events and the drainer thread. If the child emits many query sequences (for example repeated ESC[6n) while not draining stdin fast enough, the drainer can block on write_all and the reader thread will continue enqueueing Event::PtyWrite responses, causing memory usage to grow without limit and potentially OOM the broker.

Useful? React with 👍 / 👎.

Comment thread .trajectories/index.json Outdated
"status": "completed",
"startedAt": "2026-05-18T01:56:18.236Z",
"completedAt": "2026-05-18T02:01:49.991Z",
"path": "/Users/will/Projects/AgentWorkforce/relay/.claude/worktrees/agent-a496ab143c411ff7c/.trajectories/completed/2026-05/traj_piik8r6zu3i7.json"
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 Store trajectory index paths as repository-relative paths

This new index entry records a machine-specific absolute path (/Users/will/...) instead of the repo-relative .trajectories/... format used by neighboring entries. On other environments where that absolute path does not exist, consumers that read index paths directly cannot open this trajectory record even though the file is present in the repository.

Useful? React with 👍 / 👎.

coderabbitai[bot]

This comment was marked as resolved.

Addresses PR #879 review:
- Bound writeback channel via sync_channel(128) + try_send. Prevents
  unbounded memory growth if the child floods query sequences while
  the drainer is stuck on write_all. Overflow logs a warn and drops
  the response.
- Add listener_answers_da1_with_vt102_ident regression test so DA1
  (startup-critical for many CLIs) is exercised before the old
  TerminalQueryParser path is removed.
- Strip absolute /Users/... paths from .trajectories metadata
  (index.json, traj_piik8r6zu3i7.json projectId) — repo-relative now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@willwashburn
Copy link
Copy Markdown
Member Author

Thanks for the reviews — pushed 71358b1d addressing the actionable items.

Fixed:

  • [codex P1 — bounded writeback queue] Switched std::sync::mpsc::channelsync_channel(128) and the listener to try_send. A full queue logs a tracing::warn! and drops the response rather than growing unbounded if the drainer is stuck behind a blocked write_all. Listener stays non-blocking inside Processor::advance. (src/pty.rs)
  • [CodeRabbit Minor — missing DA1 regression test] Added listener_answers_da1_with_vt102_ident. alacritty identifies as a VT102 (ESC[?6c) rather than xterm's ESC[?1;2c; test asserts the actual reply so DA1 regressions surface immediately. (src/pty.rs)
  • [codex P2 / CodeRabbit Major — absolute paths in trajectory metadata] Replaced /Users/will/... in .trajectories/index.json path and traj_piik8r6zu3i7.json projectId with repo-relative values.

Declined (with reason):

  • [CodeRabbit Heavy lift — drainer/write_all share writer mutex] Real concern, but the fix (route all writes through a unified priority queue so protocol replies preempt bulk stdin) is a separate refactor that touches every write_all caller and should not be bundled into this listener-swap PR. In practice the risk is bounded: the drainer's writes are tiny (≤8 bytes for DSR/DA1, ≤16 for CPR) so even if it has to wait behind a bulk stdin write, the wait is short and the bounded queue above prevents memory blowup during the wait. Will file a follow-up issue for the unified writer queue.

CI green again (cubic + CodeRabbit), 616 tests pass, clippy clean.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 5 additional findings.

Open in Devin Review

@willwashburn willwashburn merged commit 885df69 into main May 18, 2026
3 checks passed
@willwashburn willwashburn deleted the worktree-agent-a496ab143c411ff7c branch May 18, 2026 02:34
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.

broker: replace hand-rolled TerminalQueryParser with an alacritty EventListener

1 participant