Skip to content

Navigation Menu

Sign in
Sign up

Stop duplicating and fabricating session hook rows - #761

Merged
Yuandi (DDKinger) merged 10 commits into
main from
dev/yuazha/fix-session-hook-ghosts
Sep 7, 2026
Merged

Stop duplicating and fabricating session hook rows #761
Yuandi (DDKinger) merged 10 commits into
main from
dev/yuazha/fix-session-hook-ghosts

Conversation

@DDKinger

@DDKinger Yuandi (DDKinger) commented Sep 2, 2026
edited
Loading

Copy link
Copy Markdown
Contributor

Summary

Make wta-master process 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.

  • Prevent ghost sessions from unknown end hooks; preserve legitimate agent.error handling and clean up superseded pane placeholders.
  • Preserve shell-exit ordering and resume ownership; make identical connection failures idempotent and keep ACP title lookup off the event-consumer path.
  • Add background listener recovery with exponential backoff, an eight-failure limit, a reset after 30 seconds of stable subscription, and bounded stderr capture.

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

  • WTA: 2073 passed, 0 failed, 1 ignored; WTA and wtcli builds succeeded.
  • Added regression coverage for hookless chat, automatic/manual Autofix, history rendering/resume dispatch, listener recovery and lifecycle ordering.
  • ItE2E cases C296-C298 cover single processing, unknown terminal hooks and first-observed errors. Duplicate-record and report-mapping oracles were checked offline.
  • Current-revision live UI E2E remains pending; earlier live results were from an older binary.

Follow-up: #819 tracks removing the remaining per-helper global WT subscriptions.

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
Copilot AI lite review requested due to automatic review settings September 2, 2026 05:39
Comment thread src/tools/wtcli/main.cpp Fixed
Comment thread src/tools/wtcli/wtcli_functions.h Fixed
Comment thread tools/wta/src/app.rs Fixed
Comment thread tools/wta/src/app_tests.rs Fixed
Comment thread tools/wta/src/app_tests.rs Fixed
Comment thread tools/wta/src/app_tests.rs Fixed
Comment thread tools/wta/src/app_tests.rs Fixed
Comment thread tools/wta/src/app_tests.rs Fixed
Comment thread tools/wta/src/session_registry.rs Fixed

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.

🟡 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_id in wtcli hook 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 before sessions/changed fan-out.
  • Prevent synthetic-start creation for terminal session events on unknown sessions (while preserving the agent.error carve-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.

Comment thread tools/wta/src/master/mod.rs Outdated
Comment thread tools/wta/src/master/mod.rs Outdated

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
Copilot AI review requested due to automatic review settings September 2, 2026 06: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.

🟢 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.

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
Copilot AI review requested due to automatic review settings September 2, 2026 11:20

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.

🟢 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
Copilot AI review requested due to automatic review settings September 2, 2026 11:26

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.

🟢 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_LEN understates retained memory and has a small format mismatch: the code stores each id in both seen and order (two owned Strings per id), and wtcli’s GuidToString strips braces so the minted format is GUID#{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
Copilot AI review requested due to automatic review settings September 3, 2026 03:28

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.

🟡 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

Comment thread tools/wta/src/shell/wt_channel/cli_channel.rs Outdated
Comment thread tools/wta/src/app.rs Outdated
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
Copilot AI review requested due to automatic review settings September 3, 2026 03:44

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.

🔵 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
Copilot AI review requested due to automatic review settings September 3, 2026 04:09

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.

🟢 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 using eq_ignore_ascii_case against the borrowed &str instead.
  • 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
Copilot AI review requested due to automatic review settings September 3, 2026 04:48

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.

🔵 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
Copilot AI review requested due to automatic review settings September 7, 2026 06:51

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.

🔵 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
Copilot AI review requested due to automatic review settings September 7, 2026 07:42

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.

🔵 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

Yuandi (DDKinger) merged commit e90ab68 into main Sep 7, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

Copilot code review Copilot
Copilot review effort, defaults to Lite
Applies to this pull request for everyone.Learn more about Copilot code review.
Copilot left review comments
@vanzue Kai Tao (vanzue) vanzue approved these changes
@haonanttt haonanttt haonanttt approved these changes
+1 more reviewer
@github-advanced-security github-advanced-security[bot] github-advanced-security[bot] left review comments
Reviewers whose approvals may not affect merge requirements

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

AltStyle によって変換されたページ (->オリジナル) /