{
  "summary": "Pinned S4's failing helper path spawns no CLI. B's dispatcher registers the scoped entry, then queries its home-derived seed IPC name although the isolated B fixture binds only a differently named broker and no docs listener. Linux transport uses abstract-namespace UDS, so a stale filesystem socket is not an evidenced explanation. A failed ServeFor still counts toward B's served total; the missing helperline prevents the fifth, follow-on fetch.",
  "files": [
    {
      "path": "crates/spt-daemon/src/webproxy.rs",
      "description": "409-540 owner ServeFor authorization, registration before docs discovery, and reply; 691-768 requester in-process Brain/QUIC request."
    },
    {
      "path": "crates/spt-daemon/src/wan.rs",
      "description": "1304 ingress helper invocation; 1321-1404 quoted-path scan, spawned thread, canonical local IPC name, and helperline only on Registered."
    },
    {
      "path": "crates/spt-daemon/src/endpoint.rs",
      "description": "23-33 hashes active SPT_HOME-derived PathBuf with DefaultHasher and formats canonical seed name."
    },
    {
      "path": "crates/spt-daemon/tests/twohost_web.rs",
      "description": "256-270 isolated NetHost/broker fixture; 681-1030 B construction, registration witness and counts; 1085-1131 A canonical helper broker and HTTP listener; 1337-1387 helperline wait then fetch."
    },
    {
      "path": "crates/spt-daemon/src/broker.rs",
      "description": "5645-5680 constructor delegation and single local listener bound at supplied name."
    },
    {
      "path": "crates/spt-daemon/src/transport.rs",
      "description": "36-54 Linux abstract namespace/Windows named pipe naming and direct bind/connect."
    },
    {
      "path": "crates/spt-daemon/src/docshost.rs",
      "description": "28-44 process-local bound-port publication/retirement; 477-530 starts listener, publishing only for broker-backed listeners."
    },
    {
      "path": "crates/spt-daemon/src/servehost.rs",
      "description": "238-245 scoped registry write persists before return; 388-405 docs query and strict no-listener response."
    },
    {
      "path": "crates/spt-daemon/src/dispatch.rs",
      "description": "765-777 in-process worker threads and monotonically counted Served outcome; 1234-1236 includes application failures."
    }
  ],
  "architecture": "B test process: fixed-port QUIC NetHost + custom-named local Broker + dispatcher/worker threads, no canonical seed-control or HTTP listener. A helper test process: its own SPT_HOME, canonical-seed-named Broker + QUIC NetHost + broker-backed ephemeral HTTP listener + ingress dispatcher. A's helper thread asks B over QUIC; B performs a local registry write followed by an unavailable canonical-seed IPC query.",
  "report": "Scope: source-only inspection under C:/Users/decid/Documents/projects/spt-core/.worktrees/304-web-helper-rca. No edits, executions, builds, tests, validation, producer launches or assembly changes.\n\n1. No spawned helper executable, argv or child environment exists on this path.\nSOURCE FACT: wan.rs:1304 calls request_quoted_paths following ingress; 1365-1369 captures endpoint::seed_socket_name(), current home and starts std::thread::spawn. That thread calls webproxy::request_serve_for, not Command. webproxy.rs:691-717 creates a pump-mode Brain client, connects to the supplied local broker, resolves/dials B, opens/subscribes a QUIC stream and sends a WebRecord; 739-746 creates ServeFor and calls that helper. B's dispatch runs workers via thread::spawn (dispatch.rs:765-767), dispatches StreamFamily::Web to serve_web (1234-1236), and serve_for itself calls servehost::apply_at and docs_port (webproxy.rs:496,522). The fixture explicitly states that it plays the sending CLI by writing the MSG_OUT row and shipping the envelope, because this package's test does not build the spt binary (twohost_web.rs:858-865). Executable selection, helper CLI argv, and overridden/inherited child env are therefore not applicable; the prior premise must not be retained.\n\n2. Exact environment and socket topology.\nSOURCE FACT: B creates a TempDir and sets process-global SPT_HOME before any owner stores/broker setup (twohost_web.rs:686-687). It constructs spt-twohost-web-b-{pid} and binds it via rig_broker (725-726). rig_broker creates a NetHost with relay disabled, discovery off and fixed port scope, then Broker::bind_in_with_net(name,...) and a broker serving thread (256-270). Broker's constructor binds exactly the passed name via LocalSocketTransport::bind(name) (broker.rs:5645-5650,5659-5680). B's dispatcher uses the same custom broker_name (twohost_web.rs:825-842). No docs listener or canonical seed listener is created by this B fixture.\nSOURCE FACT: The requested seed name instead is spt-daemon-seed-{home_tag}.sock (endpoint.rs:32-33); home_tag hashes spt_home() with DefaultHasher::new and renders a 16-digit hex digest (23-27). Thus B's live broker name and its queried canonical seed name differ by construction, not by a hypothesized race or differing helper subprocess environment.\nSOURCE FACT: A's helper requester first sets its own SPT_HOME (twohost_web.rs:1085-1089), chooses endpoint::seed_socket_name() for its local broker (1111-1116), and starts a broker-backed HTTP listener with explicit port 0 (1117-1124). Other A cells use custom names. Neither the B fixture nor this direct listener construction consults SPT_DOCS_PORT. In the production daemon, by contrast, docs port setup reads the env at daemon.rs:283-289 and starts serving at 310-315; canonical seed-control serving is daemon.rs:482. Ambient SPT_DOCS_PORT cannot create the omitted B listener, and there is no child env override here.\n\n3. Absent endpoint versus stale filesystem path.\nSOURCE FACT: transport.rs:36-38 documents Linux abstract-namespace Unix-domain sockets and Windows named pipes. Both bind and connect convert with to_ns_name::<GenericNamespaced>() (45-54). Despite '.sock' in the logical name, this is not evidence of a filesystem socket path requiring cleanup. B simply does not bind the name it queries.\nRUNTIME INFERENCE: The established Linux Connection refused (os error 111) is consistent with connecting to an unbound abstract-namespace endpoint. Source establishes the omission/name mismatch; it does not establish another process's presence, any endpoint disappearance timing, or the precise runtime owner of that name. Stale pathname residue is neither needed nor supported for this transport. Do not label it ENOENT or claim a stale .sock file without separate runtime evidence. DiscoveryKinds owns the exact io::ErrorKind propagation analysis.\n\n4. Registry insertion is before the failure, not after it.\nSOURCE FACT: webproxy.rs:483-501 constructs AddScoped and invokes apply_at before docs_port at 522. servehost.rs:238-245 adds the reference, applies audience/TTL/origin and saves the registry before returning Entry. A successful insertion followed by discovery failure returns WebErr/Failed without rollback (webproxy.rs:524-528). B's registration witness polls the on-disk registry for HELPER_MSG_ID (twohost_web.rs:964-983), so it can legitimately print that registration succeeded even though A receives no successful helper result. There is no helper child launched before insertion. The asynchronous requester thread necessarily starts before the remote registration; the failing owner port lookup occurs after persistence.\n\n5. Why 4/5 is consistent with this failure.\nSOURCE FACT: A_CELLS=5 is explicitly three W1 exchanges (fetch, range, deny), plus ServeFor plus the subsequent helper fetch (twohost_web.rs:88-107). dispatch.rs:1234-1236 maps every Ok(web outcome), including WebServeOutcome::Failed, to DispatchOutcome::Served, and 771-777 increments the monotonic count. webproxy.rs:524-528 sends the error and returns Ok(Failed), so this application failure can be the fourth served cell. wan.rs:1379-1397 writes a helperline only for Registered, not for the returned WebErr. A waits for a helperline before extracting its served name and performing the final fetch (twohost_web.rs:1337-1387).\nRUNTIME INFERENCE: Given the reported 4/5 and this error, the source-consistent schedule is the three W1 exchanges plus answered-but-failed ServeFor; the fifth helper fetch never starts because no helperline was written. Source proves the dependency and accounting, not the chronological ordering of independent W1 cells.\n\n6. Implications for proposed in-process port read and one-host regression.\nSOURCE FACT: bound_docs_port is a process-local atomic, initially 0/None (docshost.rs:28-37). Only start_serving_with_broker(..., Some(...)) publishes its actual bound IPv4 port, before reporting bind success; its guard retires publication on listener exit (477-530,40-44). B's current fixture has no such listener, so merely replacing seed IPC with bound_docs_port will still yield None in B. Keep the requested strict None hard failure; do not synthesize a configured/default port to make this fixture pass. The regression fixture must first create a real broker-backed B HTTP listener while deliberately leaving the canonical seed endpoint unbound; then assert the ServeFor success and actual returned-port semantics. Parent owns preparation, not implemented here.\nSOURCE FACT: The rig accepts any parseable peer IP and configurable role ports, with per-role derived identities (twohost_web.rs:166-191,212-238); it does not require two physical machines. Its NetHost disables relay/discovery (256-263). INFERENCE: two separate processes on one Linux box using loopback addresses and disjoint role ports can exercise the same QUIC ServeFor -> owner registry -> canonical-seed discovery path, without reproducing a two-host firewall environment. Use process isolation: both B and A helper mutate global SPT_HOME and bound-port publication is process-global. This is source-supported reachability, not an executed reproduction.\nA simpler direct CLI can exercise its own docs-discovery behavior, but cannot demonstrate this exact daemon ServeFor authorization/registry/error-return path: it is not the called code here, and its fallback semantics differ. A focused daemon-side one-host fixture using the same request_serve_for/dispatcher path is the relevant regression. Also note production's separate brain-process architecture (webproxy.rs:519-520; daemon.rs:505) when evaluating any global bound_docs_port replacement: the isolated threaded fixture does not prove availability of broker-process atomic state inside a separate production worker process."
}
