{
  "summary": "Strongest prediction: the immediate controlled=true/driven_by=None observation is a pre-existing or partially cleared control level, not evidence of gen2 replay. Control stamps are two separate persisted mutations, and stream retirement is not a synchronization barrier for the old serving connection’s stamp cleanup. The observed finished Durable peer row plus absent ConnectionBound opener matches ADR-0040’s intended composition. Source alone cannot distinguish transient cleanup from a genuinely retained stale stamp.",
  "files": [
    {
      "path": ".worktrees/asm-304-v3/crates/spt-daemon/src/broker.rs",
      "description": "Controller install/clear, identity- and generation-fenced detach, separate stamp writes, KIND_SESSIONS convergence, and conn-exit ConnectionBound sweep."
    },
    {
      "path": ".worktrees/asm-304-v3/crates/spt-daemon/src/attach.rs",
      "description": "Request-to-controller establishment and explicit EOF cleanup ordering; serving request generation is distinct from daemon generation."
    },
    {
      "path": ".worktrees/asm-304-v3/crates/spt-daemon/src/dispatch.rs",
      "description": "Finished Attach exclusion in worker before serve_attach; retirement after Served; normal Served is silent in DISPATCH anomaly logs."
    },
    {
      "path": ".worktrees/asm-304-v3/crates/spt-daemon/src/nethost.rs",
      "description": "FIN-to-finished transition, eligible-versus-physical stream retention, retired-row opener exclusion."
    },
    {
      "path": ".worktrees/asm-304-v3/crates/spt-daemon/src/conn.rs",
      "description": "Windows poison/cancel/sever sequence causes handler cleanup asynchronously; no direct stamp clear."
    },
    {
      "path": ".worktrees/asm-304-v3/crates/spt-store/src/info.rs",
      "description": "Each control field setter independently locks, reads, mutates, and atomically publishes info.json; lock-free readers may observe the intermediate pair."
    },
    {
      "path": ".worktrees/asm-304-v3/docs/adr/0040-bounded-stream-seat-lifecycle-one-way-terminal.md",
      "description": "Decision 6 and Amendment 1 specify opener-only class, finished-Attach retire-unserved composition, and rig-observation hazards."
    },
    {
      "path": ".worktrees/asm-304-v3/docs/adr/0041-endpoint-lifecycle-truth.md",
      "description": "Decision 4 requires validated control-stamp convergence; every no-session path clears the triple."
    },
    {
      "path": ".worktrees/asm-304-v3/CONTEXT.md",
      "description": "Lines 407–415 define controller/viewer and redispatch terminology; distinguish session continuity from worker reconstruction."
    }
  ],
  "architecture": "The broker owns live controller truth and persisted control markers. A daemon generation owns serving workers/connections, not the surviving PTY/session or durable target stream. Opener-side ConnectionBound teardown sends FIN and removes that opener row; the target’s Durable peer becomes finished and the next dispatcher retires it without establishing a controller. These transport, connection-cleanup, and file-publication paths are asynchronous.",
  "report": "## Scope and evidence limits\nRead-only successor-tree product-path investigation. No tests, builds, commands changing state, lifecycle actions, or edits were performed. User-reported runtime observations were accepted, not reproduced. Test source inspection remained Main’s responsibility. Tool discovery (`read xd://`) exposed only spt_checkpoint, and no Rust LSP tool is available in this agent’s tool inventory; symbol/range searches were used instead.\n\n## Precise control lifetime and ordering\n\n### 1. Establishment\n`OutputLog::become_controller` (broker.rs:2592–2694) takes the previous sink, advances controller_epoch, constructs and installs the new ControllerSink, then calls `stamp_driven_by`, then `on_control_edge`. Thus live controller truth precedes its persisted representation.\n\n`stamp_driven_by` (broker.rs:3608–3627) requests an attachment observation, writes `set_driven_by(controller_by())`, then writes `set_controlled(controller.is_some())`. Errors are best-effort/discarded. `controller_by` and `has_controller` are separate facts (broker.rs:1603–1611). In particular, a local controller can truthfully be controlled=true with driven_by=None; the latter is not a standalone empty-slot predicate (info.rs:707–715).\n\n### 2. Serving-generation death\n`handle_conn` breaks on any `read_frame` error (broker.rs:6085–6090). After leaving the read loop it aborts ceremonies, clones only that connection’s subscribed session logs out of the shared table, and calls `detach_if(&send)` on them (broker.rs:6424–6448). Sessions/PTYS remain alive.\n\n`detach_if` checks current controller connection identity using Arc::ptr_eq (broker.rs:3562–3570). If this connection still owns the slot, `clear_controller` takes the sink and calls the same stamper (broker.rs:2752–2777), so the ordered publications are:\n\n1. in-memory controller=None;\n2. optional diagnostic `CONTROLLER_SLOT_CLOSED` (before persistence finishes);\n3. info.json driven_by=None;\n4. info.json controlled=false;\n5. control-edge work;\n6. `session-detach` lifecycle record (broker.rs:3576–3584).\n\nA newer controller on another connection is not cleared by old connection cleanup. The death of gen1’s process is not itself a barrier proving every old handler has reached step 4.\n\n### 3. Explicit controller detach after opener FIN\n`serve_attach` uses a receiving/controlling Brain connection and a separate wire-forwarding connection (attach.rs:483–503). On its stream EOF it first FINs the reply through `wire`, then feeds the resting Detach edge for a controller, then best-effort calls `brain.detach_session_gen(sid, Some(serve_gen))`, then returns Detached (attach.rs:872–888).\n\n`dispatch_unsubscribe` forwards to `detach_if_gen` (broker.rs:8815–8829). A mismatched request/serve generation leaves the newer controller intact, while viewer cleanup remains connection-identity based; a matching generation still passes through the connection identity check (broker.rs:3547–3570). `serve_gen` is copied from the attach request’s req_gen (attach.rs:417–434), NOT the daemon’s generation number.\n\nIf gen1’s serve worker is already dead, no surviving worker must execute this EOF branch. Old server-side connection cleanup is the independent route that releases its controller.\n\n### 4. ConnectionBound opener cleanup\nOn any opener IPC handler exit, broker.rs:6450–6474 first detaches net subscription seats, then for each tracked ConnectionBound stream executes `host.send_stream(id, [], true)`, `host.retire_stream_terminal(id)`, and emits `STREAM_CONNBOUND_RETIRE:<id>`. Both calls are best-effort. Durable opens do not enter this list.\n\nThe target read pump independently translates EOF/error into `StreamLog::finish` (nethost.rs:1144–1157); finish sets finished=true before enqueueing an EOF notification (nethost.rs:519–528). Therefore observing old opener row2 absent and target row1 finished proves transport/lifecycle progress, not completion of the old target controller’s file writes. Opener and old serving worker use different IPC handlers; their cleanup has no global join.\n\n### 5. Poison and delayed cleanup\nPoison does not clear stamps directly. On Windows `poison_and_cancel` latches poison and calls CancelIoEx; `sever` subsequently calls DisconnectNamedPipe plus another CancelIoEx, after canceled I/O completion, so reads fail and handle_conn can reach its exit cleanup (conn.rs:379–435). Writer failure/reaping provides another path: `reap_dead_controller` removes an exited writer in memory, or deferred-stall-evicts a blocked one, then KIND_SESSIONS convergence writes outside the log lock (broker.rs:2779–2821, 6314–6353).\n\nA genuinely idle writer awaiting its channel, whose IPC EOF has not been processed, is not inferred dead by writer-exit polling (broker.rs:2795–2800). Do not transplant ADR-0040’s approximately 65-second real-QUIC silence bound to this in-process duplex case; Amendment 1 explicitly distinguishes these transports.\n\n## Why the exact observed pair is significant\n\n`set_driven_by` and `set_controlled` each call `mutate_info` independently (info.rs:703–715). Each invocation acquires the per-perch sentinel, reads, modifies one field, and writes the full record (info.rs:500–509). Readers remain lock-free (info.rs:485–487). Per-write atomicity prevents torn JSON, but does not make the two-field transition atomic.\n\nThus a normal clear from {controlled:true, driven_by:Some(old)} publishes {controlled:true, driven_by:None} between the two setter calls. File locking, scheduling, and actual writes can expose that intermediate state to a watcher. It can also be a pre-existing local-controller state. Neither interpretation requires gen2 to execute any attach code.\n\n[INFERENCE] Given the supplied 0.29-second body and immediate true/None sample, the highest-value prediction is that the watcher starts without first establishing a post-gen1 cleared baseline and accepts an existing true level as a resurrection. The observed pair specifically fits the middle of the clear path. Source cannot prove that this particular sample was between the setters; it establishes a concrete sufficient mechanism.\n\n## Can the old finished row replay?\n\nThe row types in T2 are expected: class is opener-side. The accepted/duplex peer stays Durable; ADR-0040 Amendment 1 explicitly rejects filtering the served row by class (ADR:240–245). A claimable finished row is NOT already a retired row.\n\nThe dispatch loop captures `s.finished` when enumerating and passes it into worker (dispatch.rs:729–767). Worker first classifies the pinned opener, then calls `finished_row_is_terminal`; finished && Attach returns Served(\"finished attach: terminal, retired unserved\") BEFORE creating the serving Brain or invoking serve_attach (dispatch.rs:1033–1107; discriminator at 573–574). Completion then invokes net_stream_retire (dispatch.rs:778–785).\n\nEligibility retirement hides a row from stream enumeration and opener queries (nethost.rs:1956–1962, 2005–2038, 2079–2085); terminal retirement physically removes it (2053–2071). A physically retained but eligibility-retired row is not replay-eligible. A finished but not yet retired target Attach may still be claimed, but it is retired unserved. This is precisely ADR-0040 Amendment 1:149–160, which warns that the lifecycle-state worker gate, not an enumeration-time finished filter, is the load-bearing exclusion.\n\n[INFERENCE] Because the supplied T2 says row1 was already finished before gen2, the stale-enumeration variant (worker captured unfinished before FIN) is lower-ranked for this observation. It would require a different ordering than the supplied setup. Do not infer actual execution from row1 being claimable or from it remaining Durable.\n\n## Ranked causal predictions and discriminators\n\n1. **Oracle mistakes old/intermediate level for a gen2 edge — strongest.** Prediction: a raw info.json baseline taken after old controller release has completed and before gen2 starts will be false/None; gen2 will not subsequently establish control from row1. A watcher armed while true/None still exists can immediately declare resurrection without any gen2 attach execution. This directly fits the supplied old-row replay=false observation, subject to what that observation measures.\n2. **Real transient or persistent stale-stamp problem, separate from replay.** Prediction: the old slot is gone, no gen2 attach reaches establishment, but disk still reports control. A short true/None interval is permitted by the two-write implementation. Persistence after completed cleanup and a current authoritative sessions/convergence pass would instead support a stamp-write/convergence issue; that would not establish stream replay.\n3. **Actual old-row replay/steal — lower on supplied evidence.** Prediction: gen2 executes the old request’s attach path, producing an establishment event attributable to the old request/session/connection, or demonstrably changes live controller ownership before any fresh sibling exists. That is incompatible with normal execution of worker’s finished=true gate and would demand evidence about captured finished state, classification, or an alternate attach path.\n\nClean observational sequence for Main’s rig work: keep gen2 stopped; observe old serving controller closure and complete false/None baseline separately from opener row retirement; then start gen2 and watch for a NEW attributable control establishment, retaining the whole pre-sibling interval. Diagnose any failure to reach the baseline as stale cleanup rather than labeling it resurrection. Do not let the fresh sibling overwrite the evidence: T3 after rows3/4 succeed legitimately contains sibling-driven state and cannot retrospectively identify row1 as its cause.\n\nExisting event options:\n- `CONTROLLER_SLOT_CLOSED` (diag-gated, broker.rs:2759–2770) proves a real in-memory close, but precedes stamp completion.\n- `CONN_LIFECYCLE event=session-detach` with was_controller=true is emitted after clear_controller returns (broker.rs:3576–3584); pair it with an actual disk read because setters discard errors.\n- `CONN_LIFECYCLE event=controller-attach` is emitted in become_controller (broker.rs:2604–2614); use conn/session attribution and the before-sibling window. It is not itself proof both disk writes completed.\n- `ATTACH_INTENT_CHOSEN: site=serve_dispatch ... gen=<request-gen>` (attach.rs:407–434) proves the establishment path was reached; it occurs before attach_as_coded, so pair it with broker acceptance/actual slot establishment. Request generation is not gen2’s process generation.\n- `STREAM_CONNBOUND_RETIRE` proves the opener sweep ran, not target stamp convergence.\n- `DISPATCH:<id>` is emitted only for non-Served outcomes (dispatch.rs:797–806). Its absence does NOT prove no replay or successful retirement; ADR-0040:269–272 explicitly prohibits that success oracle. The generic Served counter also counts finished-Attach retire-unserved (dispatch.rs:771–785), so it cannot distinguish establishment from exclusion by itself.\n\nAn authoritative sessions query is not a passive disk probe: KIND_SESSIONS reaps and converges stamps (broker.rs:6314–6353). If used to establish baseline, record that it drives cleanup. The serialized SessionInfo reply exposes controller_by but not has_controller (broker.rs:6355–6365); None alone cannot distinguish empty from a local controller.\n\n## Product invariant boundary\n\nADR-0040 decision 6 (81–86) forbids a dead ConnectionBound viewport from indefinitely pinning the controller; Amendment 1 requires no re-establishment from the finished old Attach. The supplied T2 transport facts and replay=false are consistent with, not contradictory to, that composition.\n\nADR-0041 decision 4 (62–65) requires validated control-stamp convergence, and broker.rs:1174–1251 implements divergence-gated repair with a per-endpoint generation guard against session reap. Lasting controlled=true while the authoritative slot is absent would violate control-status convergence, even if stream replay is perfectly excluded. An instantaneous true/None sample does not demonstrate that lasting violation; the code explicitly publishes that intermediate pair, and true/None can also be a valid local controller.\n\nA separate source-level race worth keeping distinct, not a finding for this red: session snapshots are taken under the log lock and converged later off-lock; the stamp generation described at broker.rs:1144–1153 advances for definitive session reap, not ordinary controller detach. A pre-detach snapshot can therefore be stale relative to a controller edge while its session generation still matches. The next census is intended to converge it. No supplied evidence identifies that interleaving here, and no product change is justified from it alone.\n\nNo final test-versus-product classification is made from source alone. The causal prediction favors a resurrection-oracle precondition defect; an independently measured failure of stamp convergence would remain a real product issue rather than being excused by the replay gate."
}
