Skip to content

wta-master: subscribe to WT pane-close events for push-based liveness#138

Closed
DDKinger wants to merge 1 commit into
mainfrom
dev/yuazha/master-pane-closed-subscribe
Closed

wta-master: subscribe to WT pane-close events for push-based liveness#138
DDKinger wants to merge 1 commit into
mainfrom
dev/yuazha/master-pane-closed-subscribe

Conversation

@DDKinger

Copy link
Copy Markdown
Contributor

Problem

When a WT pane hosting an agent session closes (Ctrl+Shift+W, tab close, etc.) the helper running inside that pane usually exits with the pane → master''s drop_sessions_for_helper reaps the registry rows → session_removed broadcasts and the F2 session row demotes. But for Gemini-class CLIs that don''t reliably exit on stdin EOF (or any case where the agent CLI subprocess outlives the pane), the helper pipe stays alive, drop_sessions_for_helper never fires, and the F2 row stays stuck at Idle/Live until WT restarts.

What this PR does

Master subscribes to the same wtcli listen event stream the helpers consume and turns connection_state: closed events into per-pane registry cleanup. Push-based, zero-latency counterpart to the existing per-helper reap path — no user action required, no polling overhead.

Architecture

WT closes pane
  ↓
TerminalPage emits connection_state:closed event (params.pane_id)
  ↓
wtcli --json listen subprocess (running under master) parses JSON
  ↓
run_master_event_drainer.recv() — picks up the JSON
  ↓
handle_master_wt_event filters: type=event + method=connection_state + state=closed
  extracts params.pane_id (with session_id fallback for old wtcli builds)
  ↓
drop_sessions_for_pane finds every registry row whose pane_session_id matches
  (case-insensitive). For each:
    • remove from session_to_helper
    • remove from registry  (gated: only broadcast if remove returned Some)
    • broadcast session_removed
  Trailing: broadcast sessions/changed (once, if any dropped)
  ↓
All helpers
  • session_removed → apply_master_session_ended → row → Ended
  • sessions/changed → refetch → UI repaints

Why connection_state: failed is NOT a drop

Helper-side keeps failed as Error/SessionEvent::ConnectionFailed, distinct from closed/Ended. Treating failed as a row-drop here would collapse two user-visible states into one.

Why we didn''t extend the WtChannel trait

The trait is intentionally minimal (request + is_available). Adding subscribe_events / start_event_reader would force the existing test fakes (MockWtChannel) to carry production-only event-API state. Holding the concrete Arc<CliChannel> as a local in run_master_mode is structurally simpler and confines the change to one site.

Tests (12 new, 573 total)

drop_sessions_for_pane (5):

  • drops all matching rows + emits trailing sessions/changed
  • no-op on no-match
  • case-insensitive pane id match
  • skips Historical / no-binding rows
  • idempotent on already-dropped sids (race guard)

handle_master_wt_event (7):

  • happy path: closed drops matching row
  • fallback to session_id when pane_id empty (old wtcli builds)
  • pane_id preferred when both present
  • ignores non-event messages
  • ignores non-connection_state methods (e.g. vt_sequence)
  • ignores non-closed states (failed, connected, unknown, ...)
  • ignores events with neither pane_id nor session_id

End-to-end drainer test deferred to manual smoke (would require spawning wtcli inside a real WT process). Handler tests cover all dispatch logic.

