Hook-free agent session tracking for shell-pane CLIs#258
Conversation
…server) Design spec to replace the wt-agent-hooks plugin mechanism with a hook-free, zero-install subsystem in wta-master: a filesystem watcher over each CLI's on-disk session files (discovery + activity) plus a process binder (binding + liveness), both emitting the existing SessionEvents so the downstream registry / UI / Enter-routing are unchanged. Settled via three live probes (2026-06-08/09): - Binding (Spike A): copilot=inuse.<pid>.lock, codex=Restart Manager (both exact); claude+gemini=cwd correlation. PEB *_SESSION_ID and CREATE-instant Restart Manager were tried and rejected. WT_SESSION lives in every CLI's PEB, so pid->pane is trivial; only file-id->pid differs per CLI. - Activity (Spike B): all CLIs write tool records incrementally (live Working/Idle works); Gemini rewrites a \.messages snapshot so its classifier must re-parse + diff rather than byte-tail. - Antigravity (agy.exe), Gemini CLI's 2026-06-18 successor: documented but deferred to a later phase gated on WTA ACP support. Initial scope is Copilot/Claude/Codex/Gemini. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Three sequential bite-sized TDD plans for the initial scope (copilot/claude/codex/gemini), grounded in live probes + a three-agent investigation of the master/helper protocol and hook-removal surface: - Plan A: proc_bind binding primitives (lock / Restart Manager / PEB env / parent pid) via inline validated Win32 FFI, no new deps. - Plan B: session_watcher + per-CLI classifiers (real probe-verified field names) + cwd-correlation binder; adds notify. - Plan C: master wiring (reuse existing apply_event + sessions/changed broadcast reducer) + full hook teardown (Rust installer/subcommand/env/ bundle + the one C++ WTA_HOOK_LOG_DIR injection + Settings UI). Antigravity remains the documented, deferred follow-up per the spec. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…dead_code - Add targeted #[allow(dead_code)] + comment to Candidate, correlate_by_cwd, bind_copilot, bind_codex in session_watcher/bind.rs (Plan C wiring pending) - Add targeted #[allow(dead_code)] + comment to cwd_for_pid in proc_bind.rs - Fix clippy: remove redundant 'as usize' cast in cwd_for_pid (len_word & 0xFFFF) - Fix clippy: add blank line before standalone doc paragraph in bind.rs module doc - cargo fmt applied to all Plan B scope files; out-of-scope files reverted Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ne loss - Codex: extract full UUID (last 5 hyphen groups) from rollout filename instead of just the last dash-group, so Emitted.key matches what history_loader reads from session_meta.payload.id. - Gemini: derive canonical key from the header line's \sessionId\ field; the filename only carries the first 8 hex chars of the UUID. Fall back to the path-derived stem when the field is absent. - Append branch: only advance the byte offset through the last newline; a trailing partial line (record still being written) is left for the next tick, preventing permanent data loss when the write completes. - Replace codex_path test with codex_path_uses_full_uuid_key that asserts the full UUID, and add process_change_does_not_lose_a_partial_line regression test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Spawns the hookless file watcher inside run_master_loop and wires emitted events into master's registry via the same reducer pattern as handle_session_hook. Two new functions: - apply_watcher_event: applies one Emitted event and broadcasts sessions/changed if state changed, mirroring handle_session_hook without the ext-request round-trip. - ensure_watched_session_row: creates a minimal SessionInfo row (CliSource + Idle + Unknown origin) on first sight of a session key, so apply_event always has a pre-existing row to mutate. The watcher runs on a dedicated OS thread (blocking notify loop); a second bridge thread forwards into a tokio unbounded_channel; a spawn_local task on the LocalSet drains the channel and calls apply_watcher_event — no blocking work on the async executor. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tomatic) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
check-spelling found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Pull request overview
This PR replaces the PowerShell hook/plugin-based mechanism for tracking “Class-B” agent CLI sessions (run directly in shell panes) with a hook-free implementation in wta-master that watches each CLI’s on-disk session records and binds sessions to panes via process inspection. In support of that shift, it removes the wt-agent-hooks bundle and tears out the related Settings/FRE UI and helper wiring.
Changes:
- Add a new Rust
session_watcher/subsystem (file watcher + per-CLI classifiers + pane-binding helpers) and wire it intowta-master’s registry update/broadcast path. - Remove the entire
wt-agent-hooksbundle and associated hook log-dir env injection / hook upgrade paths. - Remove C++/XAML UI for “Install hooks” (Settings + FRE), delete the C++ hook-status parser and its unit tests, and update localization resources accordingly.
Reviewed changes
Copilot reviewed 51 out of 53 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tools/wta/wt-agent-hooks/README.md | Removes hook bundle documentation (bundle deleted). |
| tools/wta/wt-agent-hooks/hook-debug/state-logger.ps1 | Removes hook debug utility script. |
| tools/wta/wt-agent-hooks/gemini-extension/README.md | Removes Gemini hook extension docs. |
| tools/wta/wt-agent-hooks/gemini-extension/hooks/send-event.ps1 | Removes Gemini hook forwarding script. |
| tools/wta/wt-agent-hooks/gemini-extension/hooks/hooks.json | Removes Gemini hook registration. |
| tools/wta/wt-agent-hooks/gemini-extension/gemini-extension.json | Removes Gemini extension manifest. |
| tools/wta/wt-agent-hooks/copilot/wt-agent-hooks/hooks/send-event.ps1 | Removes Copilot hook forwarding script. |
| tools/wta/wt-agent-hooks/copilot/wt-agent-hooks/hooks/hooks.json | Removes Copilot hook registration. |
| tools/wta/wt-agent-hooks/copilot/wt-agent-hooks/.claude-plugin/plugin.json | Removes Copilot plugin manifest. |
| tools/wta/wt-agent-hooks/copilot/.claude-plugin/marketplace.json | Removes Copilot local marketplace manifest. |
| tools/wta/wt-agent-hooks/codex/wt-agent-hooks/hooks/send-event.ps1 | Removes Codex hook forwarding script. |
| tools/wta/wt-agent-hooks/codex/wt-agent-hooks/hooks/hooks.json | Removes Codex hook registration. |
| tools/wta/wt-agent-hooks/codex/wt-agent-hooks/.codex-plugin/plugin.json | Removes Codex plugin manifest. |
| tools/wta/wt-agent-hooks/codex/.agents/plugins/marketplace.json | Removes Codex local marketplace manifest. |
| tools/wta/wt-agent-hooks/claude/wt-agent-hooks/hooks/send-event.ps1 | Removes Claude hook forwarding script. |
| tools/wta/wt-agent-hooks/claude/wt-agent-hooks/hooks/hooks.json | Removes Claude hook registration. |
| tools/wta/wt-agent-hooks/claude/wt-agent-hooks/.claude-plugin/plugin.json | Removes Claude plugin manifest. |
| tools/wta/wt-agent-hooks/claude/.claude-plugin/marketplace.json | Removes Claude local marketplace manifest. |
| tools/wta/src/ui/chat.rs | Removes rendering support for hook-originated “AgentEvent” chat messages. |
| tools/wta/src/theme.rs | Removes styles that were only used for hook event rendering. |
| tools/wta/src/session_watcher/mod.rs | New: core file-watcher, incremental tailing, and event emission. |
| tools/wta/src/session_watcher/discover.rs | New: map changed paths to (cli, key, cwd) identities. |
| tools/wta/src/session_watcher/classify_gemini.rs | New: classify Gemini snapshot-style logs into SessionEvents. |
| tools/wta/src/session_watcher/classify_copilot.rs | New: classify Copilot events.jsonl into SessionEvents. |
| tools/wta/src/session_watcher/classify_codex.rs | New: classify Codex rollout logs into SessionEvents. |
| tools/wta/src/session_watcher/classify_claude.rs | New: classify Claude logs into SessionEvents. |
| tools/wta/src/session_watcher/bind.rs | New: bind discovered sessions to panes via lock/RM/cwd correlation. |
| tools/wta/src/protocol/acp/spawn.rs | Removes WTA_HOOK_LOG_DIR env injection (hooks removed). |
| tools/wta/src/master/mod.rs | Wires watcher into master loop; adds reducer application + broadcast path. |
| tools/wta/src/history_loader.rs | Exposes a Codex rollout lookup helper for master binding. |
| tools/wta/src/agent_sessions.rs | Removes now-unused CLI source parsing helper + related test. |
| tools/wta/cgmanifest.json | Updates component governance manifest for new Rust deps. |
| tools/wta/Cargo.toml | Adds notify dependency for filesystem watching. |
| tools/wta/Cargo.lock | Locks notify and its transitive dependencies. |
| src/cascadia/ut_app/TerminalApp.UnitTests.vcxproj | Drops removed hook-status unit test compilation unit. |
| src/cascadia/ut_app/AgentHooksStatusTests.cpp | Deletes unit tests for hook-status JSON parsing (feature removed). |
| src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw | Removes hook-install UI strings from Settings resources. |
| src/cascadia/TerminalSettingsEditor/AIAgentsViewModel.idl | Removes hook-install ViewModel surface area. |
| src/cascadia/TerminalSettingsEditor/AIAgentsViewModel.h | Removes hook-install state/methods and header dependency. |
| src/cascadia/TerminalSettingsEditor/AIAgentsViewModel.cpp | Removes hook-install status polling and action handlers. |
| src/cascadia/TerminalSettingsEditor/AIAgents.xaml | Removes “Agent session tracking (hooks)” UI section. |
| src/cascadia/TerminalConnection/ConptyConnection.cpp | Removes WTA_HOOK_LOG_DIR env injection (hooks removed). |
| src/cascadia/TerminalApp/Resources/en-US/Resources.resw | Removes FRE hook-install failure string. |
| src/cascadia/TerminalApp/FreOverlay.h | Removes FRE problem kind / hook install API surface. |
| src/cascadia/TerminalApp/FreOverlay.cpp | Removes FRE hook install step and failure handling. |
| src/cascadia/inc/IntelligentTerminalPaths.h | Updates comments after removing hook log-dir coupling. |
| src/cascadia/inc/AgentHooksStatus.h | Deletes the hook-status JSON parser/formatter header. |
| NOTICE.md | Updates third-party notices for new Rust deps (e.g., notify, filetime). |
| let mut info = crate::session_registry::SessionInfo::new(sid, cwd); | ||
| info.cli_source = Some(emitted.cli.clone()); | ||
| info.status = Some(crate::agent_sessions::AgentStatus::Idle); | ||
| info.origin = Some(crate::agent_sessions::SessionOrigin::Unknown); | ||
| info.pane_session_id = pane; | ||
| state.registry.upsert(info).await; |
| for root in watched_roots() { | ||
| // A missing root is fine (the user may not have that CLI) — log + skip. | ||
| if root.exists() { | ||
| if let Err(err) = watcher.watch(&root, RecursiveMode::Recursive) { | ||
| tracing::warn!( | ||
| target: "session_watcher", | ||
| root = %root.display(), | ||
| error = %err, | ||
| "watch failed" | ||
| ); | ||
| } | ||
| } | ||
| } |
| std::thread::Builder::new() | ||
| .name("wta-session-watch".into()) | ||
| .spawn(move || { | ||
| if let Err(err) = crate::session_watcher::watch(sync_tx) { | ||
| tracing::warn!(target: "session_watcher", error = %err, "watcher exited"); | ||
| } | ||
| }) | ||
| .ok(); |
| std::thread::Builder::new() | ||
| .name("wta-session-watch-bridge".into()) | ||
| .spawn(move || { | ||
| for emitted in sync_rx { | ||
| if async_tx.send(emitted).is_err() { | ||
| break; | ||
| } | ||
| } | ||
| }) | ||
| .ok(); |
…for Copilot Copilot live activity was tool-granular: WORKING only flickered during the brief tool.execution_start->complete windows, so a session showed IDLE while the agent was thinking / streaming text during a multi-minute turn. Drive status from the assistant *turn* instead: assistant.turn_start -> WORKING for the whole turn, assistant.turn_end -> IDLE. tool.execution_complete is no longer mapped (IDLE is owned by turn_end, so a tool finishing mid-turn no longer prematurely idles the row). Consecutive turns share a turn_end/turn_start timestamp and batch together, so there is no IDLE flicker between them. Also surface Copilot's permission.requested record (the approve/deny command gate) as ATTENTION; it clears on the approved tool's start (WORKING) or the turn's end (IDLE). Copilot-only: reuses the existing ToolStarting/ToolCompleted/Notification events (current_tool / attention_reason are not rendered), so the shared reducer, wire conversions, and the other CLIs are untouched. Claude/Codex/Gemini keep their tool-granular model pending per-CLI turn-marker verification. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
…ility Post-PR live-testing fixes for the file/process watcher that replaced the PowerShell hooks. All within wta-master's watcher path; the shared reducer's terminal-state guard stays untouched so Class-A agent-pane rows are unaffected. Codex activity model (classify_codex): - Turn-based status: event_msg/task_started -> Working, task_complete -> Idle; function_call_output dropped (Idle is owned by task_complete, so a tool finishing mid-turn no longer flips the row to Idle while the agent works). - require_escalated sandbox permission -> Attention, surfacing the call's justification as the reason (parsed from the nested arguments JSON). Class-B (shell-pane) session lifecycle: - Revive a resumed Historical/Ended row in ensure_watched_session_row (watcher path only) so `codex resume` reappears as Idle and rebinds its pane. - 5s pid-liveness reaper: shell-pane CLIs write no "session ended" record and WT panes only track the conpty root shell, so a Ctrl+C left the row stuck. Poll each bound pid (OpenProcess + GetExitCodeProcess) and end any whose process exited. Adds proc_bind::pid_alive, SessionInfo.bound_pid, and threads the owner pid out of the bind_* resolvers. - Codex cwd from session_meta (codex_cwd_from_rollout) so the row has a cwd for the session-view title fallback before the first user message exists. Watcher reliability (session_watcher): - Seed per-file offsets to EOF at startup (seed_existing_progress_in) so a spurious notify event on a pre-existing historical file can no longer replay it from offset 0 -> revive every historical session -> flood master with thousands of sessions/changed broadcasts. - Periodic catch-up sweep for dropped notify events. ReadDirectoryChangesW is an edge signal that coalesces writes and gives no guarantee an event arrives after the final write is durable, so a turn's last record (task_complete / turn_end -> Idle) could be stranded, leaving the row stuck on Working. The sweep re-runs the incremental process_change, but only over a "hot" set of files whose session is currently Working/Attention, so its cost scales with active sessions rather than the (mostly historical) tracked-file count. title fix (app): session_info_to_agent_session filled a None title with the literal "-", which is non-empty and so bypassed display_title's cwd-basename fallback. Use an empty string so the fallback applies. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
| /// `None` for agent-pane sessions and any session we couldn't bind to a | ||
| /// pid. Master-internal — skipped on the wire. | ||
| #[serde(default, skip_serializing_if = "Option::is_none")] | ||
| pub bound_pid: Option<u32>, |
| //! * Claude/Gemini → cwd correlation: among live CLI processes, pick the | ||
| //! one whose working directory matches the session's cwd; ties (same cwd) | ||
| //! are left unresolved (returns None) to avoid a wrong bind. |
| None | ||
| } | ||
|
|
||
| #[cfg(test)] |
Codex's multi_agent_v1/spawn_agent tool forks a child thread that gets its own rollout-*.jsonl with `source.subagent` in its session_meta. The fork inherits the parent's full history (so its first user message — and thus its title — is identical), which the file watcher surfaced as a second, duplicate-looking session row. Skip these internal worker threads in both surfaces, keyed on the `session_meta.payload.source.subagent` discriminator (top-level sessions carry `source:"cli"`/`"user"`): - history loader (`load_codex`): `read_codex_session_meta` now reports `is_subagent`; subagent rollouts are skipped during the startup scan. - live watcher (`process_change`): on first read, a `session_meta` with `source.subagent` flags the file (`Progress.ignored`) and drops it wholesale; later reads short-circuit. Shared `codex_record_is_subagent_meta` / `codex_payload_is_subagent` helpers (pub(crate)) back both paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@check-spelling-bot Report
|
| 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 🙅 (1)
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 preexisting
[Pp]re[- ]existing
Errors and Warnings ❌ (2)
See the 📂 files view, the 📜action log, 👼 SARIF report, or 📝 job summary for details.
| ❌ Errors and Warnings | Count |
|---|---|
| 54 | |
| ❌ forbidden-pattern | 2 |
See ❌ Event descriptions for more information.
✏️ 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.txtfile 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 thepatterns.txtfile.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.
Summary
Replaces the PowerShell-hook mechanism for tracking Class-B agent CLI sessions (Copilot / Claude / Codex / Gemini run directly in shell panes) with a hook-free file + process observer living in
wta-master. Session discovery and live activity (Working / Idle / Attention) now come from tailing each CLI's own on-disk session records; pane binding comes from process inspection. The entire PowerShell-hook apparatus (installer,wta hookssubcommand, embeddedwt-agent-hooksbundle, env injection, Settings/FRE install UI) is removed.Design spec:
doc/specs/hookless-agent-session-tracking.md.How it works
tools/wta/src/session_watcher/): anotifywatch over the four CLI session roots; per-CLI classifiers turn each on-disk record into the existingSessionEvents.tools/wta/src/proc_bind.rs, spec Decision This repo is missing important files #3): Copilot =inuse.<pid>.lock; Codex = Restart Manager owner of the rollout file; Claude = cwd-correlation among liveclaude.exe; Gemini = unbound (cwd not path-encoded — deferred).pid → pane GUIDviaWT_SESSIONin the process PEB (inline Win32 FFI: PEB read, Restart Manager, Toolhelp32).tools/wta/src/master/mod.rs): watcher →ensure_watched_session_row(creates + binds the row on first sight) →apply_event(status) → broadcastsessions/changed. Renders through master's existing registry/snapshot path; the helper-side ingest is gone.Removed (hook teardown)
agent_hooks_installer.rs, thewta hookssubcommand, thewt-agent-hooks/**bundle, theWTA_HOOK_LOG_DIRenv injection (spawn.rs), the master-startup hooks auto-upgrade, the dormantagent_eventingest, and its deadagent.*wire-format parser.WTA_HOOK_LOG_DIRinjection (ConptyConnection.cpp); the Settings "Install hooks" UI +AgentHooksStatus+ its unit test; the FRE hooks-install Save step. The FRE "Session management" toggle UI is kept (it just no longer installs hooks).Verification
cargo test→ 623 pass, 0 fail; build clean at the pre-existing 44-warning baseline; changed files rustfmt-clean (pinnedms-prod-1.93toolchain).CascadiaPackage(F5) — verify the WinRT IDL → .h → .cpp projection + XAML compile. (Not buildable in the authoring environment; validated here only via exhaustive grep + XML-parse of both XAML files.)copilot/claude/codex/geminiin a normal shell pane; confirm it appears in the session-management view with live Working/Idle and that Enter focuses its pane.origin=Unknownrow. The watcher de-dupes Class-A vs Class-B by session-key equality; this holds only if the CLI's on-disk key equals the ACPsession/newid (plausible for Copilot; Codex/Claude via the zed ACP adapter may mint a different id). If duplicates appear, the fix is a pane-ownership guard inensure_watched_session_row.Notes / deferred
hooks.*(Rust) and non-en-USAIAgents_Hooks*(resw) locale keys are intentionally left in place to avoid 89-file churn; the localization pipeline prunes them.