-
Notifications
You must be signed in to change notification settings - Fork 151
Stop duplicating and fabricating session hook rows - #761
Conversation
Two independent defects made the /sessions picker show rows that no reconcile pass could ever remove. Fan-out amplification. One `wtcli agent-hook` invocation produces one COM `agent_event`, which the protocol server broadcasts to every subscribed helper. Each helper both updates its own registry and forwards the event to wta-master, so master applied a single real hook once per live helper and re-broadcast `sessions/changed` for each copy. A live master log shows one SessionStarted arriving three times over HelperId(1..3) with nine resulting `sessions/changed` writes; the same log shows exactly two copies earlier, when only two helpers were connected. `wtcli` now stamps a per-invocation `broadcast_id` GUID, helpers forward it, and master keeps a bounded FIFO of ids it has already applied. The suffix identifying which of an event's several SessionEvents is being published comes from the emit site (`HookSlot`), never from a position in the emitted sequence: `session_known` is per-helper local state, so an unaware helper prepends a synthetic start while an aware one does not. Positional indices would give the same logical event different keys on different helpers and, worse, alias one helper's placeholder onto another's real event. Fabricated terminal rows. `needs_synthetic_start` excluded only `agent.session.started`, so an `agent.session.end` for a session WTA had never seen invented a SessionStarted titled after the cwd basename, shipped it to master, then shipped the real SessionStopped. That left a permanent `Ended` row — observed as a "yuazha" row for an abandoned session with zero turns and no on-disk state — which `is_stale_host_history_row` cannot prune, since it only drops ids the listing agent itself returned and later stopped returning. Terminal events no longer synthesize a start. `agent.error` is deliberately not excluded. It describes a live but failing session, and its `ConnectionFailed` reducer resolves the row through `active_by_pane`, so removing the synthetic start would silently drop a first-observed connection failure instead of surfacing it as `Error`. An absent `broadcast_id` — born-bound registrations, resume bookkeeping, or a `wtcli` predating the field — keeps the previous apply-every-copy behavior, so old and new components interoperate in both directions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c6ef3e46-71af-4e5a-82af-a172700ca3ce
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
Master-side replay suppression currently retains arbitrary-length broadcast_id strings, so adding a small length cap (or equivalent bound) is advisable to keep memory usage predictably bounded.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR hardens the session-hook routing path so /sessions no longer accumulates duplicate or "ghost" rows, by deduping COM fan-out replays in wta-master and avoiding synthetic-start fabrication for terminal events on unknown sessions.
Changes:
- Stamp an optional per-invocation
broadcast_idinwtclihook events and plumb it helper → master so master can dedupe sibling-helper replays. - Add master-side bounded replay suppression (
SeenBroadcastIds) that short-circuits duplicates before the reducer and beforesessions/changedfan-out. - Prevent synthetic-start creation for terminal session events on unknown sessions (while preserving the
agent.errorcarve-out), with new unit tests covering both behaviors.
File summaries
| File | Description |
|---|---|
| tools/wta/src/session_registry.rs | ExtRequest/ExtResponse plumbing updated to carry optional broadcast_id alongside SessionEvent and parse it safely. |
| tools/wta/src/protocol/acp/client.rs | Helper-to-master publish path now forwards (event, broadcast_id) and logs the id for diagnostics. |
| tools/wta/src/master/mod.rs | Adds master-side SeenBroadcastIds and drops replayed hooks early when a broadcast_id is present. |
| tools/wta/src/master/tests.rs | Adds/updates tests to validate dedupe semantics, sibling-event survival, id-less behavior, and FIFO eviction. |
| tools/wta/src/app.rs | Introduces QueuedSessionHook and HookSlot to produce stable per-emit-site dedupe suffixes. |
| tools/wta/src/app_events.rs | Extracts broadcast_id from WT hook params, qualifies it with stable HookSlot, and queues it to master. |
| tools/wta/src/app_tests.rs | Adds coverage for "no synthetic start for terminal events", preserves agent.error behavior, and validates slot stability. |
| src/tools/wtcli/wtcli_functions.h | Extends BuildAgentHookEventJson to include broadcast_id inside the measured wire budget. |
| src/tools/wtcli/main.cpp | Mints a per-invocation GUID broadcast_id for hook events. |
| src/tools/wtcli/ft_fuzzer/fuzzmain.cpp | Updates fuzz harness to include/fuzz the new broadcastId input segment. |
Review details
- Files reviewed: 10/10 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
This comment has been minimized.
This comment has been minimized.
The WTA unit tests exercise one helper's routing function and the master's handler in isolation, inside a single process. Both defects this PR fixes live in the fan-out itself: one `wtcli agent-hook` invocation becomes one COM broadcast that reaches every subscribed helper process, each of which forwards it to master over its own named pipe. No unit test can span that boundary. Three cases, C287-C289: - **One hook broadcast applies once per helper fan-out.** Injects `agent.tool.starting` for an unseen session, which deliberately expands into two published events, and asserts each is applied exactly once. The `start` and `primary` slots must appear as separate dedupe keys — a positional suffix would collide them, because whether a helper emits the synthetic start depends on its own local registry state. A guard asserts a replay was actually dropped, so the case cannot pass vacuously on a single-helper machine. - **Terminal hook for an unknown session creates no row.** Injects `agent.session.end` for a fresh id and asserts no `SessionStarted` and no cwd-basename title follow it. The absence is only asserted after the matching `SessionStopped` proves the hook reached master. - **Agent error for an unknown session still records the failure.** The false-positive control: `agent.error` is a live-but-failing session whose pane-keyed reducer needs the row, so it must keep creating one. Extra tabs rather than agent panes supply the fan-out. Every eligible tab pre-warms a stashed helper, which is already connected to master and already subscribed to the broadcast, so the suite needs neither `winapp` nor an ACP handshake to reach the boundary under test. Oracle is the master log. `wta sessions list` would be the more direct state oracle but is identity-gated outside the package, and the reducer is idempotent, so a registry row looks the same whether it was applied once or N times. Validation: 3/3 pass against the deployed Debug package with C287-C289 marked `[x]` through `Invoke-ItE2EReport.ps1 -UpdateReport`; `Feature.HookTrace` 5/5 still passes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c6ef3e46-71af-4e5a-82af-a172700ca3ce
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟢 Approval recommended
The changes are internally consistent across wtcli/helper/master, include targeted unit + E2E coverage for the fan-out boundary, and introduce no verified correctness issues in the reviewed diffs.
Review details
- Files reviewed: 13/13 changed files
- Comments generated: 0 new
- Review effort level: Lite
This comment has been minimized.
This comment has been minimized.
Review round 1 on #761. Copilot (2 threads): `SeenBroadcastIds` stored the full `broadcast_id` in both its set and its queue, so `CAPACITY` bounded the entry count but not the memory — a sender emitting long ids (still inside wtcli's `kMaxHookEventChars` envelope) could inflate the window arbitrarily, and the "~60 bytes per id" sizing note asserted an invariant nothing enforced. Adds `MAX_ID_LEN`, which makes the capacity a real `CAPACITY * MAX_ID_LEN` bound, and rewrites the note to reference it. An overlong id degrades to "apply every copy" rather than being dropped: that is exactly the behavior before deduplication existed, and the reducer is idempotent, so a repeat is harmless — whereas dropping would lose session state the window cannot key. check-spelling (9 threads): reworded rather than adding dictionary entries. Three findings were a developer username copied out of a live log into code comments and test data, including one in production code; those now use a neutral working directory, matching the `C:\Users\dev` other registry tests already use. The rest were coined words (`dedupable`, `undedupable`) and two forbidden patterns (`pre-existing`, `, otherwise`). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c6ef3e46-71af-4e5a-82af-a172700ca3ce
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟢 Approval recommended
The changes are well-covered by new UT/E2E tests and the only issue found is a minor checklist-ID mismatch in a test header comment.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
test/e2e/tests/Feature.SessionHookRouting.Tests.ps1:2
- The header comment references checklist IDs C279–C281, but this PR adds the SessionHookRouting coverage under C287–C289 (see doc/release-check-list.md). This makes the test’s self-documentation misleading when cross-referencing the release checklist.
- Files reviewed: 13/13 changed files
- Comments generated: 0 new
- Review effort level: Lite
Review round 2 on #761. The header comment still carried the placeholder IDs C279-C281 written before `Set-ChecklistIds.ps1` ran; the suite's cases are actually C287-C289, which is what `doc/release-check-list.md` and the generated report use. Comment-only change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c6ef3e46-71af-4e5a-82af-a172700ca3ce
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟢 Approval recommended
The behavior changes are well-covered by new unit/E2E tests and only a minor doc-comment accuracy nit remains.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
tools/wta/src/master/mod.rs:282
- The memory-bound note for
MAX_ID_LENunderstates retained memory and has a small format mismatch: the code stores each id in bothseenandorder(two ownedStrings per id), andwtcli’sGuidToStringstrips braces so the minted format isGUID#{slot}(36 chars for GUID), not{GUID}#{slot}(38). Updating the doc comment avoids future confusion when reasoning about bounds.
- Files reviewed: 13/13 changed files
- Comments generated: 0 new
- Review effort level: Lite
Replace the helper-forwarded hook architecture introduced earlier in this PR. Master already owns a WT protocol event subscription, so it should consume the single COM broadcast directly instead of receiving one named-pipe copy from every helper and deduplicating them afterwards. - Route `agent_event` in `handle_master_wt_event` into the authoritative master registry. Helpers still process the broadcast locally because their OSC 133;A exit heuristic and autofix target lookup need synchronous pane-to-session bindings, but they never forward agent CLI hooks. - Extract `plan_agent_event`, a pure shared event taxonomy used by master and helper. Synthetic starts, terminal-event suppression, sessionless fallbacks, interactive-tool splitting and ignored events therefore cannot drift between the two registries. - Preserve watcher coordination through a shared `apply_master_session_event` boundary. Per-session lifecycle gates serialize reducer and ownership changes; a late no-op ResumePaneAssigned no longer downgrades a direct hook's current `hook_owned` generation, while ResumeDispatched and born-bound registrations still mark a new hook-free generation. - Apply all COM-hook reducer transitions and broadcast their immediate state before scheduling ACP title refresh, so a five-second session/list timeout cannot stall the sole COM consumer or leave a synthetic row temporarily Idle. - Make the master listener load-bearing and observable. `wtcli listen` emits an internal ready marker after COM Subscribe succeeds; master refuses to start without it. Pre-subscribe failures retry with backoff, while a previously subscribed listener restarts immediately because COM broadcasts are not replayed. stderr is retained in the exit log instead of discarded. - Make identical ConnectionFailed events idempotent in both registries. Every helper still observes WT connection_state failures, but only the first copy changes state and triggers sessions/changed; a different reason remains new information. - Remove the now-obsolete broadcast GUID, HookSlot, SeenBroadcastIds and wire envelope machinery from Rust and wtcli. - Rewrite C287-C289 to assert the real architecture using an info-level, post-reducer master breadcrumb: multiple helpers are connected, master processes one COM hook, helper-originated lifecycle records for the id are absent, and final state is Working / None / Error as appropriate. Validation: - cargo test: 1880 passed, 0 failed, 1 ignored - full solution build: 0 errors - SessionHookRouting ItE2E: 3 passed, 0 failed - HookTrace ItE2E: 5 passed, 0 failed - live: three helpers observed the injected sid, helper forwards=0, master applied one SessionStarted and one ToolStarting - deployment preserved settings.json byte-for-byte Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c6ef3e46-71af-4e5a-82af-a172700ca3ce
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
There is at least one confirmed correctness issue in the new shared agent.session.start handling plus an operational risk from unbounded stderr buffering in the long-lived wtcli listener.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 13/13 changed files
- Comments generated: 2
- Review effort level: Lite
Review round after the master-owned hook routing rewrite. - Drain `wtcli listen` stderr continuously while retaining only a 16 KiB diagnostic prefix. A simple bounded read would stop consuming after the cap and eventually block a noisy long-lived child when the OS pipe filled; the bounded reader discards the tail instead and reports whether it truncated. - Treat both accepted real-start spellings (`agent.session.started` and `agent.session.start`) as superseding any earlier pane-keyed placeholder. Without the singular spelling, a missing-id hook could leave an orphan local row after the real session id arrived. Added focused tests that force the bounded reader to drain 32 KiB through a 64-byte duplex pipe, and that create a pane placeholder before a singular real start. Validation: 1882 WTA tests passed, full solution built with 0 errors, and the SessionHookRouting ItE2E suite passed 3/3 after deployment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c6ef3e46-71af-4e5a-82af-a172700ca3ce
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
It changes a load-bearing cross-process event subscription and session lifecycle routing path (master/COM/hooks), which warrants final human validation despite strong UT/E2E coverage.
Review details
- Files reviewed: 13/13 changed files
- Comments generated: 0 new
- Review effort level: Lite
The WT event listener is a session-status enhancement, not an availability gate for the agent stack. Do not make agent-pane chat, Autofix, history listing or resume wait for or fail with COM hook subscription readiness. Master now accepts helpers immediately and starts the listener readiness/retry loop in the background. A missing WT_COM_CLSID or a listener that remains unready is logged as "live session status may be stale" while the rest of the agent experience continues unchanged. Helpers likewise launch their listener without holding ACP startup behind the 15-second readiness window. Once the listener reconnects, direct master-owned hook routing resumes. No special unavailable state is introduced in /sessions: it continues to list and resume known history, with the same existing limitation that live status can be stale while hooks are unavailable. Validation: 1882 WTA tests passed, full solution built with 0 errors, the SessionHookRouting ItE2E suite passed 3/3 after deployment, and settings.json was unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c6ef3e46-71af-4e5a-82af-a172700ca3ce
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟢 Approval recommended
The changes align with the PR’s stated regression fixes and add strong UT/E2E validation; the only feedback is a small, non-blocking allocation optimization.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
tools/wta/src/master/mod.rs:7961
- This pane-binding lookup allocates repeatedly (
to_ascii_lowercase()for the input and again for every row). Since pane IDs are ASCII GUID-like strings, you can avoid per-hook heap allocations by usingeq_ignore_ascii_caseagainst the borrowed&strinstead.
- Files reviewed: 14/14 changed files
- Comments generated: 0 new
- Review effort level: Lite
Stop retrying `wtcli listen` after eight consecutive unstable failures. This keeps a permanently broken COM registration from turning every master/helper into a process and log storm while preserving the agreed degradation contract: chat, Autofix, session list and resume stay available; only live session status remains stale until that WTA process restarts. A subscription that stays healthy for 30 seconds resets the failure streak. The first exit after a stable subscription still restarts immediately because COM broadcasts are not replayed; repeated fast subscribe/exit cycles retain the streak, enter exponential backoff, and eventually stop rather than tight-loop. Added a focused policy test covering the eight-attempt ceiling, fast-exit counting and stable-uptime reset. Validation: 1883 WTA tests passed, full solution built with 0 errors, the SessionHookRouting ItE2E suite passed 3/3 after deployment, and settings.json was unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c6ef3e46-71af-4e5a-82af-a172700ca3ce
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
It changes master/helper hook ownership and COM event routing semantics across multiple concurrency boundaries, so a final human review is warranted despite strong unit/E2E coverage.
Review details
- Files reviewed: 14/14 changed files
- Comments generated: 0 new
- Review effort level: Lite
Preserve upstream lifecycle, cancellation and settings changes while retaining master-owned hook routing. Resolve the event-dispatch conflict using consistent LF endings. Reconcile shell prompts in the ordered master COM stream so a helper exit cannot overtake a queued birth. Preserve exponential retry delays across unstable subscriptions and measure stability only after readiness. Count all E2E processing records and require currently subscribed helper listeners. Allocate fresh hook checklist IDs C296-C298 without changing upstream IDs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c6ef3e46-71af-4e5a-82af-a172700ca3ce
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
It changes cross-process COM event routing and listener lifecycle behavior in a way that can affect correctness and reliability across many runtime scenarios, so a final human review is warranted despite strong test coverage.
Review details
- Files reviewed: 14/14 changed files
- Comments generated: 0 new
- Review effort level: Lite
Add hookless regression cases for automatic Autofix prompt routing and negative controls, manual fix with auto-suggest disabled, snapshot rendering and resume dispatch without hook rows, ACP chat while listener readiness is pending, and an actual failed-then-recovered listener subprocess delivering a shell error into the Autofix prompt queue. The only new channel constructor is cfg(test); no shipping behavior or dependency changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c6ef3e46-71af-4e5a-82af-a172700ca3ce
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
It changes multi-process COM event routing and listener lifecycle behavior across master/helper/CLI boundaries, which merits final human validation despite strong UT/E2E coverage.
Review details
- Files reviewed: 14/14 changed files
- Comments generated: 0 new
- Review effort level: Lite
Uh oh!
There was an error while loading. Please reload this page.
Summary
Make
wta-masterprocess agent hooks directly through its existing WT event subscription. Helpers retain local pane/session bindings but no longer forward hooks, eliminating duplicate master updates without a dedupe cache.agent.errorhandling and clean up superseded pane placeholders.Availability
Missing agent hooks do not gate startup or disable chat, Autofix, history listing or resume; live session status may be stale. Automatic error detection still depends on the helper's general WT event channel, as before.
Validation
Follow-up: #819 tracks removing the remaining per-helper global WT subscriptions.