fix(mcp): wire_send uses a cheap pidfile liveness check, not the full scan (#350)#353
Merged
Merged
Conversation
… scan (#350) Willard reported painfully slow MCP on Windows. Root cause confirmed in code: `tool_send` (every `wire_send`, the primary MCP action) called `daemon_liveness()` purely to read one diagnostic boolean (`daemon_seen` = `snap.pidfile_alive`). But `daemon_liveness()` also runs the machine-wide orphan-detection scan the send path never uses: - `find_processes_by_cmdline("wire daemon")` → PowerShell `Get-CimInstance` on Windows (enumerates ALL processes, ~1-3s cold), - `list_sessions()` → reads `agent-card.json` from EVERY by-key home (Willard's box has hundreds — see #351's session proliferation), each an NTFS dir read, - `pid_is_alive` per session daemon pid → a `tasklist.exe` spawn each. On Unix that's a cheap `pgrep` + `/proc`; on Windows it's seconds — paid on every send. The annotation only needs `daemon_seen`. Fix: new `ensure_up::daemon_pidfile_alive()` — one `daemon.pid` read + one `pid_is_alive` (one `tasklist` on Windows) — yielding the identical boolean. `tool_send`'s two branches use it instead of full `daemon_liveness()`. `wire_status` keeps the full scan (it legitimately reports orphans). Cross-platform; Windows CI (`install-smoke-windows`) compile-verifies. The deeper Windows wins (native Win32 to kill the remaining PowerShell/tasklist/reg spawns, #350) and the MCP/CLI identity split (#351) remain tracked; this removes the per-send hot-path scan, the dominant cost. Test `daemon_pidfile_alive_false_without_pidfile`. 610 lib tests; fmt+clippy clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Deploying wireup-landing with
|
| Latest commit: |
9a2f63d
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://f7cb7293.wireup-landing.pages.dev |
| Branch Preview URL: | https://fix-mcp-send-cheap-daemon-ch.wireup-landing.pages.dev |
laulpogan
added a commit
that referenced
this pull request
Jul 10, 2026
…t self-report The recurring "display name ≠ actual name" bug: a Claude session's statusline shows one wire identity while the session operates on wire as another. Confirmed live — a dot session's statusline read daydream-gorge (its CLAUDE_CODE_SESSION_ID) while its MCP served merry-spindle, a key matching no live session. Box-wide it's systemic: 434 running wire procs, 115 serving an identity no live session uses. Root cause: the long-lived `wire mcp` server resolves + FREEZES its identity at launch; the statusline re-resolves live each render. When they diverge — the MCP booted before Claude wrote its session pidfile, so resolve_session_key() missed and the server minted a throwaway `mcp-proc-*` key, pinned it into WIRE_SESSION_ID, and served it for life — you get two permanent names. The prior dash detection (9c3b62a) couldn't see it: a fresh CLI's env is already live, so its env-vs-pidfile check never inspected the frozen MCP process. Fixes: - session.rs: before minting at MCP startup, RETRY the pidfile (walk the ancestor chain ONCE, re-read only the cached pids across a short backoff — no per-hop `ps` re-fork, avoiding the #353 Windows perf trap). Recovers the live identity instead of freezing a throwaway one. Stop pinning the minted key into WIRE_SESSION_ID (that slot is the operator-override / live channel; a minted value parked there masqueraded as an override and beat the live session on re-resolve — the root of the split). WIRE_HOME already pins the process + is inherited by children. - session.rs: redesign detect_identity_split to compare OPERATIONAL identity (the home this process serves) vs LIVE pidfile identity — so the frozen MCP itself detects the split, which the old env-vs-pidfile check structurally could not. Fixes a latent bug exposed by the redesign: handle_for_key sanitized the key before hashing, but homes are created from the raw key hash; sanitize_name truncates at 32 chars, so a 36-char session-id uuid resolved to the wrong home and the split silently returned None. (Caught by the in-situ gate, not units.) - mcp.rs: surface `identity_split` in wire_status + wire_whoami (null when healthy) so an agent self-detects drift at the session-start health check it already runs, instead of waiting for a human to run `wire dash`. - setup.rs: bake `env:{WIRE_SESSION_ID:"${CLAUDE_CODE_SESSION_ID}"}` into the canonical MCP entry `wire setup` writes, so the forward survives `--apply` (was a hand-edit the next apply overwrote). Inert on non-Claude hosts (the ${...} guard rejects an unexpanded value). Prevention for every new session. Existing frozen MCPs heal on next /mcp reconnect; the fix + self-report make that one-command recovery visible. 8 new tests; 643 lib tests green; fmt + clippy clean. In-situ: reproduced the split with real homes/pidfile — banner fires; silent when healthy. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VxnZEjrcrYR3SAq8Crv8hg
laulpogan
added a commit
that referenced
this pull request
Jul 10, 2026
…362) * fix(identity): self-heal the "two names" split at its source + make it self-report The recurring "display name ≠ actual name" bug: a Claude session's statusline shows one wire identity while the session operates on wire as another. Confirmed live — a dot session's statusline read daydream-gorge (its CLAUDE_CODE_SESSION_ID) while its MCP served merry-spindle, a key matching no live session. Box-wide it's systemic: 434 running wire procs, 115 serving an identity no live session uses. Root cause: the long-lived `wire mcp` server resolves + FREEZES its identity at launch; the statusline re-resolves live each render. When they diverge — the MCP booted before Claude wrote its session pidfile, so resolve_session_key() missed and the server minted a throwaway `mcp-proc-*` key, pinned it into WIRE_SESSION_ID, and served it for life — you get two permanent names. The prior dash detection (9c3b62a) couldn't see it: a fresh CLI's env is already live, so its env-vs-pidfile check never inspected the frozen MCP process. Fixes: - session.rs: before minting at MCP startup, RETRY the pidfile (walk the ancestor chain ONCE, re-read only the cached pids across a short backoff — no per-hop `ps` re-fork, avoiding the #353 Windows perf trap). Recovers the live identity instead of freezing a throwaway one. Stop pinning the minted key into WIRE_SESSION_ID (that slot is the operator-override / live channel; a minted value parked there masqueraded as an override and beat the live session on re-resolve — the root of the split). WIRE_HOME already pins the process + is inherited by children. - session.rs: redesign detect_identity_split to compare OPERATIONAL identity (the home this process serves) vs LIVE pidfile identity — so the frozen MCP itself detects the split, which the old env-vs-pidfile check structurally could not. Fixes a latent bug exposed by the redesign: handle_for_key sanitized the key before hashing, but homes are created from the raw key hash; sanitize_name truncates at 32 chars, so a 36-char session-id uuid resolved to the wrong home and the split silently returned None. (Caught by the in-situ gate, not units.) - mcp.rs: surface `identity_split` in wire_status + wire_whoami (null when healthy) so an agent self-detects drift at the session-start health check it already runs, instead of waiting for a human to run `wire dash`. - setup.rs: bake `env:{WIRE_SESSION_ID:"${CLAUDE_CODE_SESSION_ID}"}` into the canonical MCP entry `wire setup` writes, so the forward survives `--apply` (was a hand-edit the next apply overwrote). Inert on non-Claude hosts (the ${...} guard rejects an unexpanded value). Prevention for every new session. Existing frozen MCPs heal on next /mcp reconnect; the fix + self-report make that one-command recovery visible. 8 new tests; 643 lib tests green; fmt + clippy clean. In-situ: reproduced the split with real homes/pidfile — banner fires; silent when healthy. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VxnZEjrcrYR3SAq8Crv8hg * fix(identity): fold gate-2 review — cache ancestor walk, suppress fleet-share, document self-report Gate-2 (fresh-eyes review of the built diff) found one blocker + three majors: - BLOCKER (perf): detect_identity_split was wired into wire_status/wire_whoami (the session-start hot path) but re-walked the parent chain — up to 16 untimed `ps` forks — on EVERY call, on a box with 434 wire procs. Cache the ancestor chain in a OnceLock (a process's parentage is immutable): the bounded walk happens once, every later identity check re-reads only the tiny pidfiles. - MAJOR (false positive): the RFC-008 §C "deliberate fleet-share" WIRE_HOME pin (several sessions intentionally sharing one identity) tripped the detector forever. Suppress when session_source is an explicit operator WIRE_HOME pin (env:WIRE_HOME / env:WIRE_HOME_FORCE) — wire's own minted/claude-* resolution, the real bug case, is never suppressed. - MAJOR (undocumented): the new identity_split field was present but no agent was told to check it. Added it to the wire_status + wire_whoami descriptions and to the "ON SESSION START" instructions block (surface it, /mcp reconnect, don't send/pair until resolved) — the wiring that makes the self-report actionable. - MAJOR (test): the regression guard didn't call handle_for_key. Added a hermetic test (tempdir WIRE_HOME) that drives handle_for_key directly and proves a raw 36-char uuid key resolves its home while the sanitized/truncated form misses — fails if handle_for_key ever re-adds sanitize_name. Plus a suppression test. In-situ (real homes + pidfile): fire path fires (operational=merry-spindle ≠ live=verdant-palm); fleet-share WIRE_HOME pin now suppressed (0 banner lines); healthy silent; cached walk still resolves the live session. 647 lib tests green; fmt + clippy clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VxnZEjrcrYR3SAq8Crv8hg * docs(changelog): the two-names identity self-heal + self-report (#351) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VxnZEjrcrYR3SAq8Crv8hg --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Willard reported painfully slow MCP on Windows. Root cause confirmed in code, not theory.
The bug
tool_send— i.e. everywire_send, the primary MCP action — calleddaemon_liveness()purely to populate one diagnostic boolean (daemon_seen=snap.pidfile_alive). Butdaemon_liveness()also runs the machine-wide orphan scan the send path never reads:find_processes_by_cmdline("wire daemon")→ PowerShellGet-CimInstanceon Windows (enumerates all processes, ~1–3s cold)list_sessions()→ readsagent-card.jsonfrom every by-key home (Willard's box has hundreds — Two identities per session: MCP server vs SessionStart-hook WIRE_HOME bind to different DIDs → CLI monitor/pull/tail blind to MCP-paired peers #351's session proliferation), each an NTFS dir readpid_is_aliveper session daemon pid → atasklist.exespawn eachOn Unix that's a cheap
pgrep+/proc; on Windows it's seconds, paid on every send. (The code comment even called these annotations "cheap to compute" — true on Unix, false on Windows.)Fix
New
ensure_up::daemon_pidfile_alive()— onedaemon.pidread + onepid_is_alive(onetaskliston Windows) → the identicaldaemon_seenboolean for a fraction of the cost. Bothtool_sendbranches use it.wire_statuskeeps fulldaemon_liveness()(it legitimately reports orphans).Scope / verification
install-smoke-windows) compile-verifies the platform path (I can't build Windows on the Mac, but CI does).daemon_pidfile_alive_false_without_pidfile;cargo test --lib→ 610; fmt + clippy clean.tasklist/reg/whoamispawns (Windows: painfully slow MCP (and CLI) — every process/identity op shells out to PowerShell/tasklist/reg/whoami #350), and the MCP↔CLI identity split (Two identities per session: MCP server vs SessionStart-hook WIRE_HOME bind to different DIDs → CLI monitor/pull/tail blind to MCP-paired peers #351, unified-WIRE_SESSION_IDworkaround in the meantime).🤖 Generated with Claude Code