Out of scope

  • Killing wtcli --json listen on master shutdown (benign leak; master lifetime = WT process lifetime).
  • Hardening CliChannel against double-subscribe.
  • Per-session race guard (not needed: connection_state: closed arrives after WT closed the pane — a session whose pane is gone in WT cannot be reused regardless of how recently it was created. Compare to the F5 reconcile in wta: refresh in session view reconciles session list against wt live panes #121 which needs a guard because its alive-set is a snapshot.)

When a WT pane hosting an agent session closes (Ctrl+Shift+W, tab
close, etc.) the helper usually exits with the pane → master's
drop_sessions_for_helper reaps the registry rows → session_removed
broadcasts and the F2 row demotes. But for Gemini-class CLIs that
don''t reliably exit on stdin EOF, the helper pipe stays alive,
drop_sessions_for_helper never fires, and the F2 row stays stuck at
Idle/Live until WT restarts.

This PR has master subscribe to the same wtcli listen event stream
the helpers consume and turn connection_state: closed events into
per-pane registry cleanup. Push-based, zero-latency counterpart to
the existing per-helper reap path.

Components:
- drop_sessions_for_pane(state, pane_session_id) — per-sid drop
  helper, mirrors drop_sessions_for_helper but keys by pane.
  Case-insensitive pane match. Idempotent: per-sid session_removed
  broadcast gated on registry.remove(sid).is_some() so racing paths
  don''t fan out duplicates. Single trailing sessions/changed.
- handle_master_wt_event(state, &Value) — testable JSON dispatcher.
  Filters type=event + method=connection_state + state=closed.
  Extracts params["pane_id"] with params["session_id"] fallback
  (matches the helper path in main.rs:2059 — old wtcli builds emit
  session_id).
- run_master_event_drainer(state, ch) — long-running task,
  subscribes via CliChannel::subscribe_events + start_reader,
  forwards every event to handle_master_wt_event. Documents the
  single-subscriber assumption (master is the only subscriber on its
  own CliChannel; helpers each construct their own).
- run_master_mode wiring: hold the concrete Arc<CliChannel> as a
  local (no new MasterStateInner field — keeps test fakes unburdened)
  and spawn the drainer if CliChannel::connect succeeded.

Why connection_state: failed is NOT a drop: helper-side keeps failed
as Error / ConnectionFailed, distinct from closed / Ended. Treating
failed as a row-drop here would collapse two user-visible states.

Why we didn''t extend WtChannel trait: it''s intentionally minimal
(request + is_available). Adding event API would force test fakes
(MockWtChannel) to carry production-only state. Holding the concrete
Arc<CliChannel> as a local in run_master_mode is simpler.

Tests (12 new, 573 total):
- 5 drop_sessions_for_pane: drops all matching (with trailing
  sessions/changed), no-match no-op, case-insensitive, skips rows
  without pane binding, idempotent on already-dropped sids.
- 7 handle_master_wt_event: closed drops, session_id fallback,
  pane_id preferred when both present, ignores non-event messages,
  ignores non-connection_state methods, ignores non-closed states
  (failed/connected/unknown), ignores missing-id events.

End-to-end drainer deferred to manual smoke — handler tests cover
all dispatch logic without spawning wtcli.

Out of scope:
- Killing wtcli --json listen on master shutdown (benign leak;
  master lifetime == WT process lifetime).
- Hardening CliChannel against double-subscribe.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 30, 2026 07:41

Copilot AI 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.

Pull request overview

This PR adds master-side WT event listening so pane-close events can immediately clean stale live session rows when helper processes remain connected after their pane is gone.

Changes:

  • Keeps a concrete CliChannel in master mode to subscribe to WT events while still exposing the trait object for focus operations.
  • Adds a master WT event drainer that handles connection_state: closed events and drops matching registry rows by pane id.
  • Adds unit coverage for pane-based cleanup and event filtering/fallback behavior.

let mut map = state.session_to_helper.lock().await;
map.remove(sid);
}
if state.registry.remove(sid).await.is_some() {
@github-actions

Copy link
Copy Markdown

@check-spelling-bot Report

⚠️ Dictionary not found

Problems were encountered retrieving check dictionaries (cspell:python/src/additional_words.txt cspell:java/src/java.txt cspell:fullstack/dict/fullstack.txt cspell:swift/src/swift.txt cspell:svelte/dict/svelte.txt cspell:haskell/dict/haskell.txt cspell:ada/dict/ada.txt cspell:golang/dict/go.txt cspell:docker/src/docker-words.txt cspell:python/src/python/python.txt cspell:scala/dict/scala.txt cspell:rust/dict/rust.txt cspell:node/dict/node.txt cspell:python/src/common/extra.txt cspell:cpp/src/lang-jargon.txt cspell:cpp/src/compiler-gcc.txt cspell:cpp/src/lang-keywords.txt cspell:dart/src/dart.txt cspell:cpp/src/template-strings.txt cspell:ruby/dict/ruby.txt cspell:cpp/src/compiler-clang-attributes.txt cspell:cpp/src/stdlib-cpp.txt cspell:public-licenses/src/generated/public-licenses.txt cspell:typescript/dict/typescript.txt cspell:clojure/src/clojure.txt cspell:r/src/r.txt cspell:powershell/dict/powershell.txt cspell:css/dict/css.txt cspell:software-terms/dict/softwareTerms.txt cspell:npm/dict/npm.txt cspell:django/dict/django.txt cspell:redis/dict/redis.txt cspell:html/dict/html.txt cspell:lua/dict/lua.txt cspell:public-licenses/src/additional-licenses.txt cspell:sql/src/sql.txt cspell:cpp/src/ecosystem.txt cspell:python/src/python/python-lib.txt cspell:k8s/dict/k8s.txt cspell:latex/dict/latex.txt cspell:cpp/src/stdlib-c.txt cspell:dotnet/dict/dotnet.txt cspell:shell/dict/shell-all-words.txt cspell:java/src/java-terms.txt cspell:php/dict/php.txt cspell:software-terms/dict/webServices.txt cspell:cpp/src/people.txt cspell:gaming-terms/dict/gaming-terms.txt cspell:elixir/dict/elixir.txt cspell:cpp/src/compiler-msvc.txt cspell:cpp/src/stdlib-cmath.txt cspell:monkeyc/src/monkeyc_keywords.txt cspell:sql/src/tsql.txt cspell:cpp/src/stdlib-cerrno.txt).

⚠️ For more information, see check-dictionary-not-found.

🔴 Please review

See the 📂 files view, the 📜action log, 👼 SARIF report, or 📝 job summary for details.

❌ Errors and Warnings Count
⚠️ binary-file 6
⚠️ check-dictionary-not-found 54
❌ forbidden-pattern 13
⚠️ ignored-expect-variant 1
⚠️ noisy-file 7
⚠️ single-line-file 1

See ❌ Event descriptions for more information.

These words are not needed and should be removed Backgrounder Ccc cplusplus ctl Debian dotnet drv endptr EOFs evt Fullwidth gitlab hdr idl IME inbox intelligentterminal Ioctl KVM lbl lld lsb NONINFRINGEMENT notif oss outdir Podcast pri prioritization PSobject rcv segfault Signtool sourced SWP Tbl testname transitioning unk unparseable unregisters Virt VMs VTE webpage websites WTCLI xsi

Some files were automatically ignored 🙈

These sample patterns would exclude them:

^\.dotnet\/\.dotnet\/TelemetryStorageService/
^\Q.dotnet/.dotnet/.workloadAdvertisingManifestSentinel10.0.200\E$
^\Q.dotnet/.dotnet/10.0.201.aspNetCertificateSentinel\E$
^\Q.dotnet/.dotnet/10.0.201.dotnetFirstUseSentinel\E$
^\Q.dotnet/.dotnet/10.0.201.toolpath.sentinel\E$
^\Qinstaller/bootstrap/target/.rustc_info.json\E$
^copilot-version\.err$
^copilot-version\.out$

You should consider excluding directory paths (e.g. (?:^|/)vendor/), filenames (e.g. (?:^|/)yarn\.lock$), or file extensions (e.g. \.gz$)

You should consider adding them to:

.github/actions/spelling/excludes.txt

File matching is via Perl regular expressions.

To check these files, more of their words need to be in the dictionary than not. You can use patterns.txt to exclude portions, add items to the dictionary (e.g. by adding them to allow.txt), or fix typos.

To update file exclusions and remove the previously acknowledged and now absent words, you could run the following commands

... in a clone of the git@github.com:microsoft/intelligent-terminal.git repository
on the dev/yuazha/master-pane-closed-subscribe branch (ℹ️ how do I use this?):

curl -s -S -L 'https://raw.githubusercontent.com/check-spelling/check-spelling/cfb6f7e75bbfc89c71eaa30366d0c166f1bd9c8c/apply.pl' |
perl - 'https://github.com/microsoft/intelligent-terminal/actions/runs/26678329557/attempts/1' &&
git commit -m 'Update check-spelling metadata'
Available 📚 dictionaries could cover words (expected and unrecognized) not in the 📘 dictionary

This includes both expected items (2063) from .github/actions/spelling/expect/alphabet.txt .github/actions/spelling/expect/expect.txt .github/actions/spelling/expect/web.txt

Dictionary Entries Covers Uniquely
cspell:csharp/csharp.txt 32 2 2
cspell:aws/aws.txt 232 2 2
cspell:fonts/fonts.txt 536 1 1

Consider adding to the extra_dictionaries array (in the .github/actions/spelling/config.json file):

    "cspell:csharp/csharp.txt",
    "cspell:aws/aws.txt",
    "cspell:fonts/fonts.txt",

To stop checking additional dictionaries, put (in the .github/actions/spelling/config.json file):

"check_extra_dictionaries": []
Forbidden patterns 🙅 (8)

In order to address this, you could change the content to not match the forbidden patterns (comments before forbidden patterns may help explain why they're forbidden), add patterns for acceptable instances, or adjust the forbidden patterns themselves.

These forbidden patterns matched content:

Should be nonexistent
\b[Nn]o[nt][- ]existent\b
Should probably be Otherwise,
(?<=\. )Otherwise\s
Should be preexisting
[Pp]re[- ]existing
Complete sentences in parentheticals should not have a space before the period.
\s\.\)(?!.*\}\})
Should be ; otherwise or . Otherwise

https://study.com/learn/lesson/otherwise-in-a-sentence.html

, [Oo]therwise\b
Should be reentrant
[Rr]e[- ]entrant
Should be whether or not ...
(?i)\b(?:whe|ra)ther(?:\s\w+)+ or not\.
Should be WinGet
\bWinget\b
✏️ Contributor please read this

By default the command suggestion will generate a file named based on your commit. That's generally ok as long as you add the file to your commit. Someone can reorganize it later.

If the listed items are:

  • ... misspelled, then please correct them instead of using the command.
  • ... names, please add them to .github/actions/spelling/allow/names.txt.
  • ... APIs, you can add them to a file in .github/actions/spelling/allow/.
  • ... just things you're using, please add them to an appropriate file in .github/actions/spelling/expect/.
  • ... tokens you only need in one place and shouldn't generally be used, you can add an item in an appropriate file in .github/actions/spelling/patterns/.

See the README.md in each directory for more information.

🔬 You can test your commits without appending to a PR by creating a new branch with that extra change and pushing it to your fork. The check-spelling action will run in response to your push -- it doesn't require an open pull request. By using such a branch, you can limit the number of typos your peers see you make. 😉

If the flagged items are 🤯 false positives

If items relate to a ...

  • binary file (or some other file you wouldn't want to check at all).

    Please add a file path to the excludes.txt file matching the containing file.

    File paths are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your files.

    ^ refers to the file's path from the root of the repository, so ^README\.md$ would exclude README.md (on whichever branch you're using).

  • well-formed pattern.

    If you can write a pattern that would match it,
    try adding it to the patterns.txt file.

    Patterns are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your lines.

    Note that patterns can't match multiline strings.

@DDKinger DDKinger marked this pull request as draft May 31, 2026 10:19
@DDKinger DDKinger closed this Jun 5, 2026
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.

2 participants